mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-01 19:16:16 -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>
|
||||
</message>
|
||||
</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>
|
||||
<name>AutoType</name>
|
||||
<message>
|
||||
|
@ -2661,10 +2672,6 @@ This is definitely a bug, please report it to the developers.</source>
|
|||
<source>No Results</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Enter a unique name or overwrite an existing search from the list:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -2833,6 +2840,17 @@ Disable safe saves and try again?</source>
|
|||
<source>Failed to save backup database: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</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>
|
||||
<name>EditEntryWidget</name>
|
||||
|
@ -3904,21 +3922,6 @@ This may cause the affected plugins to malfunction.</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File name</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File contents...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>EntryAttachmentsModel</name>
|
||||
<message>
|
||||
|
@ -3944,10 +3947,6 @@ This may cause the affected plugins to malfunction.</source>
|
|||
<source>Add new attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Add</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Remove selected attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -3968,10 +3967,6 @@ This may cause the affected plugins to malfunction.</source>
|
|||
<source>Save selected attachment to disk</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -4065,16 +4060,28 @@ Error: %1</source>
|
|||
Would you like to overwrite the existing attachment?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Preview</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to preview an attachment: Attachment not found</source>
|
||||
<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>
|
||||
</message>
|
||||
</context>
|
||||
|
@ -4640,6 +4647,13 @@ You can enable the DuckDuckGo website icon service in the security section of th
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ImageAttachmentsWidget</name>
|
||||
<message>
|
||||
<source>Zoom:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ImportWizard</name>
|
||||
<message>
|
||||
|
@ -6466,25 +6480,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Attachment name cannot be empty</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Attachment with the same name already exists</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Save attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>New entry attachment</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NixUtils</name>
|
||||
<message>
|
||||
|
@ -7253,15 +7248,15 @@ Do you want to overwrite it?</source>
|
|||
<context>
|
||||
<name>PreviewEntryAttachmentsDialog</name>
|
||||
<message>
|
||||
<source>Preview entry attachment</source>
|
||||
<source>Form</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No preview available</source>
|
||||
<source>Preview: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image format not supported</source>
|
||||
<source>Save…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
|
@ -9248,6 +9243,10 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<source>Tags</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fit</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
|
@ -10182,6 +10181,24 @@ This option is deprecated, use --set-key-file instead.</source>
|
|||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</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>
|
||||
<name>TotpDialog</name>
|
||||
<message>
|
||||
|
|
|
@ -159,8 +159,14 @@ set(gui_SOURCES
|
|||
gui/entry/EntryAttachmentsModel.cpp
|
||||
gui/entry/EntryAttachmentsWidget.cpp
|
||||
gui/entry/EntryAttributesModel.cpp
|
||||
gui/entry/NewEntryAttachmentsDialog.cpp
|
||||
gui/entry/EditEntryAttachmentsDialog.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/EntryModel.cpp
|
||||
gui/entry/EntryView.cpp
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include <QIODevice>
|
||||
#include <QLocale>
|
||||
#include <QMetaProperty>
|
||||
#include <QMimeDatabase>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
@ -478,29 +479,57 @@ namespace Tools
|
|||
|
||||
MimeType toMimeType(const QString& mimeName)
|
||||
{
|
||||
static QStringList textFormats = {
|
||||
"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/soap+xml",
|
||||
"application/x-yaml",
|
||||
"application/protobuf",
|
||||
};
|
||||
static QStringList imageFormats = {"image/"};
|
||||
const static QStringList TextFormats = {"text/",
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/soap+xml",
|
||||
"application/x-yaml",
|
||||
"application/protobuf",
|
||||
"application/x-zerosize"};
|
||||
const static QStringList HtmlFormats = {"text/html"};
|
||||
const static QStringList MarkdownFormats = {"text/markdown"};
|
||||
const static QStringList ImageFormats = {"image/"};
|
||||
|
||||
static auto isCompatible = [](const QString& format, const QStringList& list) {
|
||||
return std::any_of(
|
||||
list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); });
|
||||
};
|
||||
|
||||
if (isCompatible(mimeName, imageFormats)) {
|
||||
if (isCompatible(mimeName, ImageFormats)) {
|
||||
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::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
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "core/Global.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QSet>
|
||||
|
@ -119,10 +120,16 @@ namespace Tools
|
|||
{
|
||||
Image,
|
||||
PlainText,
|
||||
Html,
|
||||
Markdown,
|
||||
Unknown
|
||||
};
|
||||
|
||||
MimeType toMimeType(const QString& mimeName);
|
||||
MimeType getMimeType(const QByteArray& data);
|
||||
MimeType getMimeType(const QFileInfo& fileInfo);
|
||||
bool isTextMimeType(MimeType mimeType);
|
||||
|
||||
} // namespace Tools
|
||||
|
||||
#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
|
||||
|
||||
#include "attachments/AttachmentTypes.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
class EditEntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class QByteArray;
|
||||
class EntryAttachments;
|
||||
|
||||
class NewEntryAttachmentsDialog : public QDialog
|
||||
class EditEntryAttachmentsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NewEntryAttachmentsDialog(QPointer<EntryAttachments> attachments, QWidget* parent = nullptr);
|
||||
~NewEntryAttachmentsDialog() override;
|
||||
explicit EditEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||
~EditEntryAttachmentsDialog() override;
|
||||
|
||||
private slots:
|
||||
void saveAttachment();
|
||||
void fileNameTextChanged(const QString& fileName);
|
||||
void setAttachment(attachments::Attachment attachment);
|
||||
attachments::Attachment getAttachment() const;
|
||||
|
||||
private:
|
||||
bool validateFileName(const QString& fileName, QString& error) const;
|
||||
|
||||
QPointer<EntryAttachments> m_attachments;
|
||||
QScopedPointer<Ui::EntryAttachmentsDialog> m_ui;
|
||||
QScopedPointer<Ui::EditEntryAttachmentsDialog> 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());
|
||||
}
|
||||
|
||||
int EntryAttachmentsModel::rowByKey(const QString& key) const
|
||||
{
|
||||
if (!m_entryAttachments) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return m_entryAttachments->keys().indexOf(key);
|
||||
}
|
||||
|
||||
void EntryAttachmentsModel::attachmentChange(const QString& 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;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
QString keyByIndex(const QModelIndex& index) const;
|
||||
int rowByKey(const QString& key) const;
|
||||
|
||||
private slots:
|
||||
void attachmentChange(const QString& key);
|
||||
|
|
|
@ -17,23 +17,41 @@
|
|||
|
||||
#include "EntryAttachmentsWidget.h"
|
||||
|
||||
#include "EditEntryAttachmentsDialog.h"
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "NewEntryAttachmentsDialog.h"
|
||||
#include "PreviewEntryAttachmentsDialog.h"
|
||||
#include "ui_EntryAttachmentsWidget.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDropEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "EntryAttachmentsModel.h"
|
||||
#include "core/EntryAttachments.h"
|
||||
#include "core/Tools.h"
|
||||
#include "gui/FileDialog.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)
|
||||
: QWidget(parent)
|
||||
, m_ui(new Ui::EntryAttachmentsWidget)
|
||||
|
@ -67,14 +85,30 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
|
|||
// clang-format on
|
||||
connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
|
||||
|
||||
connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(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->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
|
||||
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->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();
|
||||
updateButtonsEnabled();
|
||||
}
|
||||
|
@ -175,19 +209,34 @@ void EntryAttachmentsWidget::newAttachments()
|
|||
return;
|
||||
}
|
||||
|
||||
NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this);
|
||||
if (newEntryDialog.exec() == QDialog::Accepted) {
|
||||
emit widgetUpdated();
|
||||
}
|
||||
// Create a temporary file to allow the user to edit the attachment
|
||||
auto newFileName = generateUniqueName(DefaultName, m_entryAttachments->keys());
|
||||
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()
|
||||
{
|
||||
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()) {
|
||||
qWarning() << tr("Failed to preview an attachment: Attachment not found");
|
||||
qWarning() << "Failed to preview an attachment: Attachment not found";
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -198,7 +247,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
|||
auto data = m_entryAttachments->value(name);
|
||||
|
||||
PreviewEntryAttachmentsDialog previewDialog(this);
|
||||
previewDialog.setAttachment(name, data);
|
||||
previewDialog.setAttachment({name, data});
|
||||
|
||||
connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments()));
|
||||
connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments()));
|
||||
|
@ -208,7 +257,7 @@ void EntryAttachmentsWidget::previewSelectedAttachment()
|
|||
&previewDialog,
|
||||
[&previewDialog, &name, this](const QString& key) {
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Q_ASSERT(m_entryAttachments);
|
||||
|
@ -346,35 +440,38 @@ void EntryAttachmentsWidget::openSelectedAttachments()
|
|||
|
||||
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->newAttachmentButton->setEnabled(!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->previewAttachmentButton->setEnabled(hasSelection);
|
||||
m_ui->openAttachmentButton->setEnabled(hasSelection);
|
||||
|
||||
updateSpacers();
|
||||
updateLinesVisibility();
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateSpacers()
|
||||
void EntryAttachmentsWidget::updateLinesVisibility()
|
||||
{
|
||||
if (m_buttonsVisible && !m_readOnly) {
|
||||
m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding);
|
||||
} else {
|
||||
m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
}
|
||||
m_ui->editPreviewLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
m_ui->previewRemoveLine->setVisible(m_buttonsVisible && !m_readOnly);
|
||||
}
|
||||
|
||||
void EntryAttachmentsWidget::updateButtonsVisible()
|
||||
{
|
||||
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);
|
||||
|
||||
updateSpacers();
|
||||
updateLinesVisibility();
|
||||
}
|
||||
|
||||
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
|
||||
|
|
|
@ -58,6 +58,7 @@ signals:
|
|||
private slots:
|
||||
void insertAttachments();
|
||||
void newAttachments();
|
||||
void editSelectedAttachment();
|
||||
void previewSelectedAttachment();
|
||||
void removeSelectedAttachments();
|
||||
void saveSelectedAttachments();
|
||||
|
@ -68,7 +69,7 @@ private slots:
|
|||
void attachmentModifiedExternally(const QString& key, const QString& filePath);
|
||||
|
||||
private:
|
||||
void updateSpacers();
|
||||
void updateLinesVisibility();
|
||||
|
||||
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>337</width>
|
||||
<height>258</height>
|
||||
<width>332</width>
|
||||
<height>312</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -47,7 +47,7 @@
|
|||
</item>
|
||||
<item>
|
||||
<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">
|
||||
<number>0</number>
|
||||
</property>
|
||||
|
@ -60,16 +60,6 @@
|
|||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="newAttachmentButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>New</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addAttachmentButton">
|
||||
<property name="enabled">
|
||||
|
@ -79,22 +69,26 @@
|
|||
<string>Add new attachment</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
<string>Add file…</string>
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="previewAttachmentButton">
|
||||
|
@ -128,22 +122,19 @@
|
|||
<string>Save selected attachment to disk</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
<string>Save…</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<widget class="Line" name="previewRemoveLine">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>173</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeAttachmentButton">
|
||||
|
@ -164,7 +155,7 @@
|
|||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<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 "ui_EntryAttachmentsDialog.h"
|
||||
#include "ui_PreviewEntryAttachmentsDialog.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QMimeDatabase>
|
||||
#include <QPushButton>
|
||||
#include <QTextCursor>
|
||||
#include <QtDebug>
|
||||
|
||||
PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::EntryAttachmentsDialog)
|
||||
, m_ui(new Ui::PreviewEntryAttachmentsDialog)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
setWindowTitle(tr("Preview entry attachment"));
|
||||
// Disable the help button in the title bar
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
// Set to read-only
|
||||
m_ui->titleEdit->setReadOnly(true);
|
||||
m_ui->attachmentTextEdit->setReadOnly(true);
|
||||
m_ui->errorLabel->setVisible(false);
|
||||
|
||||
// Initialize dialog buttons
|
||||
m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save);
|
||||
auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close);
|
||||
closeButton->setDefault(true);
|
||||
|
||||
connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject()));
|
||||
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) {
|
||||
auto pressedButton = m_ui->dialogButtons->standardButton(button);
|
||||
|
||||
const auto attachment = m_ui->attachmentWidget->getAttachment();
|
||||
if (pressedButton == QDialogButtonBox::Open) {
|
||||
emit openAttachment(m_name);
|
||||
emit openAttachment(attachment.name);
|
||||
} else if (pressedButton == QDialogButtonBox::Save) {
|
||||
emit saveAttachment(m_name);
|
||||
emit saveAttachment(attachment.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default;
|
||||
|
||||
void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data)
|
||||
void PreviewEntryAttachmentsDialog::setAttachment(attachments::Attachment attachment)
|
||||
{
|
||||
m_name = name;
|
||||
m_ui->titleEdit->setText(m_name);
|
||||
setWindowTitle(tr("Preview: %1").arg(attachment.name));
|
||||
|
||||
m_type = attachmentType(data);
|
||||
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();
|
||||
}
|
||||
m_ui->attachmentWidget->openAttachment(std::move(attachment), attachments::OpenMode::ReadOnly);
|
||||
}
|
||||
|
|
|
@ -17,14 +17,16 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "core/Tools.h"
|
||||
#include "attachments/AttachmentTypes.h"
|
||||
|
||||
#include <core/Tools.h>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class EntryAttachmentsDialog;
|
||||
class PreviewEntryAttachmentsDialog;
|
||||
}
|
||||
|
||||
class PreviewEntryAttachmentsDialog : public QDialog
|
||||
|
@ -35,29 +37,12 @@ public:
|
|||
explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr);
|
||||
~PreviewEntryAttachmentsDialog() override;
|
||||
|
||||
void setAttachment(const QString& name, const QByteArray& data);
|
||||
void setAttachment(attachments::Attachment attachment);
|
||||
|
||||
signals:
|
||||
void openAttachment(const QString& name);
|
||||
void saveAttachment(const QString& name);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
Tools::MimeType attachmentType(const QByteArray& data) const;
|
||||
|
||||
void update();
|
||||
void updateTextAttachment(const QByteArray& data);
|
||||
void updateImageAttachment(const QByteArray& data);
|
||||
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};
|
||||
QScopedPointer<Ui::PreviewEntryAttachmentsDialog> m_ui;
|
||||
};
|
||||
|
|
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 "core/Clock.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
|
@ -277,10 +279,8 @@ void TestTools::testMimeTypes()
|
|||
{
|
||||
const QStringList TextMimeTypes = {
|
||||
"text/plain", // Plain text
|
||||
"text/html", // HTML documents
|
||||
"text/css", // CSS stylesheets
|
||||
"text/javascript", // JavaScript files
|
||||
"text/markdown", // Markdown documents
|
||||
"text/xml", // XML documents
|
||||
"text/rtf", // Rich Text Format
|
||||
"text/vcard", // vCard files
|
||||
|
@ -327,6 +327,9 @@ void TestTools::testMimeTypes()
|
|||
"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) {
|
||||
QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText);
|
||||
}
|
||||
|
@ -339,3 +342,89 @@ void TestTools::testMimeTypes()
|
|||
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
|
||||
#define KEEPASSX_TESTTOOLS_H
|
||||
|
||||
#include "core/Tools.h"
|
||||
#include <QObject>
|
||||
|
||||
class TestTools : public QObject
|
||||
{
|
||||
|
@ -38,6 +38,9 @@ private slots:
|
|||
void testConvertToRegex_data();
|
||||
void testArrayContainsValues();
|
||||
void testMimeTypes();
|
||||
void testGetMimeType();
|
||||
void testGetMimeTypeByFileInfo();
|
||||
void testIsTextMimeType();
|
||||
};
|
||||
|
||||
#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 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)
|
||||
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
|
||||
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