mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-06 05:24:13 -04:00
Refactor attachment handling system with enhanced UI (#12085)
* Renamed NewEntryAttachmentsDialog to EditEntryAttachmentsDialog for clarity. * Introduced EditEntryAttachmentsDialog class to manage editing of existing attachments. * Added functionality to preview attachments while editing them. * Enhanced EntryAttachmentsModel with rowByKey method for better key management. * Add image attachment support with zoom functionality. * Add html and markdown detection. * Improve button layout on the attachment section when editing an entry
This commit is contained in:
parent
c4b4be48a5
commit
f2a4cc7e66
54 changed files with 3254 additions and 388 deletions
|
@ -663,6 +663,17 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>AttachmentWidget</name>
|
||||||
|
<message>
|
||||||
|
<source>Attachment Viewer</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Unknown attachment type</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>AutoType</name>
|
<name>AutoType</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -2661,10 +2672,6 @@ This is definitely a bug, please report it to the developers.</source>
|
||||||
<source>No Results</source>
|
<source>No Results</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>Save</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Enter a unique name or overwrite an existing search from the list:</source>
|
<source>Enter a unique name or overwrite an existing search from the list:</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
|
@ -2833,6 +2840,17 @@ Disable safe saves and try again?</source>
|
||||||
<source>Failed to save backup database: %1</source>
|
<source>Failed to save backup database: %1</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Save</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>EditEntryAttachmentsDialog</name>
|
||||||
|
<message>
|
||||||
|
<source>Edit: %1</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>EditEntryWidget</name>
|
<name>EditEntryWidget</name>
|
||||||
|
@ -3904,21 +3922,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</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>
|
<context>
|
||||||
<name>EntryAttachmentsModel</name>
|
<name>EntryAttachmentsModel</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -3944,10 +3947,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||||
<source>Add new attachment</source>
|
<source>Add new attachment</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>Add</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Remove selected attachment</source>
|
<source>Remove selected attachment</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
|
@ -3968,10 +3967,6 @@ This may cause the affected plugins to malfunction.</source>
|
||||||
<source>Save selected attachment to disk</source>
|
<source>Save selected attachment to disk</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>Save</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Select files</source>
|
<source>Select files</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
|
@ -4065,16 +4060,28 @@ Error: %1</source>
|
||||||
Would you like to overwrite the existing attachment?</source>
|
Would you like to overwrite the existing attachment?</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>New</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Preview</source>
|
<source>Preview</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Failed to preview an attachment: Attachment not found</source>
|
<source>Edit</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>New Text Document</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Add file…</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Load from Disk…</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Save…</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
@ -4640,6 +4647,13 @@ You can enable the DuckDuckGo website icon service in the security section of th
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>ImageAttachmentsWidget</name>
|
||||||
|
<message>
|
||||||
|
<source>Zoom:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>ImportWizard</name>
|
<name>ImportWizard</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -6466,25 +6480,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</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>
|
<context>
|
||||||
<name>NixUtils</name>
|
<name>NixUtils</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -7253,15 +7248,15 @@ Do you want to overwrite it?</source>
|
||||||
<context>
|
<context>
|
||||||
<name>PreviewEntryAttachmentsDialog</name>
|
<name>PreviewEntryAttachmentsDialog</name>
|
||||||
<message>
|
<message>
|
||||||
<source>Preview entry attachment</source>
|
<source>Form</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>No preview available</source>
|
<source>Preview: %1</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source>Image format not supported</source>
|
<source>Save…</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
@ -9248,6 +9243,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||||
<source>Tags</source>
|
<source>Tags</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Fit</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>QtIOCompressor</name>
|
<name>QtIOCompressor</name>
|
||||||
|
@ -10182,6 +10181,24 @@ This option is deprecated, use --set-key-file instead.</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>TextAttachmentsEditWidget</name>
|
||||||
|
<message>
|
||||||
|
<source>Preview</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>TextAttachmentsPreviewWidget</name>
|
||||||
|
<message>
|
||||||
|
<source>Form</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Type:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>TotpDialog</name>
|
<name>TotpDialog</name>
|
||||||
<message>
|
<message>
|
||||||
|
|
|
@ -159,8 +159,14 @@ set(gui_SOURCES
|
||||||
gui/entry/EntryAttachmentsModel.cpp
|
gui/entry/EntryAttachmentsModel.cpp
|
||||||
gui/entry/EntryAttachmentsWidget.cpp
|
gui/entry/EntryAttachmentsWidget.cpp
|
||||||
gui/entry/EntryAttributesModel.cpp
|
gui/entry/EntryAttributesModel.cpp
|
||||||
gui/entry/NewEntryAttachmentsDialog.cpp
|
gui/entry/EditEntryAttachmentsDialog.cpp
|
||||||
gui/entry/PreviewEntryAttachmentsDialog.cpp
|
gui/entry/PreviewEntryAttachmentsDialog.cpp
|
||||||
|
gui/entry/attachments/TextAttachmentsWidget.cpp
|
||||||
|
gui/entry/attachments/ImageAttachmentsWidget.cpp
|
||||||
|
gui/entry/attachments/ImageAttachmentsView.cpp
|
||||||
|
gui/entry/attachments/TextAttachmentsPreviewWidget.cpp
|
||||||
|
gui/entry/attachments/TextAttachmentsEditWidget.cpp
|
||||||
|
gui/entry/attachments/AttachmentWidget.cpp
|
||||||
gui/entry/EntryHistoryModel.cpp
|
gui/entry/EntryHistoryModel.cpp
|
||||||
gui/entry/EntryModel.cpp
|
gui/entry/EntryModel.cpp
|
||||||
gui/entry/EntryView.cpp
|
gui/entry/EntryView.cpp
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QMetaProperty>
|
#include <QMetaProperty>
|
||||||
|
#include <QMimeDatabase>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
@ -478,29 +479,57 @@ namespace Tools
|
||||||
|
|
||||||
MimeType toMimeType(const QString& mimeName)
|
MimeType toMimeType(const QString& mimeName)
|
||||||
{
|
{
|
||||||
static QStringList textFormats = {
|
const static QStringList TextFormats = {"text/",
|
||||||
"text/",
|
"application/json",
|
||||||
"application/json",
|
"application/xml",
|
||||||
"application/xml",
|
"application/soap+xml",
|
||||||
"application/soap+xml",
|
"application/x-yaml",
|
||||||
"application/x-yaml",
|
"application/protobuf",
|
||||||
"application/protobuf",
|
"application/x-zerosize"};
|
||||||
};
|
const static QStringList HtmlFormats = {"text/html"};
|
||||||
static QStringList imageFormats = {"image/"};
|
const static QStringList MarkdownFormats = {"text/markdown"};
|
||||||
|
const static QStringList ImageFormats = {"image/"};
|
||||||
|
|
||||||
static auto isCompatible = [](const QString& format, const QStringList& list) {
|
static auto isCompatible = [](const QString& format, const QStringList& list) {
|
||||||
return std::any_of(
|
return std::any_of(
|
||||||
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
|
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isCompatible(mimeName, imageFormats)) {
|
if (isCompatible(mimeName, ImageFormats)) {
|
||||||
return MimeType::Image;
|
return MimeType::Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompatible(mimeName, textFormats)) {
|
if (isCompatible(mimeName, TextFormats)) {
|
||||||
|
if (isCompatible(mimeName, HtmlFormats)) {
|
||||||
|
return MimeType::Html;
|
||||||
|
} else if (isCompatible(mimeName, MarkdownFormats)) {
|
||||||
|
return MimeType::Markdown;
|
||||||
|
}
|
||||||
|
|
||||||
return MimeType::PlainText;
|
return MimeType::PlainText;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MimeType::Unknown;
|
return MimeType::Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MimeType getMimeType(const QByteArray& data)
|
||||||
|
{
|
||||||
|
QMimeDatabase mimeDb;
|
||||||
|
const auto mime = mimeDb.mimeTypeForData(data);
|
||||||
|
return toMimeType(mime.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
MimeType getMimeType(const QFileInfo& fileInfo)
|
||||||
|
{
|
||||||
|
QMimeDatabase mimeDb;
|
||||||
|
const auto mime = mimeDb.mimeTypeForFile(fileInfo);
|
||||||
|
return toMimeType(mime.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTextMimeType(MimeType mimeType)
|
||||||
|
{
|
||||||
|
return mimeType == Tools::MimeType::PlainText || mimeType == Tools::MimeType::Html
|
||||||
|
|| mimeType == Tools::MimeType::Markdown;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Tools
|
} // namespace Tools
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
@ -119,10 +120,16 @@ namespace Tools
|
||||||
{
|
{
|
||||||
Image,
|
Image,
|
||||||
PlainText,
|
PlainText,
|
||||||
|
Html,
|
||||||
|
Markdown,
|
||||||
Unknown
|
Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
MimeType toMimeType(const QString& mimeName);
|
MimeType toMimeType(const QString& mimeName);
|
||||||
|
MimeType getMimeType(const QByteArray& data);
|
||||||
|
MimeType getMimeType(const QFileInfo& fileInfo);
|
||||||
|
bool isTextMimeType(MimeType mimeType);
|
||||||
|
|
||||||
} // namespace Tools
|
} // namespace Tools
|
||||||
|
|
||||||
#endif // KEEPASSX_TOOLS_H
|
#endif // KEEPASSX_TOOLS_H
|
||||||
|
|
52
src/gui/entry/EditEntryAttachmentsDialog.cpp
Normal file
52
src/gui/entry/EditEntryAttachmentsDialog.cpp
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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 "EditEntryAttachmentsDialog.h"
|
||||||
|
#include "ui_EditEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <core/EntryAttachments.h>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
|
EditEntryAttachmentsDialog::EditEntryAttachmentsDialog(QWidget* parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_ui(new Ui::EditEntryAttachmentsDialog)
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
|
||||||
|
connect(m_ui->dialogButtons, &QDialogButtonBox::accepted, this, &EditEntryAttachmentsDialog::accept);
|
||||||
|
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &EditEntryAttachmentsDialog::reject);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditEntryAttachmentsDialog::~EditEntryAttachmentsDialog() = default;
|
||||||
|
|
||||||
|
void EditEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Edit: %1").arg(attachment.name));
|
||||||
|
|
||||||
|
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment EditEntryAttachmentsDialog::getAttachment() const
|
||||||
|
{
|
||||||
|
return m_ui->attachmentWidget->getAttachment();
|
||||||
|
}
|
|
@ -17,32 +17,29 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "attachments/AttachmentTypes.h"
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
class EntryAttachmentsDialog;
|
class EditEntryAttachmentsDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
class QByteArray;
|
|
||||||
class EntryAttachments;
|
class EntryAttachments;
|
||||||
|
|
||||||
class NewEntryAttachmentsDialog : public QDialog
|
class EditEntryAttachmentsDialog : public QDialog
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent = nullptr);
|
explicit EditEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||||
~NewEntryAttachmentsDialog() override;
|
~EditEntryAttachmentsDialog() override;
|
||||||
|
|
||||||
private slots:
|
void setAttachment(attachments::Attachment attachment);
|
||||||
void saveAttachment();
|
attachments::Attachment getAttachment() const;
|
||||||
void fileNameTextChanged(const QString& fileName);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool validateFileName(const QString& fileName, QString& error) const;
|
QScopedPointer<Ui::EditEntryAttachmentsDialog> m_ui;
|
||||||
|
|
||||||
QPointer<EntryAttachments> m_attachments;
|
|
||||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
|
||||||
};
|
};
|
46
src/gui/entry/EditEntryAttachmentsDialog.ui
Normal file
46
src/gui/entry/EditEntryAttachmentsDialog.ui
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>EditEntryAttachmentsDialog</class>
|
||||||
|
<widget class="QDialog" name="EditEntryAttachmentsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>447</width>
|
||||||
|
<height>424</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string notr="true"/>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>AttachmentWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -1,55 +0,0 @@
|
||||||
<?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>
|
|
|
@ -130,6 +130,15 @@ QString EntryAttachmentsModel::keyByIndex(const QModelIndex& index) const
|
||||||
return m_entryAttachments->keys().at(index.row());
|
return m_entryAttachments->keys().at(index.row());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int EntryAttachmentsModel::rowByKey(const QString& key) const
|
||||||
|
{
|
||||||
|
if (!m_entryAttachments) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_entryAttachments->keys().indexOf(key);
|
||||||
|
}
|
||||||
|
|
||||||
void EntryAttachmentsModel::attachmentChange(const QString& key)
|
void EntryAttachmentsModel::attachmentChange(const QString& key)
|
||||||
{
|
{
|
||||||
int row = m_entryAttachments->keys().indexOf(key);
|
int row = m_entryAttachments->keys().indexOf(key);
|
||||||
|
|
|
@ -44,6 +44,7 @@ public:
|
||||||
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
|
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
|
||||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||||
QString keyByIndex(const QModelIndex& index) const;
|
QString keyByIndex(const QModelIndex& index) const;
|
||||||
|
int rowByKey(const QString& key) const;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void attachmentChange(const QString& key);
|
void attachmentChange(const QString& key);
|
||||||
|
|
|
@ -17,23 +17,41 @@
|
||||||
|
|
||||||
#include "EntryAttachmentsWidget.h"
|
#include "EntryAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include "EditEntryAttachmentsDialog.h"
|
||||||
#include "EntryAttachmentsModel.h"
|
#include "EntryAttachmentsModel.h"
|
||||||
#include "NewEntryAttachmentsDialog.h"
|
|
||||||
#include "PreviewEntryAttachmentsDialog.h"
|
#include "PreviewEntryAttachmentsDialog.h"
|
||||||
#include "ui_EntryAttachmentsWidget.h"
|
#include "ui_EntryAttachmentsWidget.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDropEvent>
|
#include <QDropEvent>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMenu>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTemporaryFile>
|
#include <QTemporaryFile>
|
||||||
|
|
||||||
#include "EntryAttachmentsModel.h"
|
|
||||||
#include "core/EntryAttachments.h"
|
#include "core/EntryAttachments.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "gui/FileDialog.h"
|
#include "gui/FileDialog.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const char* DefaultName = "New Attachment";
|
||||||
|
constexpr const char* Suffix = ".txt";
|
||||||
|
|
||||||
|
QString generateUniqueName(const QString& name, const QStringList& existingNames)
|
||||||
|
{
|
||||||
|
uint64_t i = 0;
|
||||||
|
QString newName = QStringLiteral("%1%2").arg(name).arg(Suffix);
|
||||||
|
while (existingNames.contains(newName)) {
|
||||||
|
newName = QStringLiteral("%1_%2%3").arg(name).arg(++i).arg(Suffix);
|
||||||
|
}
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_ui(new Ui::EntryAttachmentsWidget)
|
, m_ui(new Ui::EntryAttachmentsWidget)
|
||||||
|
@ -67,14 +85,30 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
||||||
// clang-format on
|
// clang-format on
|
||||||
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
|
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
|
||||||
|
|
||||||
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(previewSelectedAttachment()));
|
connect(m_ui->attachmentsView, &QAbstractItemView::doubleClicked, [this](const QModelIndex&) {
|
||||||
|
m_readOnly ? previewSelectedAttachment() : editSelectedAttachment();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_ui->attachmentsView->itemDelegate(), &QAbstractItemDelegate::commitData, [this](QWidget* editor) {
|
||||||
|
if (auto lineEdit = qobject_cast<QLineEdit*>(editor)) {
|
||||||
|
auto index = m_attachmentsModel->rowByKey(lineEdit->text());
|
||||||
|
m_ui->attachmentsView->setCurrentIndex(m_attachmentsModel->index(index, 0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
|
connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
|
||||||
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
|
connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
|
||||||
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
|
connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
|
||||||
connect(m_ui->newAttachmentButton, SIGNAL(clicked()), SLOT(newAttachments()));
|
connect(m_ui->editAttachmentButton, SIGNAL(clicked()), SLOT(editSelectedAttachment()));
|
||||||
connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment()));
|
connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment()));
|
||||||
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
|
connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
|
||||||
|
|
||||||
|
auto addButtonMenu = new QMenu(this);
|
||||||
|
addButtonMenu->addAction(tr("New Text Document"), this, &EntryAttachmentsWidget::newAttachments);
|
||||||
|
addButtonMenu->addAction(tr("Load from Disk…"), this, QOverload<>::of(&EntryAttachmentsWidget::insertAttachments));
|
||||||
|
|
||||||
|
m_ui->addAttachmentButton->setMenu(addButtonMenu);
|
||||||
|
|
||||||
updateButtonsVisible();
|
updateButtonsVisible();
|
||||||
updateButtonsEnabled();
|
updateButtonsEnabled();
|
||||||
}
|
}
|
||||||
|
@ -175,19 +209,34 @@ void EntryAttachmentsWidget::newAttachments()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this);
|
// Create a temporary file to allow the user to edit the attachment
|
||||||
if (newEntryDialog.exec() == QDialog::Accepted) {
|
auto newFileName = generateUniqueName(DefaultName, m_entryAttachments->keys());
|
||||||
emit widgetUpdated();
|
m_entryAttachments->set(newFileName, QByteArray());
|
||||||
}
|
|
||||||
|
auto currentIndex = m_attachmentsModel->index(m_attachmentsModel->rowByKey(newFileName), 0);
|
||||||
|
m_ui->attachmentsView->setCurrentIndex(currentIndex);
|
||||||
|
m_ui->attachmentsView->edit(currentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryAttachmentsWidget::previewSelectedAttachment()
|
void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_entryAttachments);
|
Q_ASSERT(m_entryAttachments);
|
||||||
|
|
||||||
const auto index = m_ui->attachmentsView->selectionModel()->selectedIndexes().first();
|
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||||
|
if (!selectionModel) {
|
||||||
|
qWarning() << "Failed to preview an attachment: No selection model";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto indexes = selectionModel->selectedIndexes();
|
||||||
|
if (indexes.empty()) {
|
||||||
|
qWarning() << "Failed to edit an attachment: No attachment selected";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto index = indexes.first();
|
||||||
if (!index.isValid()) {
|
if (!index.isValid()) {
|
||||||
qWarning() << tr("Failed to preview an attachment: Attachment not found");
|
qWarning() << "Failed to preview an attachment: Attachment not found";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +247,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||||
auto data = m_entryAttachments->value(name);
|
auto data = m_entryAttachments->value(name);
|
||||||
|
|
||||||
PreviewEntryAttachmentsDialog previewDialog(this);
|
PreviewEntryAttachmentsDialog previewDialog(this);
|
||||||
previewDialog.setAttachment(name, data);
|
previewDialog.setAttachment({name, data});
|
||||||
|
|
||||||
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
|
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
|
||||||
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
|
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
|
||||||
|
@ -208,7 +257,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||||
&previewDialog,
|
&previewDialog,
|
||||||
[&previewDialog, &name, this](const QString& key) {
|
[&previewDialog, &name, this](const QString& key) {
|
||||||
if (key == name) {
|
if (key == name) {
|
||||||
previewDialog.setAttachment(name, m_entryAttachments->value(name));
|
previewDialog.setAttachment({name, m_entryAttachments->value(name)});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -218,6 +267,51 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
||||||
setFocus();
|
setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EntryAttachmentsWidget::editSelectedAttachment()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_entryAttachments);
|
||||||
|
|
||||||
|
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||||
|
if (!selectionModel) {
|
||||||
|
qWarning() << "Failed to edit an attachment: No selection model";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto selectedIndexes = selectionModel->selectedIndexes();
|
||||||
|
if (selectedIndexes.isEmpty()) {
|
||||||
|
qWarning() << "Failed to edit an attachment: No attachment selected";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto index = selectedIndexes.first();
|
||||||
|
|
||||||
|
if (!index.isValid()) {
|
||||||
|
qWarning() << "Failed to edit 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);
|
||||||
|
|
||||||
|
EditEntryAttachmentsDialog editDialog(this);
|
||||||
|
editDialog.setAttachment({name, data});
|
||||||
|
|
||||||
|
if (editDialog.exec() == QDialog::Accepted) {
|
||||||
|
auto attachment = editDialog.getAttachment();
|
||||||
|
|
||||||
|
// Edit dialog cannot change the name of the attachment
|
||||||
|
if (attachment.name == name) {
|
||||||
|
m_entryAttachments->set(attachment.name, attachment.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set focus back to the widget to allow keyboard navigation
|
||||||
|
setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
void EntryAttachmentsWidget::removeSelectedAttachments()
|
void EntryAttachmentsWidget::removeSelectedAttachments()
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_entryAttachments);
|
Q_ASSERT(m_entryAttachments);
|
||||||
|
@ -346,35 +440,38 @@ void EntryAttachmentsWidget::openSelectedAttachments()
|
||||||
|
|
||||||
void EntryAttachmentsWidget::updateButtonsEnabled()
|
void EntryAttachmentsWidget::updateButtonsEnabled()
|
||||||
{
|
{
|
||||||
const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
|
const auto selectionModel = m_ui->attachmentsView->selectionModel();
|
||||||
|
const bool hasSelection = selectionModel && selectionModel->hasSelection();
|
||||||
|
|
||||||
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
|
m_ui->addAttachmentButton->setEnabled(!m_readOnly);
|
||||||
m_ui->newAttachmentButton->setEnabled(!m_readOnly);
|
|
||||||
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||||
|
|
||||||
|
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly);
|
||||||
|
if (const auto indexes = selectionModel ? selectionModel->selectedIndexes() : QModelIndexList{}; !indexes.empty()) {
|
||||||
|
auto mimeType = Tools::getMimeType(m_entryAttachments->value(m_attachmentsModel->keyByIndex(indexes.first())));
|
||||||
|
m_ui->editAttachmentButton->setEnabled(hasSelection && !m_readOnly && Tools::isTextMimeType(mimeType));
|
||||||
|
}
|
||||||
|
|
||||||
m_ui->saveAttachmentButton->setEnabled(hasSelection);
|
m_ui->saveAttachmentButton->setEnabled(hasSelection);
|
||||||
m_ui->previewAttachmentButton->setEnabled(hasSelection);
|
m_ui->previewAttachmentButton->setEnabled(hasSelection);
|
||||||
m_ui->openAttachmentButton->setEnabled(hasSelection);
|
m_ui->openAttachmentButton->setEnabled(hasSelection);
|
||||||
|
|
||||||
updateSpacers();
|
updateLinesVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryAttachmentsWidget::updateSpacers()
|
void EntryAttachmentsWidget::updateLinesVisibility()
|
||||||
{
|
{
|
||||||
if (m_buttonsVisible && !m_readOnly) {
|
m_ui->editPreviewLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||||
m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding);
|
m_ui->previewRemoveLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||||
} else {
|
|
||||||
m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryAttachmentsWidget::updateButtonsVisible()
|
void EntryAttachmentsWidget::updateButtonsVisible()
|
||||||
{
|
{
|
||||||
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||||
m_ui->newAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
m_ui->editAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||||
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
|
||||||
|
|
||||||
updateSpacers();
|
updateLinesVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
|
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
|
||||||
|
|
|
@ -58,6 +58,7 @@ signals:
|
||||||
private slots:
|
private slots:
|
||||||
void insertAttachments();
|
void insertAttachments();
|
||||||
void newAttachments();
|
void newAttachments();
|
||||||
|
void editSelectedAttachment();
|
||||||
void previewSelectedAttachment();
|
void previewSelectedAttachment();
|
||||||
void removeSelectedAttachments();
|
void removeSelectedAttachments();
|
||||||
void saveSelectedAttachments();
|
void saveSelectedAttachments();
|
||||||
|
@ -68,7 +69,7 @@ private slots:
|
||||||
void attachmentModifiedExternally(const QString& key, const QString& filePath);
|
void attachmentModifiedExternally(const QString& key, const QString& filePath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateSpacers();
|
void updateLinesVisibility();
|
||||||
|
|
||||||
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
|
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>337</width>
|
<width>332</width>
|
||||||
<height>258</height>
|
<height>312</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QWidget" name="actionsWidget" native="true">
|
<widget class="QWidget" name="actionsWidget" native="true">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1,0,0,0,1,0,0">
|
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0,0,0,0,0">
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -60,16 +60,6 @@
|
||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="newAttachmentButton">
|
|
||||||
<property name="enabled">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>New</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="addAttachmentButton">
|
<widget class="QPushButton" name="addAttachmentButton">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
|
@ -79,22 +69,26 @@
|
||||||
<string>Add new attachment</string>
|
<string>Add new attachment</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Add</string>
|
<string>Add file…</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="previewVSpacer">
|
<widget class="QPushButton" name="editAttachmentButton">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="Line" name="editPreviewLine">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
</widget>
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>40</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="previewAttachmentButton">
|
<widget class="QPushButton" name="previewAttachmentButton">
|
||||||
|
@ -128,22 +122,19 @@
|
||||||
<string>Save selected attachment to disk</string>
|
<string>Save selected attachment to disk</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Save</string>
|
<string>Save…</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer_2">
|
<widget class="Line" name="previewRemoveLine">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
</widget>
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>173</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="removeAttachmentButton">
|
<widget class="QPushButton" name="removeAttachmentButton">
|
||||||
|
@ -164,7 +155,7 @@
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeType">
|
<property name="sizeType">
|
||||||
<enum>QSizePolicy::Minimum</enum>
|
<enum>QSizePolicy::Expanding</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
* 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->setStandardButtons(QDialogButtonBox::Ok | 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,117 +16,48 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "PreviewEntryAttachmentsDialog.h"
|
#include "PreviewEntryAttachmentsDialog.h"
|
||||||
#include "ui_EntryAttachmentsDialog.h"
|
#include "ui_PreviewEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QTextCursor>
|
|
||||||
#include <QtDebug>
|
|
||||||
|
|
||||||
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
|
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
|
||||||
: QDialog(parent)
|
: QDialog(parent)
|
||||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
, m_ui(new Ui::PreviewEntryAttachmentsDialog)
|
||||||
{
|
{
|
||||||
m_ui->setupUi(this);
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
setWindowTitle(tr("Preview entry attachment"));
|
|
||||||
// Disable the help button in the title bar
|
// Disable the help button in the title bar
|
||||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
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
|
// Initialize dialog buttons
|
||||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
|
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
|
||||||
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
|
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
|
||||||
closeButton->setDefault(true);
|
closeButton->setDefault(true);
|
||||||
|
|
||||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
auto saveButton = m_ui->dialogButtons->button(QDialogButtonBox::Save);
|
||||||
|
saveButton->setText(tr("Save…"));
|
||||||
|
|
||||||
|
connect(m_ui->dialogButtons, &QDialogButtonBox::rejected, this, &PreviewEntryAttachmentsDialog::reject);
|
||||||
connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
|
connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) {
|
||||||
auto pressedButton = m_ui->dialogButtons->standardButton(button);
|
auto pressedButton = m_ui->dialogButtons->standardButton(button);
|
||||||
|
|
||||||
|
const auto attachment = m_ui->attachmentWidget->getAttachment();
|
||||||
if (pressedButton == QDialogButtonBox::Open) {
|
if (pressedButton == QDialogButtonBox::Open) {
|
||||||
emit openAttachment(m_name);
|
emit openAttachment(attachment.name);
|
||||||
} else if (pressedButton == QDialogButtonBox::Save) {
|
} else if (pressedButton == QDialogButtonBox::Save) {
|
||||||
emit saveAttachment(m_name);
|
emit saveAttachment(attachment.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
|
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
|
||||||
|
|
||||||
void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data)
|
void PreviewEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
|
||||||
{
|
{
|
||||||
m_name = name;
|
setWindowTitle(tr("Preview: %1").arg(attachment.name));
|
||||||
m_ui->titleEdit->setText(m_name);
|
|
||||||
|
|
||||||
m_type = attachmentType(data);
|
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadOnly);
|
||||||
m_data = data;
|
|
||||||
m_imageCache = QImage();
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (m_imageCache.isNull() && !m_imageCache.loadFromData(data)) {
|
|
||||||
updateTextAttachment(tr("Image format not supported").toUtf8());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateImageAttachment(m_imageCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PreviewEntryAttachmentsDialog::updateImageAttachment(const QImage& image)
|
|
||||||
{
|
|
||||||
m_ui->attachmentTextEdit->clear();
|
|
||||||
auto cursor = m_ui->attachmentTextEdit->textCursor();
|
|
||||||
|
|
||||||
cursor.insertImage(image.scaled(calculateImageSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize PreviewEntryAttachmentsDialog::calculateImageSize()
|
|
||||||
{
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,16 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/Tools.h"
|
#include "attachments/AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <core/Tools.h>
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
class EntryAttachmentsDialog;
|
class PreviewEntryAttachmentsDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreviewEntryAttachmentsDialog : public QDialog
|
class PreviewEntryAttachmentsDialog : public QDialog
|
||||||
|
@ -35,29 +37,12 @@ public:
|
||||||
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
|
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||||
~PreviewEntryAttachmentsDialog() override;
|
~PreviewEntryAttachmentsDialog() override;
|
||||||
|
|
||||||
void setAttachment(const QString& name, const QByteArray& data);
|
void setAttachment(attachments::Attachment attachment);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void openAttachment(const QString& name);
|
void openAttachment(const QString& name);
|
||||||
void saveAttachment(const QString& name);
|
void saveAttachment(const QString& name);
|
||||||
|
|
||||||
protected:
|
|
||||||
void resizeEvent(QResizeEvent* event) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Tools::MimeType attachmentType(const QByteArray& data) const;
|
QScopedPointer<Ui::PreviewEntryAttachmentsDialog> m_ui;
|
||||||
|
|
||||||
void update();
|
|
||||||
void updateTextAttachment(const QByteArray& data);
|
|
||||||
void updateImageAttachment(const QByteArray& data);
|
|
||||||
void updateImageAttachment(const QImage& data);
|
|
||||||
|
|
||||||
QSize calculateImageSize();
|
|
||||||
|
|
||||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
|
||||||
|
|
||||||
QString m_name;
|
|
||||||
QByteArray m_data;
|
|
||||||
QImage m_imageCache;
|
|
||||||
Tools::MimeType m_type{Tools::MimeType::Unknown};
|
|
||||||
};
|
};
|
||||||
|
|
58
src/gui/entry/PreviewEntryAttachmentsDialog.ui
Normal file
58
src/gui/entry/PreviewEntryAttachmentsDialog.ui
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>PreviewEntryAttachmentsDialog</class>
|
||||||
|
<widget class="QDialog" name="PreviewEntryAttachmentsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>557</width>
|
||||||
|
<height>454</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="AttachmentWidget" name="attachmentWidget" native="true">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="dialogButtons">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>AttachmentWidget</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header location="global">gui/entry/attachments/AttachmentWidget.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
37
src/gui/entry/attachments/AttachmentTypes.h
Normal file
37
src/gui/entry/attachments/AttachmentTypes.h
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace attachments
|
||||||
|
{
|
||||||
|
struct Attachment
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
QByteArray data;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class OpenMode
|
||||||
|
{
|
||||||
|
ReadOnly,
|
||||||
|
ReadWrite
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace attachments
|
89
src/gui/entry/attachments/AttachmentWidget.cpp
Normal file
89
src/gui/entry/attachments/AttachmentWidget.cpp
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentWidget.h"
|
||||||
|
|
||||||
|
#include "ImageAttachmentsWidget.h"
|
||||||
|
#include "TextAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <core/Tools.h>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
AttachmentWidget::AttachmentWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Attachment Viewer"));
|
||||||
|
|
||||||
|
auto verticalLayout = new QVBoxLayout(this);
|
||||||
|
verticalLayout->setSpacing(0);
|
||||||
|
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
|
||||||
|
verticalLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
verticalLayout->setAlignment(Qt::AlignCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachmentWidget::~AttachmentWidget() = default;
|
||||||
|
|
||||||
|
void AttachmentWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
|
||||||
|
{
|
||||||
|
m_attachment = std::move(attachment);
|
||||||
|
m_mode = mode;
|
||||||
|
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AttachmentWidget::updateUi()
|
||||||
|
{
|
||||||
|
auto type = Tools::getMimeType(m_attachment.data);
|
||||||
|
|
||||||
|
if (m_attachmentWidget) {
|
||||||
|
layout()->removeWidget(m_attachmentWidget);
|
||||||
|
m_attachmentWidget->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Tools::isTextMimeType(type)) {
|
||||||
|
auto widget = new TextAttachmentsWidget(this);
|
||||||
|
widget->openAttachment(m_attachment, m_mode);
|
||||||
|
|
||||||
|
m_attachmentWidget = widget;
|
||||||
|
} else if (type == Tools::MimeType::Image) {
|
||||||
|
auto widget = new ImageAttachmentsWidget(this);
|
||||||
|
widget->openAttachment(m_attachment, m_mode);
|
||||||
|
|
||||||
|
m_attachmentWidget = widget;
|
||||||
|
} else {
|
||||||
|
auto label = new QLabel(tr("Unknown attachment type"), this);
|
||||||
|
label->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
m_attachmentWidget = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(m_attachmentWidget);
|
||||||
|
m_attachmentWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||||
|
layout()->addWidget(m_attachmentWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment AttachmentWidget::getAttachment() const
|
||||||
|
{
|
||||||
|
// Text attachments can be edited at this time so pass this call forward
|
||||||
|
if (auto textWidget = qobject_cast<TextAttachmentsWidget*>(m_attachmentWidget)) {
|
||||||
|
return textWidget->getAttachment();
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_attachment;
|
||||||
|
}
|
67
src/gui/entry/attachments/AttachmentWidget.h
Normal file
67
src/gui/entry/attachments/AttachmentWidget.h
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <core/Tools.h>
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class AttachmentWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The AttachmentWidget class provides a way to manage attachments in a GUI application.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class AttachmentWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AttachmentWidget(QWidget* parent = nullptr);
|
||||||
|
~AttachmentWidget() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Opens an attachment in the specified mode.
|
||||||
|
*
|
||||||
|
* @param attachment - The attachment to be opened.
|
||||||
|
* @param mode - The mode in which to open the attachment (read-only or read-write).
|
||||||
|
*/
|
||||||
|
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the current attachment.
|
||||||
|
*
|
||||||
|
* @return Attachment - The current attachment.
|
||||||
|
*/
|
||||||
|
attachments::Attachment getAttachment() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateUi();
|
||||||
|
|
||||||
|
QPointer<QWidget> m_attachmentWidget;
|
||||||
|
|
||||||
|
attachments::Attachment m_attachment;
|
||||||
|
attachments::OpenMode m_mode;
|
||||||
|
};
|
98
src/gui/entry/attachments/ImageAttachmentsView.cpp
Normal file
98
src/gui/entry/attachments/ImageAttachmentsView.cpp
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* 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 "ImageAttachmentsView.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
ImageAttachmentsView::ImageAttachmentsView(QWidget* parent)
|
||||||
|
: QGraphicsView(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::wheelEvent(QWheelEvent* event)
|
||||||
|
{
|
||||||
|
if (event->modifiers() == Qt::ControlModifier) {
|
||||||
|
emit ctrlWheelEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsView::wheelEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::resizeEvent(QResizeEvent* event)
|
||||||
|
{
|
||||||
|
QGraphicsView::resizeEvent(event);
|
||||||
|
|
||||||
|
if (m_autoFitInView) {
|
||||||
|
fitSceneInView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::showEvent(QShowEvent* event)
|
||||||
|
{
|
||||||
|
if (m_autoFitInView) {
|
||||||
|
fitSceneInView();
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsView::showEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::fitSceneInView()
|
||||||
|
{
|
||||||
|
if (auto scene = ImageAttachmentsView::scene()) {
|
||||||
|
ImageAttachmentsView::fitInView(scene->itemsBoundingRect(), Qt::KeepAspectRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::enableAutoFitInView()
|
||||||
|
{
|
||||||
|
m_autoFitInView = true;
|
||||||
|
fitSceneInView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsView::disableAutoFitInView()
|
||||||
|
{
|
||||||
|
m_autoFitInView = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageAttachmentsView::isAutoFitInViewActivated() const
|
||||||
|
{
|
||||||
|
return m_autoFitInView;
|
||||||
|
}
|
||||||
|
|
||||||
|
double ImageAttachmentsView::calculateFitInViewFactor() const
|
||||||
|
{
|
||||||
|
auto viewPort = viewport();
|
||||||
|
if (auto currentScene = scene(); currentScene && viewPort) {
|
||||||
|
const auto itemsRect = currentScene->itemsBoundingRect().size();
|
||||||
|
|
||||||
|
// If the image rect is empty
|
||||||
|
if (itemsRect.isEmpty()) {
|
||||||
|
return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto viewPortSize = viewPort->size();
|
||||||
|
// Calculate the zoom factor based on the current size and the image rect
|
||||||
|
return std::min(viewPortSize.width() / itemsRect.width(), viewPortSize.height() / itemsRect.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
}
|
46
src/gui/entry/attachments/ImageAttachmentsView.h
Normal file
46
src/gui/entry/attachments/ImageAttachmentsView.h
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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 <QGraphicsView>
|
||||||
|
|
||||||
|
class ImageAttachmentsView : public QGraphicsView
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ImageAttachmentsView(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void enableAutoFitInView();
|
||||||
|
void disableAutoFitInView();
|
||||||
|
bool isAutoFitInViewActivated() const;
|
||||||
|
|
||||||
|
double calculateFitInViewFactor() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void ctrlWheelEvent(QWheelEvent* event);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void wheelEvent(QWheelEvent* event) override;
|
||||||
|
void showEvent(QShowEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void fitSceneInView();
|
||||||
|
|
||||||
|
bool m_autoFitInView = false;
|
||||||
|
};
|
258
src/gui/entry/attachments/ImageAttachmentsWidget.cpp
Normal file
258
src/gui/entry/attachments/ImageAttachmentsWidget.cpp
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
/*
|
||||||
|
* 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 "ImageAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include "ui_ImageAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QGraphicsScene>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QSizeF>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Predefined zoom levels must be in ascending order
|
||||||
|
constexpr std::array ZoomList = {0.25, 0.5, 0.75, 1.0, 2.0};
|
||||||
|
constexpr double WheelZoomStep = 1.1;
|
||||||
|
|
||||||
|
const QString FitText = QObject::tr("Fit");
|
||||||
|
|
||||||
|
QString formatZoomText(double zoomFactor)
|
||||||
|
{
|
||||||
|
return QString("%1%").arg(QString::number(zoomFactor * 100, 'f', 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
double parseZoomText(const QString& zoomText)
|
||||||
|
{
|
||||||
|
auto zoomTextTrimmed = zoomText.trimmed();
|
||||||
|
|
||||||
|
if (auto percentIndex = zoomTextTrimmed.indexOf('%'); percentIndex != -1) {
|
||||||
|
// Remove the '%' character and parse the number
|
||||||
|
zoomTextTrimmed = zoomTextTrimmed.left(percentIndex).trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
double zoomFactor = zoomTextTrimmed.toDouble(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
qWarning() << "Failed to parse zoom text:" << zoomText;
|
||||||
|
return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
}
|
||||||
|
return zoomFactor / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ImageAttachmentsWidget::ImageAttachmentsWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_ui(new Ui::ImageAttachmentsWidget)
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
m_scene = new QGraphicsScene(this);
|
||||||
|
m_ui->imagesView->setScene(m_scene);
|
||||||
|
m_ui->imagesView->setDragMode(QGraphicsView::ScrollHandDrag);
|
||||||
|
m_ui->imagesView->setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
|
||||||
|
m_ui->imagesView->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
|
||||||
|
|
||||||
|
connect(m_ui->imagesView, &ImageAttachmentsView::ctrlWheelEvent, this, &ImageAttachmentsWidget::onWheelZoomEvent);
|
||||||
|
|
||||||
|
static_assert(ZoomList.size() > 0, "ZoomList must not be empty");
|
||||||
|
static_assert(ZoomList.front() < ZoomList.back(), "ZoomList must be in ascending order");
|
||||||
|
m_zoomHelper = new ZoomHelper(1.0, WheelZoomStep, ZoomList.front(), ZoomList.back(), this);
|
||||||
|
connect(m_zoomHelper, &ZoomHelper::zoomChanged, this, &ImageAttachmentsWidget::onZoomFactorChanged);
|
||||||
|
|
||||||
|
initZoomComboBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageAttachmentsWidget::~ImageAttachmentsWidget() = default;
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::initZoomComboBox()
|
||||||
|
{
|
||||||
|
m_ui->zoomComboBox->clear();
|
||||||
|
|
||||||
|
auto textWidth = m_ui->zoomComboBox->fontMetrics().horizontalAdvance(FitText);
|
||||||
|
|
||||||
|
m_ui->zoomComboBox->addItem(FitText, 0.0);
|
||||||
|
|
||||||
|
for (const auto& zoom : ZoomList) {
|
||||||
|
auto zoomText = formatZoomText(zoom);
|
||||||
|
textWidth = std::max(textWidth, m_ui->zoomComboBox->fontMetrics().horizontalAdvance(zoomText));
|
||||||
|
|
||||||
|
m_ui->zoomComboBox->addItem(zoomText, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int minWidth = 50;
|
||||||
|
m_ui->zoomComboBox->setMinimumWidth(textWidth + minWidth);
|
||||||
|
|
||||||
|
connect(m_ui->zoomComboBox, &QComboBox::currentTextChanged, this, &ImageAttachmentsWidget::onZoomChanged);
|
||||||
|
|
||||||
|
connect(m_ui->zoomComboBox->lineEdit(), &QLineEdit::editingFinished, [this]() {
|
||||||
|
onZoomChanged(m_ui->zoomComboBox->lineEdit()->text());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fit by default
|
||||||
|
m_ui->zoomComboBox->setCurrentIndex(m_ui->zoomComboBox->findData(0.0));
|
||||||
|
onZoomChanged(m_ui->zoomComboBox->currentText());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::onWheelZoomEvent(QWheelEvent* event)
|
||||||
|
{
|
||||||
|
m_ui->imagesView->disableAutoFitInView();
|
||||||
|
|
||||||
|
auto finInViewFactor = m_ui->imagesView->calculateFitInViewFactor();
|
||||||
|
// Limit the fit-in-view factor to a maximum of 100%
|
||||||
|
m_zoomHelper->setMinZoomOutFactor(std::isnan(finInViewFactor) ? 1.0 : std::min(finInViewFactor, 1.0));
|
||||||
|
|
||||||
|
event->angleDelta().y() > 0 ? m_zoomHelper->zoomIn() : m_zoomHelper->zoomOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::onZoomFactorChanged(double zoomFactor)
|
||||||
|
{
|
||||||
|
if (m_ui->imagesView->isAutoFitInViewActivated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ui->imagesView->setTransform(QTransform::fromScale(zoomFactor, zoomFactor));
|
||||||
|
|
||||||
|
// Update the zoom combo box to reflect the current zoom factor
|
||||||
|
if (!m_ui->zoomComboBox->lineEdit()->hasFocus()) {
|
||||||
|
m_ui->zoomComboBox->setCurrentText(formatZoomText(zoomFactor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::onZoomChanged(const QString& zoomText)
|
||||||
|
{
|
||||||
|
auto zoomFactor = 1.0;
|
||||||
|
|
||||||
|
if (zoomText == FitText) {
|
||||||
|
m_ui->imagesView->enableAutoFitInView();
|
||||||
|
|
||||||
|
zoomFactor = std::min(m_ui->imagesView->calculateFitInViewFactor(), zoomFactor);
|
||||||
|
} else {
|
||||||
|
zoomFactor = parseZoomText(zoomText);
|
||||||
|
if (!std::isnan(zoomFactor)) {
|
||||||
|
m_ui->imagesView->disableAutoFitInView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::isnan(zoomFactor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_zoomHelper->setZoomFactor(zoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
|
||||||
|
{
|
||||||
|
m_attachment = std::move(attachment);
|
||||||
|
|
||||||
|
if (mode == attachments::OpenMode::ReadWrite) {
|
||||||
|
qWarning() << "Read-write mode is not supported for image attachments";
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageAttachmentsWidget::loadImage()
|
||||||
|
{
|
||||||
|
QPixmap pixmap{};
|
||||||
|
pixmap.loadFromData(m_attachment.data);
|
||||||
|
if (pixmap.isNull()) {
|
||||||
|
qWarning() << "Failed to load image from data";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_scene->clear();
|
||||||
|
m_scene->addPixmap(std::move(pixmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment ImageAttachmentsWidget::getAttachment() const
|
||||||
|
{
|
||||||
|
return m_attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom helper
|
||||||
|
ZoomHelper::ZoomHelper(double zoomFactor, double step, double min, double max, QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_step(step)
|
||||||
|
, m_minZoomOut(min)
|
||||||
|
, m_maxZoomIn(max)
|
||||||
|
{
|
||||||
|
Q_ASSERT(!std::isnan(step) && step > 0);
|
||||||
|
Q_ASSERT(!std::isnan(zoomFactor));
|
||||||
|
Q_ASSERT(!std::isnan(min));
|
||||||
|
Q_ASSERT(!std::isnan(max));
|
||||||
|
Q_ASSERT(min < max);
|
||||||
|
|
||||||
|
setZoomFactor(zoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoomHelper::zoomIn()
|
||||||
|
{
|
||||||
|
const auto newZoomFactor = m_zoomFactor * m_step;
|
||||||
|
setZoomFactor(std::isgreater(newZoomFactor, m_maxZoomIn) ? m_zoomFactor : newZoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoomHelper::zoomOut()
|
||||||
|
{
|
||||||
|
const auto newZoomFactor = m_zoomFactor / m_step;
|
||||||
|
setZoomFactor(std::isless(newZoomFactor, m_minZoomOut) ? m_zoomFactor : newZoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoomHelper::setZoomFactor(double zoomFactor)
|
||||||
|
{
|
||||||
|
if (std::isnan(zoomFactor)) {
|
||||||
|
qWarning() << "Failed to set NaN zoom factor";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto oldValue = std::exchange(m_zoomFactor, zoomFactor);
|
||||||
|
if (std::isless(oldValue, m_zoomFactor) || std::isgreater(oldValue, m_zoomFactor)) {
|
||||||
|
Q_EMIT zoomChanged(m_zoomFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double ZoomHelper::getZoomFactor() const
|
||||||
|
{
|
||||||
|
return m_zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoomHelper::setMinZoomOutFactor(double zoomFactor)
|
||||||
|
{
|
||||||
|
if (std::isgreater(zoomFactor, m_maxZoomIn)) {
|
||||||
|
std::swap(m_maxZoomIn, zoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_minZoomOut = zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ZoomHelper::setMaxZoomInFactor(double zoomFactor)
|
||||||
|
{
|
||||||
|
if (std::isless(zoomFactor, m_minZoomOut)) {
|
||||||
|
std::swap(m_minZoomOut, zoomFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_maxZoomIn = zoomFactor;
|
||||||
|
}
|
85
src/gui/entry/attachments/ImageAttachmentsWidget.h
Normal file
85
src/gui/entry/attachments/ImageAttachmentsWidget.h
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class ImageAttachmentsWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QGraphicsView;
|
||||||
|
class QGraphicsScene;
|
||||||
|
|
||||||
|
class ZoomHelper : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ZoomHelper(double zoomFactor, double step, double min, double max, QObject* parent = nullptr);
|
||||||
|
|
||||||
|
void zoomIn();
|
||||||
|
void zoomOut();
|
||||||
|
|
||||||
|
void setZoomFactor(double zoomFactor);
|
||||||
|
double getZoomFactor() const;
|
||||||
|
|
||||||
|
void setMinZoomOutFactor(double zoomFactor);
|
||||||
|
void setMaxZoomInFactor(double zoomFactor);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void zoomChanged(double zoomFactor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
double m_zoomFactor;
|
||||||
|
double m_step;
|
||||||
|
|
||||||
|
double m_minZoomOut;
|
||||||
|
double m_maxZoomIn;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageAttachmentsWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ImageAttachmentsWidget(QWidget* parent = nullptr);
|
||||||
|
~ImageAttachmentsWidget() override;
|
||||||
|
|
||||||
|
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||||
|
attachments::Attachment getAttachment() const;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onZoomChanged(const QString& zoomText);
|
||||||
|
void onWheelZoomEvent(QWheelEvent* event);
|
||||||
|
void onZoomFactorChanged(double zoomFactor);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loadImage();
|
||||||
|
|
||||||
|
void initZoomComboBox();
|
||||||
|
|
||||||
|
QScopedPointer<Ui::ImageAttachmentsWidget> m_ui;
|
||||||
|
attachments::Attachment m_attachment;
|
||||||
|
|
||||||
|
QPointer<QGraphicsScene> m_scene;
|
||||||
|
QPointer<ZoomHelper> m_zoomHelper;
|
||||||
|
};
|
90
src/gui/entry/attachments/ImageAttachmentsWidget.ui
Normal file
90
src/gui/entry/attachments/ImageAttachmentsWidget.ui
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>ImageAttachmentsWidget</class>
|
||||||
|
<widget class="QWidget" name="ImageAttachmentsWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<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>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="zoomLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Zoom:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="zoomComboBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="editable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="ImageAttachmentsView" name="imagesView"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>ImageAttachmentsView</class>
|
||||||
|
<extends>QGraphicsView</extends>
|
||||||
|
<header location="global">gui/entry/attachments/ImageAttachmentsView.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
54
src/gui/entry/attachments/TextAttachmentsEditWidget.cpp
Normal file
54
src/gui/entry/attachments/TextAttachmentsEditWidget.cpp
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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 "TextAttachmentsEditWidget.h"
|
||||||
|
#include "ui_TextAttachmentsEditWidget.h"
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <qwidget.h>
|
||||||
|
|
||||||
|
TextAttachmentsEditWidget::TextAttachmentsEditWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_ui(new Ui::TextAttachmentsEditWidget())
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
connect(m_ui->attachmentsTextEdit, &QTextEdit::textChanged, this, &TextAttachmentsEditWidget::textChanged);
|
||||||
|
connect(m_ui->previewPushButton, &QPushButton::clicked, this, &TextAttachmentsEditWidget::previewButtonClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextAttachmentsEditWidget::~TextAttachmentsEditWidget() = default;
|
||||||
|
|
||||||
|
void TextAttachmentsEditWidget::openAttachment(attachments::Attachment attachments, attachments::OpenMode mode)
|
||||||
|
{
|
||||||
|
m_attachment = std::move(attachments);
|
||||||
|
m_mode = mode;
|
||||||
|
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment TextAttachmentsEditWidget::getAttachment() const
|
||||||
|
{
|
||||||
|
return {m_attachment.name, m_ui->attachmentsTextEdit->toPlainText().toUtf8()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsEditWidget::updateUi()
|
||||||
|
{
|
||||||
|
m_ui->attachmentsTextEdit->setPlainText(m_attachment.data);
|
||||||
|
m_ui->attachmentsTextEdit->setReadOnly(m_mode == attachments::OpenMode::ReadOnly);
|
||||||
|
}
|
51
src/gui/entry/attachments/TextAttachmentsEditWidget.h
Normal file
51
src/gui/entry/attachments/TextAttachmentsEditWidget.h
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class TextAttachmentsEditWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextAttachmentsEditWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TextAttachmentsEditWidget(QWidget* parent = nullptr);
|
||||||
|
~TextAttachmentsEditWidget() override;
|
||||||
|
|
||||||
|
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||||
|
attachments::Attachment getAttachment() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void textChanged();
|
||||||
|
void previewButtonClicked(bool isChecked);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateUi();
|
||||||
|
|
||||||
|
QScopedPointer<Ui::TextAttachmentsEditWidget> m_ui;
|
||||||
|
|
||||||
|
attachments::Attachment m_attachment;
|
||||||
|
attachments::OpenMode m_mode;
|
||||||
|
};
|
66
src/gui/entry/attachments/TextAttachmentsEditWidget.ui
Normal file
66
src/gui/entry/attachments/TextAttachmentsEditWidget.ui
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>TextAttachmentsEditWidget</class>
|
||||||
|
<widget class="QWidget" name="TextAttachmentsEditWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<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>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="previewPushButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Preview</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTextEdit" name="attachmentsTextEdit"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
133
src/gui/entry/attachments/TextAttachmentsPreviewWidget.cpp
Normal file
133
src/gui/entry/attachments/TextAttachmentsPreviewWidget.cpp
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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 "TextAttachmentsPreviewWidget.h"
|
||||||
|
#include "ui_TextAttachmentsPreviewWidget.h"
|
||||||
|
|
||||||
|
#include <core/Tools.h>
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMetaEnum>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr TextAttachmentsPreviewWidget::PreviewTextType ConvertToPreviewTextType(Tools::MimeType mimeType) noexcept
|
||||||
|
{
|
||||||
|
if (mimeType == Tools::MimeType::Html) {
|
||||||
|
return TextAttachmentsPreviewWidget::Html;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||||
|
if (mimeType == Tools::MimeType::Markdown) {
|
||||||
|
return TextAttachmentsPreviewWidget::Markdown;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return TextAttachmentsPreviewWidget::PlainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TextAttachmentsPreviewWidget::TextAttachmentsPreviewWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_ui(new Ui::TextAttachmentsPreviewWidget())
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
|
||||||
|
initTypeCombobox();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextAttachmentsPreviewWidget::~TextAttachmentsPreviewWidget() = default;
|
||||||
|
|
||||||
|
void TextAttachmentsPreviewWidget::openAttachment(attachments::Attachment attachments, attachments::OpenMode mode)
|
||||||
|
{
|
||||||
|
if (mode == attachments::OpenMode::ReadWrite) {
|
||||||
|
qWarning() << "Read-write mode is not supported for text preview attachments";
|
||||||
|
}
|
||||||
|
|
||||||
|
m_attachment = std::move(attachments);
|
||||||
|
|
||||||
|
updateUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment TextAttachmentsPreviewWidget::getAttachment() const
|
||||||
|
{
|
||||||
|
return m_attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsPreviewWidget::initTypeCombobox()
|
||||||
|
{
|
||||||
|
QStandardItemModel* model = new QStandardItemModel(this);
|
||||||
|
|
||||||
|
const auto metaEnum = QMetaEnum::fromType<TextAttachmentsPreviewWidget::PreviewTextType>();
|
||||||
|
for (int i = 0; i < metaEnum.keyCount(); ++i) {
|
||||||
|
QStandardItem* item = new QStandardItem(metaEnum.key(i));
|
||||||
|
item->setData(metaEnum.value(i), Qt::UserRole);
|
||||||
|
model->appendRow(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSortFilterProxyModel* filterProxyMode = new QSortFilterProxyModel(this);
|
||||||
|
filterProxyMode->setSourceModel(model);
|
||||||
|
filterProxyMode->sort(0, Qt::SortOrder::DescendingOrder);
|
||||||
|
m_ui->typeComboBox->setModel(filterProxyMode);
|
||||||
|
|
||||||
|
connect(m_ui->typeComboBox,
|
||||||
|
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
|
this,
|
||||||
|
&TextAttachmentsPreviewWidget::onTypeChanged);
|
||||||
|
|
||||||
|
m_ui->typeComboBox->setCurrentIndex(m_ui->typeComboBox->findData(PlainText));
|
||||||
|
|
||||||
|
onTypeChanged(m_ui->typeComboBox->currentIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsPreviewWidget::updateUi()
|
||||||
|
{
|
||||||
|
if (!m_attachment.name.isEmpty()) {
|
||||||
|
const auto mimeType = Tools::getMimeType(QFileInfo(m_attachment.name));
|
||||||
|
|
||||||
|
auto index = m_ui->typeComboBox->findData(ConvertToPreviewTextType(mimeType));
|
||||||
|
m_ui->typeComboBox->setCurrentIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTypeChanged(m_ui->typeComboBox->currentIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsPreviewWidget::onTypeChanged(int index)
|
||||||
|
{
|
||||||
|
if (index < 0) {
|
||||||
|
qWarning() << "TextAttachmentsPreviewWidget: Unknown text format";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fileType = m_ui->typeComboBox->itemData(index).toInt();
|
||||||
|
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::PlainText) {
|
||||||
|
m_ui->previewTextBrowser->setPlainText(m_attachment.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::Html) {
|
||||||
|
m_ui->previewTextBrowser->setHtml(m_attachment.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||||
|
if (fileType == TextAttachmentsPreviewWidget::PreviewTextType::Markdown) {
|
||||||
|
m_ui->previewTextBrowser->setMarkdown(m_attachment.data);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
61
src/gui/entry/attachments/TextAttachmentsPreviewWidget.h
Normal file
61
src/gui/entry/attachments/TextAttachmentsPreviewWidget.h
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class TextAttachmentsPreviewWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextAttachmentsPreviewWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TextAttachmentsPreviewWidget(QWidget* parent = nullptr);
|
||||||
|
~TextAttachmentsPreviewWidget() override;
|
||||||
|
|
||||||
|
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||||
|
attachments::Attachment getAttachment() const;
|
||||||
|
|
||||||
|
enum PreviewTextType : int
|
||||||
|
{
|
||||||
|
Html,
|
||||||
|
PlainText,
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||||
|
Markdown
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_ENUM(PreviewTextType)
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onTypeChanged(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initTypeCombobox();
|
||||||
|
void updateUi();
|
||||||
|
|
||||||
|
QScopedPointer<Ui::TextAttachmentsPreviewWidget> m_ui;
|
||||||
|
|
||||||
|
attachments::Attachment m_attachment;
|
||||||
|
};
|
70
src/gui/entry/attachments/TextAttachmentsPreviewWidget.ui
Normal file
70
src/gui/entry/attachments/TextAttachmentsPreviewWidget.ui
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>TextAttachmentsPreviewWidget</class>
|
||||||
|
<widget class="QWidget" name="TextAttachmentsPreviewWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>300</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>6</number>
|
||||||
|
</property>
|
||||||
|
<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>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="typeLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Type:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="typeComboBox">
|
||||||
|
<property name="editable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QTextBrowser" name="previewTextBrowser"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
105
src/gui/entry/attachments/TextAttachmentsWidget.cpp
Normal file
105
src/gui/entry/attachments/TextAttachmentsWidget.cpp
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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 "TextAttachmentsWidget.h"
|
||||||
|
#include "TextAttachmentsEditWidget.h"
|
||||||
|
#include "TextAttachmentsPreviewWidget.h"
|
||||||
|
|
||||||
|
#include "ui_TextAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
TextAttachmentsWidget::TextAttachmentsWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_ui(new Ui::TextAttachmentsWidget())
|
||||||
|
, m_previewUpdateTimer(new QTimer(this))
|
||||||
|
, m_mode(attachments::OpenMode::ReadOnly)
|
||||||
|
{
|
||||||
|
m_ui->setupUi(this);
|
||||||
|
initWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextAttachmentsWidget::~TextAttachmentsWidget() = default;
|
||||||
|
|
||||||
|
void TextAttachmentsWidget::openAttachment(attachments::Attachment attachment, attachments::OpenMode mode)
|
||||||
|
{
|
||||||
|
m_attachment = std::move(attachment);
|
||||||
|
m_mode = mode;
|
||||||
|
|
||||||
|
updateWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments::Attachment TextAttachmentsWidget::getAttachment() const
|
||||||
|
{
|
||||||
|
if (m_mode == attachments::OpenMode::ReadWrite) {
|
||||||
|
return m_editWidget->getAttachment();
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsWidget::updateWidget()
|
||||||
|
{
|
||||||
|
if (m_mode == attachments::OpenMode::ReadOnly) {
|
||||||
|
m_splitter->setSizes({0, 1});
|
||||||
|
m_editWidget->hide();
|
||||||
|
} else {
|
||||||
|
m_splitter->setSizes({1, 0});
|
||||||
|
m_editWidget->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_editWidget->openAttachment(m_attachment, m_mode);
|
||||||
|
m_previewWidget->openAttachment(m_attachment, attachments::OpenMode::ReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextAttachmentsWidget::initWidget()
|
||||||
|
{
|
||||||
|
m_splitter = new QSplitter(this);
|
||||||
|
m_editWidget = new TextAttachmentsEditWidget(this);
|
||||||
|
m_previewWidget = new TextAttachmentsPreviewWidget(this);
|
||||||
|
|
||||||
|
m_previewUpdateTimer->setSingleShot(true);
|
||||||
|
m_previewUpdateTimer->setInterval(500);
|
||||||
|
|
||||||
|
// Only update the preview after a set timeout and if it is visible
|
||||||
|
connect(m_previewUpdateTimer, &QTimer::timeout, this, [this] {
|
||||||
|
if (m_previewWidget->width() > 0) {
|
||||||
|
m_attachment = m_editWidget->getAttachment();
|
||||||
|
m_previewWidget->openAttachment(m_attachment, attachments::OpenMode::ReadOnly);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(
|
||||||
|
m_editWidget, &TextAttachmentsEditWidget::textChanged, m_previewUpdateTimer, QOverload<>::of(&QTimer::start));
|
||||||
|
|
||||||
|
connect(m_editWidget, &TextAttachmentsEditWidget::previewButtonClicked, [this] {
|
||||||
|
const auto sizes = m_splitter->sizes();
|
||||||
|
const auto previewSize = sizes.value(1, 0) > 0 ? 0 : 1;
|
||||||
|
m_splitter->setSizes({1, previewSize});
|
||||||
|
});
|
||||||
|
|
||||||
|
m_splitter->addWidget(m_editWidget);
|
||||||
|
m_splitter->addWidget(m_previewWidget);
|
||||||
|
// Prevent collapsing of the edit widget
|
||||||
|
m_splitter->setCollapsible(0, false);
|
||||||
|
|
||||||
|
m_ui->verticalLayout->addWidget(m_splitter);
|
||||||
|
|
||||||
|
updateWidget();
|
||||||
|
}
|
58
src/gui/entry/attachments/TextAttachmentsWidget.h
Normal file
58
src/gui/entry/attachments/TextAttachmentsWidget.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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 "AttachmentTypes.h"
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui
|
||||||
|
{
|
||||||
|
class TextAttachmentsWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class QSplitter;
|
||||||
|
class QTimer;
|
||||||
|
class TextAttachmentsPreviewWidget;
|
||||||
|
class TextAttachmentsEditWidget;
|
||||||
|
|
||||||
|
class TextAttachmentsWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit TextAttachmentsWidget(QWidget* parent = nullptr);
|
||||||
|
~TextAttachmentsWidget() override;
|
||||||
|
|
||||||
|
void openAttachment(attachments::Attachment attachment, attachments::OpenMode mode);
|
||||||
|
attachments::Attachment getAttachment() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateWidget();
|
||||||
|
void initWidget();
|
||||||
|
|
||||||
|
QScopedPointer<Ui::TextAttachmentsWidget> m_ui;
|
||||||
|
QPointer<QSplitter> m_splitter;
|
||||||
|
QPointer<TextAttachmentsEditWidget> m_editWidget;
|
||||||
|
QPointer<TextAttachmentsPreviewWidget> m_previewWidget;
|
||||||
|
QPointer<QTimer> m_previewUpdateTimer;
|
||||||
|
|
||||||
|
attachments::Attachment m_attachment;
|
||||||
|
attachments::OpenMode m_mode;
|
||||||
|
};
|
36
src/gui/entry/attachments/TextAttachmentsWidget.ui
Normal file
36
src/gui/entry/attachments/TextAttachmentsWidget.ui
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>TextAttachmentsWidget</class>
|
||||||
|
<widget class="QWidget" name="TextAttachmentsWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>732</width>
|
||||||
|
<height>432</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<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>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -18,7 +18,9 @@
|
||||||
#include "TestTools.h"
|
#include "TestTools.h"
|
||||||
|
|
||||||
#include "core/Clock.h"
|
#include "core/Clock.h"
|
||||||
|
#include "core/Tools.h"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QTest>
|
#include <QTest>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
@ -277,10 +279,8 @@ void TestTools::testMimeTypes()
|
||||||
{
|
{
|
||||||
const QStringList TextMimeTypes = {
|
const QStringList TextMimeTypes = {
|
||||||
"text/plain", // Plain text
|
"text/plain", // Plain text
|
||||||
"text/html", // HTML documents
|
|
||||||
"text/css", // CSS stylesheets
|
"text/css", // CSS stylesheets
|
||||||
"text/javascript", // JavaScript files
|
"text/javascript", // JavaScript files
|
||||||
"text/markdown", // Markdown documents
|
|
||||||
"text/xml", // XML documents
|
"text/xml", // XML documents
|
||||||
"text/rtf", // Rich Text Format
|
"text/rtf", // Rich Text Format
|
||||||
"text/vcard", // vCard files
|
"text/vcard", // vCard files
|
||||||
|
@ -327,6 +327,9 @@ void TestTools::testMimeTypes()
|
||||||
"application/x-shellscript", // Shell scripts
|
"application/x-shellscript", // Shell scripts
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QCOMPARE(Tools::toMimeType("text/html"), Tools::MimeType::Html);
|
||||||
|
QCOMPARE(Tools::toMimeType("text/markdown"), Tools::MimeType::Markdown);
|
||||||
|
|
||||||
for (const auto& mime : TextMimeTypes) {
|
for (const auto& mime : TextMimeTypes) {
|
||||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText);
|
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText);
|
||||||
}
|
}
|
||||||
|
@ -339,3 +342,89 @@ void TestTools::testMimeTypes()
|
||||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Unknown);
|
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Unknown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestTools::testGetMimeType()
|
||||||
|
{
|
||||||
|
const QStringList Text = {"0x42", ""};
|
||||||
|
|
||||||
|
for (const auto& text : Text) {
|
||||||
|
QCOMPARE(Tools::getMimeType(text.toUtf8()), Tools::MimeType::PlainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArrayList ImageHeaders = {
|
||||||
|
// JPEG: starts with 0xFF 0xD8 0xFF (Start of Image marker)
|
||||||
|
QByteArray::fromHex("FFD8FF"),
|
||||||
|
// PNG: starts with 0x89 0x50 0x4E 0x47 0D 0A 1A 0A (PNG signature)
|
||||||
|
QByteArray::fromHex("89504E470D0A1A0A"),
|
||||||
|
// GIF87a: original GIF format (1987 standard)
|
||||||
|
QByteArray("GIF87a"),
|
||||||
|
// GIF89a: extended GIF format (1989, supports animation, transparency, etc.)
|
||||||
|
QByteArray("GIF89a"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& image : ImageHeaders) {
|
||||||
|
QCOMPARE(Tools::getMimeType(image), Tools::MimeType::Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArrayList UnknownHeaders = {
|
||||||
|
// MP3: typically starts with ID3 tag (ID3v2)
|
||||||
|
QByteArray("ID3"),
|
||||||
|
// MP4: usually starts with a 'ftyp' box (ISO base media file format)
|
||||||
|
// Common major brands: isom, mp42, avc1, etc.
|
||||||
|
QByteArray::fromHex("000000186674797069736F6D"), // size + 'ftyp' + 'isom'
|
||||||
|
// PDF: starts with "%PDF-" followed by version (e.g., %PDF-1.7)
|
||||||
|
QByteArray("%PDF-"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& unknown : UnknownHeaders) {
|
||||||
|
QCOMPARE(Tools::getMimeType(unknown), Tools::MimeType::Unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTools::testGetMimeTypeByFileInfo()
|
||||||
|
{
|
||||||
|
const QStringList Text = {"test.txt", "test.csv", "test.xml", "test.json"};
|
||||||
|
|
||||||
|
for (const auto& text : Text) {
|
||||||
|
QCOMPARE(Tools::getMimeType(QFileInfo(text)), Tools::MimeType::PlainText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList Images = {"test.jpg", "test.png", "test.bmp", "test.svg"};
|
||||||
|
|
||||||
|
for (const auto& image : Images) {
|
||||||
|
QCOMPARE(Tools::getMimeType(QFileInfo(image)), Tools::MimeType::Image);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList Htmls = {"test.html", "test.htm"};
|
||||||
|
|
||||||
|
for (const auto& html : Htmls) {
|
||||||
|
QCOMPARE(Tools::getMimeType(QFileInfo(html)), Tools::MimeType::Html);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList Markdowns = {"test.md", "test.markdown"};
|
||||||
|
|
||||||
|
for (const auto& makdown : Markdowns) {
|
||||||
|
QCOMPARE(Tools::getMimeType(QFileInfo(makdown)), Tools::MimeType::Markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList UnknownHeaders = {"test.doc", "test.pdf", "test.docx"};
|
||||||
|
|
||||||
|
for (const auto& unknown : UnknownHeaders) {
|
||||||
|
QCOMPARE(Tools::getMimeType(unknown), Tools::MimeType::Unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTools::testIsTextMimeType()
|
||||||
|
{
|
||||||
|
const auto Text = {Tools::MimeType::PlainText, Tools::MimeType::Html, Tools::MimeType::Markdown};
|
||||||
|
|
||||||
|
for (const auto& text : Text) {
|
||||||
|
QVERIFY(Tools::isTextMimeType(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto NoText = {Tools::MimeType::Image, Tools::MimeType::Unknown};
|
||||||
|
|
||||||
|
for (const auto& noText : NoText) {
|
||||||
|
QVERIFY(!Tools::isTextMimeType(noText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
#ifndef KEEPASSX_TESTTOOLS_H
|
#ifndef KEEPASSX_TESTTOOLS_H
|
||||||
#define KEEPASSX_TESTTOOLS_H
|
#define KEEPASSX_TESTTOOLS_H
|
||||||
|
|
||||||
#include "core/Tools.h"
|
#include <QObject>
|
||||||
|
|
||||||
class TestTools : public QObject
|
class TestTools : public QObject
|
||||||
{
|
{
|
||||||
|
@ -38,6 +38,9 @@ private slots:
|
||||||
void testConvertToRegex_data();
|
void testConvertToRegex_data();
|
||||||
void testArrayContainsValues();
|
void testArrayContainsValues();
|
||||||
void testMimeTypes();
|
void testMimeTypes();
|
||||||
|
void testGetMimeType();
|
||||||
|
void testGetMimeTypeByFileInfo();
|
||||||
|
void testIsTextMimeType();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TESTTOOLS_H
|
#endif // KEEPASSX_TESTTOOLS_H
|
||||||
|
|
|
@ -18,6 +18,10 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||||
add_unit_test(NAME testgui SOURCES TestGui.cpp ../util/TemporaryFile.cpp ../mock/MockRemoteProcess.cpp LIBS ${TEST_LIBRARIES})
|
add_unit_test(NAME testgui SOURCES TestGui.cpp ../util/TemporaryFile.cpp ../mock/MockRemoteProcess.cpp LIBS ${TEST_LIBRARIES})
|
||||||
add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARIES})
|
add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
|
file(GLOB_RECURSE ATTACHMENTS_TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/attachments/*.cpp)
|
||||||
|
add_unit_test(NAME testguiattachments SOURCES ${ATTACHMENTS_TEST_SOURCES} LIBS ${TEST_LIBRARIES})
|
||||||
|
include_directories(testguiattachments PRIVATE ${PROJECT_SOURCE_DIR}/src/gui/entry)
|
||||||
|
|
||||||
if(WITH_XC_BROWSER)
|
if(WITH_XC_BROWSER)
|
||||||
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
|
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
|
||||||
endif()
|
endif()
|
||||||
|
|
94
tests/gui/attachments/TestAttachmentWidget.cpp
Normal file
94
tests/gui/attachments/TestAttachmentWidget.cpp
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
#include "TestAttachmentWidget.h"
|
||||||
|
|
||||||
|
#include <attachments/AttachmentWidget.h>
|
||||||
|
|
||||||
|
#include <attachments/AttachmentTypes.h>
|
||||||
|
#include <attachments/ImageAttachmentsWidget.h>
|
||||||
|
#include <attachments/TextAttachmentsWidget.h>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
void TestAttachmentsWidget::initTestCase()
|
||||||
|
{
|
||||||
|
m_attachmentWidget.reset(new AttachmentWidget());
|
||||||
|
|
||||||
|
QVERIFY(m_attachmentWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAttachmentsWidget::testTextAttachment()
|
||||||
|
{
|
||||||
|
for (const auto& attachment : {attachments::Attachment{.name = "Test.txt", .data = "Test"},
|
||||||
|
attachments::Attachment{.name = "Test.html", .data = "<h1> test </h1>"},
|
||||||
|
attachments::Attachment{.name = "Test.md", .data = "**bold**"}}) {
|
||||||
|
for (auto mode : {attachments::OpenMode::ReadWrite, attachments::OpenMode::ReadOnly}) {
|
||||||
|
m_attachmentWidget->openAttachment(attachment, mode);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY(layout);
|
||||||
|
|
||||||
|
QCOMPARE(layout->count(), 1);
|
||||||
|
|
||||||
|
auto item = layout->itemAt(0);
|
||||||
|
QVERIFY(item);
|
||||||
|
|
||||||
|
QVERIFY(qobject_cast<TextAttachmentsWidget*>(item->widget()));
|
||||||
|
|
||||||
|
auto actualAttachment = m_attachmentWidget->getAttachment();
|
||||||
|
QCOMPARE(actualAttachment.name, attachment.name);
|
||||||
|
QCOMPARE(actualAttachment.data, attachment.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAttachmentsWidget::testImageAttachment()
|
||||||
|
{
|
||||||
|
const auto Attachment = attachments::Attachment{.name = "Test.jpg", .data = QByteArray::fromHex("FFD8FF")};
|
||||||
|
|
||||||
|
m_attachmentWidget->openAttachment(Attachment, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY(layout);
|
||||||
|
|
||||||
|
QCOMPARE(layout->count(), 1);
|
||||||
|
|
||||||
|
auto item = layout->itemAt(0);
|
||||||
|
QVERIFY(item);
|
||||||
|
|
||||||
|
QVERIFY(qobject_cast<ImageAttachmentsWidget*>(item->widget()));
|
||||||
|
|
||||||
|
auto actualAttachment = m_attachmentWidget->getAttachment();
|
||||||
|
QCOMPARE(actualAttachment.name, Attachment.name);
|
||||||
|
QCOMPARE(actualAttachment.data, Attachment.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAttachmentsWidget::testUnknownAttachment()
|
||||||
|
{
|
||||||
|
const auto Attachment = attachments::Attachment{.name = "Test", .data = QByteArray{"ID3"}};
|
||||||
|
|
||||||
|
m_attachmentWidget->openAttachment(Attachment, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto layout = m_attachmentWidget->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY(layout);
|
||||||
|
|
||||||
|
QCOMPARE(layout->count(), 1);
|
||||||
|
|
||||||
|
auto item = layout->itemAt(0);
|
||||||
|
QVERIFY(item);
|
||||||
|
|
||||||
|
auto label = qobject_cast<QLabel*>(item->widget());
|
||||||
|
QVERIFY(label);
|
||||||
|
|
||||||
|
QVERIFY(!label->text().isEmpty());
|
||||||
|
|
||||||
|
auto actualAttachment = m_attachmentWidget->getAttachment();
|
||||||
|
QCOMPARE(actualAttachment.name, Attachment.name);
|
||||||
|
QCOMPARE(actualAttachment.data, Attachment.data);
|
||||||
|
}
|
38
tests/gui/attachments/TestAttachmentWidget.h
Normal file
38
tests/gui/attachments/TestAttachmentWidget.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/AttachmentWidget.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestAttachmentsWidget : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testTextAttachment();
|
||||||
|
void testImageAttachment();
|
||||||
|
void testUnknownAttachment();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<AttachmentWidget> m_attachmentWidget;
|
||||||
|
};
|
46
tests/gui/attachments/TestAttachmentsGui.cpp
Normal file
46
tests/gui/attachments/TestAttachmentsGui.cpp
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include "TestAttachmentWidget.h"
|
||||||
|
#include "TestEditEntryAttachmentsDialog.h"
|
||||||
|
#include "TestImageAttachmentsView.h"
|
||||||
|
#include "TestImageAttachmentsWidget.h"
|
||||||
|
#include "TestPreviewEntryAttachmentsDialog.h"
|
||||||
|
#include "TestTextAttachmentsEditWidget.h"
|
||||||
|
#include "TestTextAttachmentsPreviewWidget.h"
|
||||||
|
#include "TestTextAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <config-keepassx.h>
|
||||||
|
#include <gui/Application.h>
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
||||||
|
Application app(argc, argv);
|
||||||
|
app.setApplicationName("KeePassXC");
|
||||||
|
app.setApplicationVersion(KEEPASSXC_VERSION);
|
||||||
|
app.setQuitOnLastWindowClosed(false);
|
||||||
|
app.setAttribute(Qt::AA_Use96Dpi, true);
|
||||||
|
app.applyTheme();
|
||||||
|
|
||||||
|
TestPreviewEntryAttachmentsDialog previewDialogTest{};
|
||||||
|
TestEditEntryAttachmentsDialog editDialogTest{};
|
||||||
|
TestTextAttachmentsWidget textAttachmentsWidget{};
|
||||||
|
TestTextAttachmentsPreviewWidget textPreviewWidget{};
|
||||||
|
TestTextAttachmentsEditWidget textEditWidget{};
|
||||||
|
TestImageAttachmentsWidget imageWidget{};
|
||||||
|
TestImageAttachmentsView imageView{};
|
||||||
|
TestAttachmentsWidget attachmentWidget{};
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
result |= QTest::qExec(&previewDialogTest, argc, argv);
|
||||||
|
result |= QTest::qExec(&editDialogTest, argc, argv);
|
||||||
|
result |= QTest::qExec(&textAttachmentsWidget, argc, argv);
|
||||||
|
result |= QTest::qExec(&textPreviewWidget, argc, argv);
|
||||||
|
result |= QTest::qExec(&textEditWidget, argc, argv);
|
||||||
|
result |= QTest::qExec(&imageWidget, argc, argv);
|
||||||
|
result |= QTest::qExec(&imageView, argc, argv);
|
||||||
|
result |= QTest::qExec(&attachmentWidget, argc, argv);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
95
tests/gui/attachments/TestEditEntryAttachmentsDialog.cpp
Normal file
95
tests/gui/attachments/TestEditEntryAttachmentsDialog.cpp
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#include "TestEditEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <attachments/AttachmentWidget.h>
|
||||||
|
|
||||||
|
#include <PreviewEntryAttachmentsDialog.h>
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTestMouseEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
void TestEditEntryAttachmentsDialog::initTestCase()
|
||||||
|
{
|
||||||
|
m_editDialog.reset(new EditEntryAttachmentsDialog());
|
||||||
|
|
||||||
|
QVERIFY(m_editDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEditEntryAttachmentsDialog::testSetAttachment()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_editDialog->setAttachment(Test);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY2(m_editDialog->windowTitle().contains(Test.name), "Expected file name in the title");
|
||||||
|
|
||||||
|
auto layout = m_editDialog->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY2(layout, "QVBoxLayout not found");
|
||||||
|
QCOMPARE(layout->count(), 2);
|
||||||
|
|
||||||
|
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
|
||||||
|
QVERIFY2(widget, "Expected AttachmentWidget");
|
||||||
|
|
||||||
|
auto sizePolicy = widget->sizePolicy();
|
||||||
|
QCOMPARE(sizePolicy.horizontalPolicy(), QSizePolicy::Expanding);
|
||||||
|
QCOMPARE(sizePolicy.verticalPolicy(), QSizePolicy::Expanding);
|
||||||
|
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEditEntryAttachmentsDialog::testSetAttachmentTwice()
|
||||||
|
{
|
||||||
|
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
|
||||||
|
m_editDialog->setAttachment(TestText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const attachments::Attachment TestImage{
|
||||||
|
.name = "test.jpg", .data = QByteArray::fromHex("FFD8FFE000104A46494600010101006000600000FFD9")};
|
||||||
|
m_editDialog->setAttachment(TestImage);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY2(m_editDialog->windowTitle().contains(TestImage.name), "Expected file name in the title");
|
||||||
|
|
||||||
|
auto layout = m_editDialog->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY2(layout, "QVBoxLayout not found");
|
||||||
|
QCOMPARE(layout->count(), 2);
|
||||||
|
|
||||||
|
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
|
||||||
|
QVERIFY2(widget, "Expected AttachmentWidget");
|
||||||
|
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, TestImage.name);
|
||||||
|
QCOMPARE(attachments.data, TestImage.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEditEntryAttachmentsDialog::testBottonsBox()
|
||||||
|
{
|
||||||
|
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
|
||||||
|
m_editDialog->setAttachment(TestText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QSignalSpy acceptButton(m_editDialog.data(), &PreviewEntryAttachmentsDialog::accepted);
|
||||||
|
QSignalSpy closeButton(m_editDialog.data(), &PreviewEntryAttachmentsDialog::rejected);
|
||||||
|
|
||||||
|
auto buttonsBox = m_editDialog->findChild<QDialogButtonBox*>();
|
||||||
|
QVERIFY2(buttonsBox, "ButtonsBox not found");
|
||||||
|
|
||||||
|
for (auto button : buttonsBox->buttons()) {
|
||||||
|
QTest::mouseClick(button, Qt::LeftButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(acceptButton.count(), 1);
|
||||||
|
QCOMPARE(closeButton.count(), 1);
|
||||||
|
}
|
39
tests/gui/attachments/TestEditEntryAttachmentsDialog.h
Normal file
39
tests/gui/attachments/TestEditEntryAttachmentsDialog.h
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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 "EditEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestEditEntryAttachmentsDialog : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testSetAttachment();
|
||||||
|
void testSetAttachmentTwice();
|
||||||
|
|
||||||
|
void testBottonsBox();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<EditEntryAttachmentsDialog> m_editDialog{};
|
||||||
|
};
|
76
tests/gui/attachments/TestImageAttachmentsView.cpp
Normal file
76
tests/gui/attachments/TestImageAttachmentsView.cpp
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#include "TestImageAttachmentsView.h"
|
||||||
|
|
||||||
|
#include <attachments/ImageAttachmentsView.h>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
|
||||||
|
void TestImageAttachmentsView::initTestCase()
|
||||||
|
{
|
||||||
|
m_view.reset(new ImageAttachmentsView());
|
||||||
|
|
||||||
|
// Generate the black rectange.
|
||||||
|
QImage image(1000, 1000, QImage::Format_RGB32);
|
||||||
|
image.fill(Qt::black);
|
||||||
|
|
||||||
|
auto scene = new QGraphicsScene();
|
||||||
|
scene->addPixmap(QPixmap::fromImage(image));
|
||||||
|
|
||||||
|
m_view->setScene(scene);
|
||||||
|
m_view->show();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsView::testEmitWheelEvent()
|
||||||
|
{
|
||||||
|
QSignalSpy ctrlWheelEvent{m_view.data(), &ImageAttachmentsView::ctrlWheelEvent};
|
||||||
|
|
||||||
|
QPoint center = m_view->rect().center();
|
||||||
|
|
||||||
|
m_view->setFocus();
|
||||||
|
|
||||||
|
QWheelEvent event(center, // local pos
|
||||||
|
m_view->mapToGlobal(center), // global pos
|
||||||
|
QPoint(0, 0),
|
||||||
|
QPoint(0, 120),
|
||||||
|
Qt::NoButton,
|
||||||
|
Qt::ControlModifier,
|
||||||
|
Qt::ScrollBegin,
|
||||||
|
false);
|
||||||
|
|
||||||
|
QCoreApplication::sendEvent(m_view->viewport(), &event);
|
||||||
|
|
||||||
|
QCOMPARE(ctrlWheelEvent.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsView::testEnableFit()
|
||||||
|
{
|
||||||
|
m_view->enableAutoFitInView();
|
||||||
|
QVERIFY(m_view->isAutoFitInViewActivated());
|
||||||
|
|
||||||
|
const auto oldTransform = m_view->transform();
|
||||||
|
|
||||||
|
m_view->resize(m_view->size() + QSize(100, 100));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY(m_view->transform() != oldTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsView::testDisableFit()
|
||||||
|
{
|
||||||
|
m_view->disableAutoFitInView();
|
||||||
|
QVERIFY(!m_view->isAutoFitInViewActivated());
|
||||||
|
|
||||||
|
const auto expectedTransform = m_view->transform();
|
||||||
|
|
||||||
|
m_view->resize(m_view->size() + QSize(100, 100));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_view->transform(), expectedTransform);
|
||||||
|
}
|
38
tests/gui/attachments/TestImageAttachmentsView.h
Normal file
38
tests/gui/attachments/TestImageAttachmentsView.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/ImageAttachmentsView.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestImageAttachmentsView : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testEmitWheelEvent();
|
||||||
|
void testEnableFit();
|
||||||
|
void testDisableFit();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<ImageAttachmentsView> m_view{};
|
||||||
|
};
|
244
tests/gui/attachments/TestImageAttachmentsWidget.cpp
Normal file
244
tests/gui/attachments/TestImageAttachmentsWidget.cpp
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
#include "TestImageAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <attachments/ImageAttachmentsView.h>
|
||||||
|
#include <attachments/ImageAttachmentsWidget.h>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTransform>
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::initTestCase()
|
||||||
|
{
|
||||||
|
m_widget.reset(new ImageAttachmentsWidget());
|
||||||
|
m_zoomCombobox = m_widget->findChild<QComboBox*>("zoomComboBox");
|
||||||
|
QVERIFY(m_zoomCombobox);
|
||||||
|
|
||||||
|
m_imageAttachmentsView = m_widget->findChild<ImageAttachmentsView*>("imagesView");
|
||||||
|
QVERIFY(m_imageAttachmentsView);
|
||||||
|
|
||||||
|
// Generate the black rectange.
|
||||||
|
QImage image(1000, 1000, QImage::Format_RGB32);
|
||||||
|
image.fill(Qt::black);
|
||||||
|
|
||||||
|
QByteArray imageBytes{};
|
||||||
|
QBuffer buffer(&imageBytes);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
image.save(&buffer, "PNG");
|
||||||
|
|
||||||
|
m_widget->openAttachment({.name = "black.png", .data = std::move(imageBytes)}, attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
m_widget->show();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testFitInView()
|
||||||
|
{
|
||||||
|
QCOMPARE(m_zoomCombobox->currentText(), tr("Fit"));
|
||||||
|
QVERIFY(m_imageAttachmentsView->isAutoFitInViewActivated());
|
||||||
|
|
||||||
|
auto zoomFactor = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
m_widget->setMinimumSize(m_widget->size() + QSize{100, 100});
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY(zoomFactor != m_imageAttachmentsView->transform());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testZoomCombobox()
|
||||||
|
{
|
||||||
|
for (const auto zoom : {0.25, 0.5, 1.0, 2.0}) {
|
||||||
|
auto index = m_zoomCombobox->findData(zoom);
|
||||||
|
QVERIFY(index != -1);
|
||||||
|
|
||||||
|
m_zoomCombobox->setCurrentIndex(index);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testEditZoomCombobox()
|
||||||
|
{
|
||||||
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
|
m_zoomCombobox->setCurrentText(QString::number(i * 100));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testEditWithPercentZoomCombobox()
|
||||||
|
{
|
||||||
|
// Example 100 %
|
||||||
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
|
m_zoomCombobox->setCurrentText(QString("%1 %").arg(i * 100));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 100%
|
||||||
|
for (double i = 0.25; i < 5; i += 0.25) {
|
||||||
|
m_zoomCombobox->setCurrentText(QString("%1%").arg(i * 100));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testInvalidValueZoomCombobox()
|
||||||
|
{
|
||||||
|
auto index = m_zoomCombobox->findData(1.0);
|
||||||
|
QVERIFY(index != -1);
|
||||||
|
|
||||||
|
m_zoomCombobox->setCurrentIndex(index);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const QTransform expectedTransform = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
for (const auto& invalidValue : {"Help", "3,4", "", ".", "% 100"}) {
|
||||||
|
m_zoomCombobox->setCurrentText(invalidValue);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), expectedTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testZoomInByMouse()
|
||||||
|
{
|
||||||
|
QPoint center = m_imageAttachmentsView->rect().center();
|
||||||
|
|
||||||
|
// Set zoom: 100%
|
||||||
|
auto index = m_zoomCombobox->findData(1.0);
|
||||||
|
QVERIFY(index != -1);
|
||||||
|
m_zoomCombobox->setCurrentIndex(index);
|
||||||
|
|
||||||
|
m_imageAttachmentsView->setFocus();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const auto transform = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
QWheelEvent event(center, // local pos
|
||||||
|
m_imageAttachmentsView->mapToGlobal(center), // global pos
|
||||||
|
QPoint(0, 0),
|
||||||
|
QPoint(0, 120),
|
||||||
|
Qt::NoButton,
|
||||||
|
Qt::ControlModifier,
|
||||||
|
Qt::ScrollBegin,
|
||||||
|
false);
|
||||||
|
|
||||||
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QTransform t = m_imageAttachmentsView->transform();
|
||||||
|
QVERIFY(t.m11() > transform.m11());
|
||||||
|
QVERIFY(t.m22() > transform.m22());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testZoomOutByMouse()
|
||||||
|
{
|
||||||
|
QPoint center = m_imageAttachmentsView->rect().center();
|
||||||
|
|
||||||
|
// Set zoom: 100%
|
||||||
|
auto index = m_zoomCombobox->findData(1.0);
|
||||||
|
QVERIFY(index != -1);
|
||||||
|
m_zoomCombobox->setCurrentIndex(index);
|
||||||
|
|
||||||
|
m_imageAttachmentsView->setFocus();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const auto transform = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
QWheelEvent event(center, // local pos
|
||||||
|
center, // global pos
|
||||||
|
QPoint(0, 0),
|
||||||
|
QPoint(0, -120),
|
||||||
|
Qt::NoButton,
|
||||||
|
Qt::ControlModifier,
|
||||||
|
Qt::ScrollBegin,
|
||||||
|
true);
|
||||||
|
|
||||||
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QTransform t = m_imageAttachmentsView->transform();
|
||||||
|
QVERIFY(t.m11() < transform.m11());
|
||||||
|
QVERIFY(t.m22() < transform.m22());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testZoomLowerBound()
|
||||||
|
{
|
||||||
|
m_widget->setMinimumSize(100, 100);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto minFactor = m_imageAttachmentsView->calculateFitInViewFactor();
|
||||||
|
|
||||||
|
// Set size less then minFactor
|
||||||
|
m_zoomCombobox->setCurrentText(QString::number((minFactor * 100.0) / 2));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const auto expectTransform = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
QPoint center = m_imageAttachmentsView->rect().center();
|
||||||
|
|
||||||
|
QWheelEvent event(center, // local pos
|
||||||
|
center, // global pos
|
||||||
|
QPoint(0, 0),
|
||||||
|
QPoint(0, -120),
|
||||||
|
Qt::NoButton,
|
||||||
|
Qt::ControlModifier,
|
||||||
|
Qt::ScrollBegin,
|
||||||
|
true);
|
||||||
|
|
||||||
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageAttachmentsWidget::testZoomUpperBound()
|
||||||
|
{
|
||||||
|
m_widget->setMinimumSize(100, 100);
|
||||||
|
|
||||||
|
// Set size less then minFactor
|
||||||
|
m_zoomCombobox->setCurrentText(QString::number(500));
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const auto expectTransform = m_imageAttachmentsView->transform();
|
||||||
|
|
||||||
|
QPoint center = m_imageAttachmentsView->rect().center();
|
||||||
|
|
||||||
|
QWheelEvent event(center, // local pos
|
||||||
|
center, // global pos
|
||||||
|
QPoint(0, 0),
|
||||||
|
QPoint(0, 120),
|
||||||
|
Qt::NoButton,
|
||||||
|
Qt::ControlModifier,
|
||||||
|
Qt::ScrollBegin,
|
||||||
|
true);
|
||||||
|
|
||||||
|
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
|
||||||
|
}
|
49
tests/gui/attachments/TestImageAttachmentsWidget.h
Normal file
49
tests/gui/attachments/TestImageAttachmentsWidget.h
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/ImageAttachmentsWidget.h>
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class ImageAttachmentsView;
|
||||||
|
|
||||||
|
class TestImageAttachmentsWidget : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testFitInView();
|
||||||
|
void testZoomCombobox();
|
||||||
|
void testEditZoomCombobox();
|
||||||
|
void testEditWithPercentZoomCombobox();
|
||||||
|
void testInvalidValueZoomCombobox();
|
||||||
|
void testZoomInByMouse();
|
||||||
|
void testZoomOutByMouse();
|
||||||
|
void testZoomLowerBound();
|
||||||
|
void testZoomUpperBound();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<ImageAttachmentsWidget> m_widget{};
|
||||||
|
QPointer<QComboBox> m_zoomCombobox{};
|
||||||
|
QPointer<ImageAttachmentsView> m_imageAttachmentsView{};
|
||||||
|
};
|
97
tests/gui/attachments/TestPreviewEntryAttachmentsDialog.cpp
Normal file
97
tests/gui/attachments/TestPreviewEntryAttachmentsDialog.cpp
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#include "TestPreviewEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <attachments/AttachmentWidget.h>
|
||||||
|
|
||||||
|
#include <PreviewEntryAttachmentsDialog.h>
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTestMouseEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
void TestPreviewEntryAttachmentsDialog::initTestCase()
|
||||||
|
{
|
||||||
|
m_previewDialog.reset(new PreviewEntryAttachmentsDialog());
|
||||||
|
|
||||||
|
QVERIFY(m_previewDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestPreviewEntryAttachmentsDialog::testSetAttachment()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_previewDialog->setAttachment(Test);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY2(m_previewDialog->windowTitle().contains(Test.name), "Expected file name in the title");
|
||||||
|
|
||||||
|
auto layout = m_previewDialog->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY2(layout, "QVBoxLayout not found");
|
||||||
|
QCOMPARE(layout->count(), 2);
|
||||||
|
|
||||||
|
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
|
||||||
|
QVERIFY2(widget, "Expected AbstractAttachmentWidget");
|
||||||
|
|
||||||
|
auto sizePolicy = widget->sizePolicy();
|
||||||
|
QCOMPARE(sizePolicy.horizontalPolicy(), QSizePolicy::Expanding);
|
||||||
|
QCOMPARE(sizePolicy.verticalPolicy(), QSizePolicy::Expanding);
|
||||||
|
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestPreviewEntryAttachmentsDialog::testSetAttachmentTwice()
|
||||||
|
{
|
||||||
|
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
|
||||||
|
m_previewDialog->setAttachment(TestText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
const attachments::Attachment TestImage{
|
||||||
|
.name = "test.jpg", .data = QByteArray::fromHex("FFD8FFE000104A46494600010101006000600000FFD9")};
|
||||||
|
m_previewDialog->setAttachment(TestImage);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QVERIFY2(m_previewDialog->windowTitle().contains(TestImage.name), "Expected file name in the title");
|
||||||
|
|
||||||
|
auto layout = m_previewDialog->findChild<QVBoxLayout*>("verticalLayout");
|
||||||
|
QVERIFY2(layout, "QVBoxLayout not found");
|
||||||
|
QCOMPARE(layout->count(), 2);
|
||||||
|
|
||||||
|
auto widget = qobject_cast<AttachmentWidget*>(layout->itemAt(0)->widget());
|
||||||
|
QVERIFY2(widget, "Expected AbstractAttachmentWidget");
|
||||||
|
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, TestImage.name);
|
||||||
|
QCOMPARE(attachments.data, TestImage.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestPreviewEntryAttachmentsDialog::testBottonsBox()
|
||||||
|
{
|
||||||
|
const attachments::Attachment TestText{.name = "text.txt", .data = "Test"};
|
||||||
|
m_previewDialog->setAttachment(TestText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QSignalSpy saveButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::saveAttachment);
|
||||||
|
QSignalSpy openButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::openAttachment);
|
||||||
|
QSignalSpy closeButton(m_previewDialog.data(), &PreviewEntryAttachmentsDialog::rejected);
|
||||||
|
|
||||||
|
auto buttonsBox = m_previewDialog->findChild<QDialogButtonBox*>("dialogButtons");
|
||||||
|
QVERIFY2(buttonsBox, "ButtonsBox not found");
|
||||||
|
|
||||||
|
for (auto button : buttonsBox->buttons()) {
|
||||||
|
QTest::mouseClick(button, Qt::LeftButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
QCOMPARE(saveButton.count(), 1);
|
||||||
|
QCOMPARE(openButton.count(), 1);
|
||||||
|
QCOMPARE(closeButton.count(), 1);
|
||||||
|
}
|
40
tests/gui/attachments/TestPreviewEntryAttachmentsDialog.h
Normal file
40
tests/gui/attachments/TestPreviewEntryAttachmentsDialog.h
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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 "PreviewEntryAttachmentsDialog.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <qscopedpointer.h>
|
||||||
|
|
||||||
|
class TestPreviewEntryAttachmentsDialog : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testSetAttachment();
|
||||||
|
void testSetAttachmentTwice();
|
||||||
|
|
||||||
|
void testBottonsBox();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<PreviewEntryAttachmentsDialog> m_previewDialog{};
|
||||||
|
};
|
49
tests/gui/attachments/TestTextAttachmentsEditWidget.cpp
Normal file
49
tests/gui/attachments/TestTextAttachmentsEditWidget.cpp
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#include "TestTextAttachmentsEditWidget.h"
|
||||||
|
|
||||||
|
#include <attachments/TextAttachmentsEditWidget.h>
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTestMouseEvent>
|
||||||
|
#include <QTextEdit>
|
||||||
|
|
||||||
|
void TestTextAttachmentsEditWidget::initTestCase()
|
||||||
|
{
|
||||||
|
m_widget.reset(new TextAttachmentsEditWidget());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsEditWidget::testEmitTextChanged()
|
||||||
|
{
|
||||||
|
QSignalSpy textChangedSignal(m_widget.data(), &TextAttachmentsEditWidget::textChanged);
|
||||||
|
|
||||||
|
m_widget->openAttachment({.name = "test.txt", .data = {}}, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto textEdit = m_widget->findChild<QTextEdit*>("attachmentsTextEdit");
|
||||||
|
QVERIFY(textEdit);
|
||||||
|
|
||||||
|
const QByteArray NewText = "New test text";
|
||||||
|
textEdit->setText(NewText);
|
||||||
|
|
||||||
|
QVERIFY(textChangedSignal.count() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsEditWidget::testEmitPreviewButtonClicked()
|
||||||
|
{
|
||||||
|
QSignalSpy previwButtonClickedSignal(m_widget.data(), &TextAttachmentsEditWidget::previewButtonClicked);
|
||||||
|
|
||||||
|
m_widget->openAttachment({.name = "test.txt", .data = {}}, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto previewButton = m_widget->findChild<QPushButton*>("previewPushButton");
|
||||||
|
QVERIFY(previewButton);
|
||||||
|
|
||||||
|
QTest::mouseClick(previewButton, Qt::LeftButton);
|
||||||
|
|
||||||
|
QCOMPARE(previwButtonClickedSignal.count(), 1);
|
||||||
|
}
|
37
tests/gui/attachments/TestTextAttachmentsEditWidget.h
Normal file
37
tests/gui/attachments/TestTextAttachmentsEditWidget.h
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/TextAttachmentsEditWidget.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestTextAttachmentsEditWidget : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testEmitTextChanged();
|
||||||
|
void testEmitPreviewButtonClicked();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<TextAttachmentsEditWidget> m_widget{};
|
||||||
|
};
|
40
tests/gui/attachments/TestTextAttachmentsPreviewWidget.cpp
Normal file
40
tests/gui/attachments/TestTextAttachmentsPreviewWidget.cpp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#include "TestTextAttachmentsPreviewWidget.h"
|
||||||
|
|
||||||
|
#include <attachments/TextAttachmentsPreviewWidget.h>
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
void TestTextAttachmentsPreviewWidget::initTestCase()
|
||||||
|
{
|
||||||
|
m_widget.reset(new TextAttachmentsPreviewWidget());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsPreviewWidget::testDetectMimeByFile()
|
||||||
|
{
|
||||||
|
const auto combobox = m_widget->findChild<QComboBox*>("typeComboBox");
|
||||||
|
QVERIFY(combobox);
|
||||||
|
|
||||||
|
const attachments::Attachment Text{.name = "test.txt", .data = {}};
|
||||||
|
m_widget->openAttachment(Text, attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::PlainText);
|
||||||
|
|
||||||
|
const attachments::Attachment Html{.name = "test.html", .data = {}};
|
||||||
|
m_widget->openAttachment(Html, attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::Html);
|
||||||
|
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||||
|
const attachments::Attachment Markdown{.name = "test.md", .data = {}};
|
||||||
|
m_widget->openAttachment(Markdown, attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
QCOMPARE(combobox->currentData().toInt(), TextAttachmentsPreviewWidget::Markdown);
|
||||||
|
#endif
|
||||||
|
}
|
36
tests/gui/attachments/TestTextAttachmentsPreviewWidget.h
Normal file
36
tests/gui/attachments/TestTextAttachmentsPreviewWidget.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/TextAttachmentsPreviewWidget.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestTextAttachmentsPreviewWidget : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testDetectMimeByFile();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<TextAttachmentsPreviewWidget> m_widget{};
|
||||||
|
};
|
224
tests/gui/attachments/TestTextAttachmentsWidget.cpp
Normal file
224
tests/gui/attachments/TestTextAttachmentsWidget.cpp
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
#include "TestTextAttachmentsWidget.h"
|
||||||
|
|
||||||
|
#include <attachments/TextAttachmentsEditWidget.h>
|
||||||
|
#include <attachments/TextAttachmentsPreviewWidget.h>
|
||||||
|
#include <attachments/TextAttachmentsWidget.h>
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QTest>
|
||||||
|
#include <QTestMouseEvent>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::initTestCase()
|
||||||
|
{
|
||||||
|
m_textWidget.reset(new TextAttachmentsWidget());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testInitTextWidget()
|
||||||
|
{
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
|
||||||
|
QCOMPARE(splitter->count(), 2);
|
||||||
|
QVERIFY2(qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0)), "EditTextWidget not found");
|
||||||
|
QVERIFY2(qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1)), "PreviewTextWidget not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testTextReadWriteWidget()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
|
||||||
|
m_textWidget->show();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
auto sizes = splitter->sizes();
|
||||||
|
QCOMPARE(sizes.size(), 2);
|
||||||
|
QVERIFY2(sizes[0] > 0, "EditTextWidget width must be greater than zero");
|
||||||
|
|
||||||
|
QCOMPARE(sizes[1], 0);
|
||||||
|
|
||||||
|
auto widget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY(widget);
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
|
||||||
|
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
|
||||||
|
QVERIFY(previewWidget);
|
||||||
|
attachments = previewWidget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testTextReadWidget()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadOnly);
|
||||||
|
m_textWidget->show();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
auto sizes = splitter->sizes();
|
||||||
|
QCOMPARE(sizes.size(), 2);
|
||||||
|
QVERIFY2(sizes[1] > 0, "PreviewTextWidget width must be greater then zero");
|
||||||
|
|
||||||
|
QVERIFY(splitter->widget(0)->isHidden());
|
||||||
|
|
||||||
|
auto widget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY(widget);
|
||||||
|
auto attachments = widget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
|
||||||
|
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
|
||||||
|
QVERIFY(previewWidget);
|
||||||
|
attachments = previewWidget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.name, Test.name);
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testTextChanged()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
QCOMPARE(splitter->sizes().size(), 2);
|
||||||
|
|
||||||
|
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY2(editWidget, "Edit widget not found");
|
||||||
|
|
||||||
|
auto textEdit = editWidget->findChild<QTextEdit*>();
|
||||||
|
QVERIFY(textEdit);
|
||||||
|
|
||||||
|
const QByteArray NewText = "New test text";
|
||||||
|
textEdit->setText(NewText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto attachments = m_textWidget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.data, NewText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testTextChangedInReadOnlyMode()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadOnly);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
QCOMPARE(splitter->sizes().size(), 2);
|
||||||
|
|
||||||
|
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY2(editWidget, "Edit widget not found");
|
||||||
|
|
||||||
|
auto textEdit = editWidget->findChild<QTextEdit*>();
|
||||||
|
QVERIFY(textEdit);
|
||||||
|
|
||||||
|
const QByteArray NewText = "New test text";
|
||||||
|
textEdit->setText(NewText);
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto attachments = m_textWidget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.data, Test.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testPreviewTextChanged()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
|
||||||
|
auto previewTimer = m_textWidget->findChild<QTimer*>();
|
||||||
|
QVERIFY2(previewTimer, "PreviewTimer not found!");
|
||||||
|
|
||||||
|
QSignalSpy timeout(previewTimer, &QTimer::timeout);
|
||||||
|
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
|
||||||
|
|
||||||
|
// Waiting for the first timeout
|
||||||
|
while (timeout.count() < 1) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
QCOMPARE(splitter->sizes().size(), 2);
|
||||||
|
|
||||||
|
splitter->setSizes({1, 1});
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY2(editWidget, "Edit widget not found");
|
||||||
|
|
||||||
|
auto textEdit = editWidget->findChild<QTextEdit*>();
|
||||||
|
QVERIFY(textEdit);
|
||||||
|
|
||||||
|
const QByteArray NewText = "New test text";
|
||||||
|
textEdit->setText(NewText);
|
||||||
|
|
||||||
|
// Waiting for the second timeout
|
||||||
|
while (timeout.count() < 2) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto previewWidget = qobject_cast<TextAttachmentsPreviewWidget*>(splitter->widget(1));
|
||||||
|
auto attachments = previewWidget->getAttachment();
|
||||||
|
|
||||||
|
QCOMPARE(attachments.data, NewText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTextAttachmentsWidget::testOpenPreviewButton()
|
||||||
|
{
|
||||||
|
const attachments::Attachment Test{.name = "text.txt", .data = "Test"};
|
||||||
|
m_textWidget->openAttachment(Test, attachments::OpenMode::ReadWrite);
|
||||||
|
m_textWidget->show();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
|
auto splitter = m_textWidget->findChild<QSplitter*>();
|
||||||
|
QVERIFY2(splitter, "Splitter not found");
|
||||||
|
QCOMPARE(splitter->sizes().size(), 2);
|
||||||
|
|
||||||
|
auto editWidget = qobject_cast<TextAttachmentsEditWidget*>(splitter->widget(0));
|
||||||
|
QVERIFY2(editWidget, "Edit widget not found");
|
||||||
|
QVERIFY(editWidget->isVisible());
|
||||||
|
|
||||||
|
auto previewButton = editWidget->findChild<QPushButton*>("previewPushButton");
|
||||||
|
|
||||||
|
auto sizes = splitter->sizes();
|
||||||
|
QVERIFY(sizes[0] > 0);
|
||||||
|
QCOMPARE(sizes[1], 0);
|
||||||
|
|
||||||
|
QTest::mouseClick(previewButton, Qt::LeftButton);
|
||||||
|
|
||||||
|
sizes = splitter->sizes();
|
||||||
|
QCOMPARE(sizes.size(), 2);
|
||||||
|
QVERIFY(sizes[0] > 0);
|
||||||
|
QVERIFY(sizes[1] > 0);
|
||||||
|
|
||||||
|
QTest::mouseClick(previewButton, Qt::LeftButton);
|
||||||
|
sizes = splitter->sizes();
|
||||||
|
QCOMPARE(sizes.size(), 2);
|
||||||
|
QVERIFY(sizes[0] > 0);
|
||||||
|
QCOMPARE(sizes[1], 0);
|
||||||
|
}
|
42
tests/gui/attachments/TestTextAttachmentsWidget.h
Normal file
42
tests/gui/attachments/TestTextAttachmentsWidget.h
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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 <attachments/TextAttachmentsWidget.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class TestTextAttachmentsWidget : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
|
||||||
|
void testInitTextWidget();
|
||||||
|
void testTextReadWriteWidget();
|
||||||
|
void testTextReadWidget();
|
||||||
|
void testOpenPreviewButton();
|
||||||
|
void testPreviewTextChanged();
|
||||||
|
void testTextChanged();
|
||||||
|
void testTextChangedInReadOnlyMode();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<TextAttachmentsWidget> m_textWidget;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue