diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index b93ebac90..82d986c40 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -663,6 +663,17 @@ + + AttachmentWidget + + Attachment Viewer + + + + Unknown attachment type + + + AutoType @@ -2661,10 +2672,6 @@ This is definitely a bug, please report it to the developers. No Results - - Save - - Enter a unique name or overwrite an existing search from the list: @@ -2833,6 +2840,17 @@ Disable safe saves and try again? Failed to save backup database: %1 + + Save + + + + + EditEntryAttachmentsDialog + + Edit: %1 + + EditEntryWidget @@ -3904,21 +3922,6 @@ This may cause the affected plugins to malfunction. - - EntryAttachmentsDialog - - Form - - - - File name - - - - File contents... - - - EntryAttachmentsModel @@ -3944,10 +3947,6 @@ This may cause the affected plugins to malfunction. Add new attachment - - Add - - Remove selected attachment @@ -3968,10 +3967,6 @@ This may cause the affected plugins to malfunction. Save selected attachment to disk - - Save - - Select files @@ -4065,16 +4060,28 @@ Error: %1 Would you like to overwrite the existing attachment? - - New - - Preview - Failed to preview an attachment: Attachment not found + Edit + + + + New Text Document + + + + Add file… + + + + Load from Disk… + + + + Save… @@ -4640,6 +4647,13 @@ You can enable the DuckDuckGo website icon service in the security section of th + + ImageAttachmentsWidget + + Zoom: + + + ImportWizard @@ -6466,25 +6480,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes. - - NewEntryAttachmentsDialog - - Attachment name cannot be empty - - - - Attachment with the same name already exists - - - - Save attachment - - - - New entry attachment - - - NixUtils @@ -7253,15 +7248,15 @@ Do you want to overwrite it? PreviewEntryAttachmentsDialog - Preview entry attachment + Form - No preview available + Preview: %1 - Image format not supported + Save… @@ -9248,6 +9243,10 @@ This option is deprecated, use --set-key-file instead. Tags + + Fit + + QtIOCompressor @@ -10182,6 +10181,24 @@ This option is deprecated, use --set-key-file instead. + + TextAttachmentsEditWidget + + Preview + + + + + TextAttachmentsPreviewWidget + + Form + + + + Type: + + + TotpDialog diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 66d074c9d..9ddc46cf9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 39c7ab8eb..814233941 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -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 diff --git a/src/core/Tools.h b/src/core/Tools.h index 7265f68bb..ee5ef612e 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -22,6 +22,7 @@ #include "core/Global.h" #include +#include #include #include #include @@ -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 diff --git a/src/gui/entry/EditEntryAttachmentsDialog.cpp b/src/gui/entry/EditEntryAttachmentsDialog.cpp new file mode 100644 index 000000000..047f0a6b4 --- /dev/null +++ b/src/gui/entry/EditEntryAttachmentsDialog.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "EditEntryAttachmentsDialog.h" +#include "ui_EditEntryAttachmentsDialog.h" + +#include + +#include +#include +#include +#include + +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(); +} diff --git a/src/gui/entry/NewEntryAttachmentsDialog.h b/src/gui/entry/EditEntryAttachmentsDialog.h similarity index 62% rename from src/gui/entry/NewEntryAttachmentsDialog.h rename to src/gui/entry/EditEntryAttachmentsDialog.h index 651100f65..38c77179a 100644 --- a/src/gui/entry/NewEntryAttachmentsDialog.h +++ b/src/gui/entry/EditEntryAttachmentsDialog.h @@ -17,32 +17,29 @@ #pragma once +#include "attachments/AttachmentTypes.h" + #include #include namespace Ui { - class EntryAttachmentsDialog; + class EditEntryAttachmentsDialog; } -class QByteArray; class EntryAttachments; -class NewEntryAttachmentsDialog : public QDialog +class EditEntryAttachmentsDialog : public QDialog { Q_OBJECT public: - explicit NewEntryAttachmentsDialog(QPointer 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 m_attachments; - QScopedPointer m_ui; + QScopedPointer m_ui; }; diff --git a/src/gui/entry/EditEntryAttachmentsDialog.ui b/src/gui/entry/EditEntryAttachmentsDialog.ui new file mode 100644 index 000000000..b958f966d --- /dev/null +++ b/src/gui/entry/EditEntryAttachmentsDialog.ui @@ -0,0 +1,46 @@ + + + EditEntryAttachmentsDialog + + + + 0 + 0 + 447 + 424 + + + + + + + + + + + 0 + 0 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + AttachmentWidget + QWidget +
gui/entry/attachments/AttachmentWidget.h
+ 1 +
+
+ + +
diff --git a/src/gui/entry/EntryAttachmentsDialog.ui b/src/gui/entry/EntryAttachmentsDialog.ui deleted file mode 100644 index 2b13ea0be..000000000 --- a/src/gui/entry/EntryAttachmentsDialog.ui +++ /dev/null @@ -1,55 +0,0 @@ - - - EntryAttachmentsDialog - - - - 0 - 0 - 402 - 300 - - - - Form - - - - - - File name - - - - - - - true - - - color: #FF9696 - - - - - - - - - - File contents... - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/src/gui/entry/EntryAttachmentsModel.cpp b/src/gui/entry/EntryAttachmentsModel.cpp index d66e912ab..96a55ed72 100644 --- a/src/gui/entry/EntryAttachmentsModel.cpp +++ b/src/gui/entry/EntryAttachmentsModel.cpp @@ -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); diff --git a/src/gui/entry/EntryAttachmentsModel.h b/src/gui/entry/EntryAttachmentsModel.h index d155f25ca..ea541ebf7 100644 --- a/src/gui/entry/EntryAttachmentsModel.h +++ b/src/gui/entry/EntryAttachmentsModel.h @@ -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); diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp index 744a65931..a3b7fc763 100644 --- a/src/gui/entry/EntryAttachmentsWidget.cpp +++ b/src/gui/entry/EntryAttachmentsWidget.cpp @@ -17,23 +17,41 @@ #include "EntryAttachmentsWidget.h" +#include "EditEntryAttachmentsDialog.h" #include "EntryAttachmentsModel.h" -#include "NewEntryAttachmentsDialog.h" #include "PreviewEntryAttachmentsDialog.h" #include "ui_EntryAttachmentsWidget.h" #include #include +#include +#include #include #include #include -#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(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) diff --git a/src/gui/entry/EntryAttachmentsWidget.h b/src/gui/entry/EntryAttachmentsWidget.h index 8c15fd68a..8d82d02d8 100644 --- a/src/gui/entry/EntryAttachmentsWidget.h +++ b/src/gui/entry/EntryAttachmentsWidget.h @@ -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); diff --git a/src/gui/entry/EntryAttachmentsWidget.ui b/src/gui/entry/EntryAttachmentsWidget.ui index 5b6de67aa..7fd960972 100644 --- a/src/gui/entry/EntryAttachmentsWidget.ui +++ b/src/gui/entry/EntryAttachmentsWidget.ui @@ -6,8 +6,8 @@ 0 0 - 337 - 258 + 332 + 312 @@ -47,7 +47,7 @@ - + 0 @@ -60,16 +60,6 @@ 0 - - - - false - - - New - - - @@ -79,22 +69,26 @@ Add new attachment - Add + Add file… - + + + false + + + Edit + + + + + - Qt::Vertical + Qt::Horizontal - - - 20 - 40 - - - + @@ -128,22 +122,19 @@ Save selected attachment to disk - Save + Save… - + + + true + - Qt::Vertical + Qt::Horizontal - - - 20 - 173 - - - + @@ -164,7 +155,7 @@ Qt::Vertical - QSizePolicy::Minimum + QSizePolicy::Expanding diff --git a/src/gui/entry/NewEntryAttachmentsDialog.cpp b/src/gui/entry/NewEntryAttachmentsDialog.cpp deleted file mode 100644 index 7adbff332..000000000 --- a/src/gui/entry/NewEntryAttachmentsDialog.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2025 KeePassXC Team - * - * 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 . - */ - -#include "NewEntryAttachmentsDialog.h" -#include "core/EntryAttachments.h" -#include "ui_EntryAttachmentsDialog.h" - -#include -#include - -NewEntryAttachmentsDialog::NewEntryAttachmentsDialog(QPointer 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); - } -} diff --git a/src/gui/entry/PreviewEntryAttachmentsDialog.cpp b/src/gui/entry/PreviewEntryAttachmentsDialog.cpp index 69de02606..62be2bd34 100644 --- a/src/gui/entry/PreviewEntryAttachmentsDialog.cpp +++ b/src/gui/entry/PreviewEntryAttachmentsDialog.cpp @@ -16,117 +16,48 @@ */ #include "PreviewEntryAttachmentsDialog.h" -#include "ui_EntryAttachmentsDialog.h" +#include "ui_PreviewEntryAttachmentsDialog.h" +#include #include #include #include -#include -#include 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); } diff --git a/src/gui/entry/PreviewEntryAttachmentsDialog.h b/src/gui/entry/PreviewEntryAttachmentsDialog.h index 43bf41abd..cee23ea13 100644 --- a/src/gui/entry/PreviewEntryAttachmentsDialog.h +++ b/src/gui/entry/PreviewEntryAttachmentsDialog.h @@ -17,14 +17,16 @@ #pragma once -#include "core/Tools.h" +#include "attachments/AttachmentTypes.h" + +#include #include #include 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 m_ui; - - QString m_name; - QByteArray m_data; - QImage m_imageCache; - Tools::MimeType m_type{Tools::MimeType::Unknown}; + QScopedPointer m_ui; }; diff --git a/src/gui/entry/PreviewEntryAttachmentsDialog.ui b/src/gui/entry/PreviewEntryAttachmentsDialog.ui new file mode 100644 index 000000000..d7f8fd31c --- /dev/null +++ b/src/gui/entry/PreviewEntryAttachmentsDialog.ui @@ -0,0 +1,58 @@ + + + PreviewEntryAttachmentsDialog + + + + 0 + 0 + 557 + 454 + + + + Form + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 0 + 0 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + AttachmentWidget + QWidget +
gui/entry/attachments/AttachmentWidget.h
+ 1 +
+
+ + +
diff --git a/src/gui/entry/attachments/AttachmentTypes.h b/src/gui/entry/attachments/AttachmentTypes.h new file mode 100644 index 000000000..9d63bc23e --- /dev/null +++ b/src/gui/entry/attachments/AttachmentTypes.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include +#include + +namespace attachments +{ + struct Attachment + { + QString name; + QByteArray data; + }; + + enum class OpenMode + { + ReadOnly, + ReadWrite + }; + +} // namespace attachments diff --git a/src/gui/entry/attachments/AttachmentWidget.cpp b/src/gui/entry/attachments/AttachmentWidget.cpp new file mode 100644 index 000000000..f68df2d00 --- /dev/null +++ b/src/gui/entry/attachments/AttachmentWidget.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "AttachmentWidget.h" + +#include "ImageAttachmentsWidget.h" +#include "TextAttachmentsWidget.h" + +#include + +#include +#include + +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(m_attachmentWidget)) { + return textWidget->getAttachment(); + } + + return m_attachment; +} diff --git a/src/gui/entry/attachments/AttachmentWidget.h b/src/gui/entry/attachments/AttachmentWidget.h new file mode 100644 index 000000000..73dd5b2b5 --- /dev/null +++ b/src/gui/entry/attachments/AttachmentWidget.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "AttachmentTypes.h" + +#include + +#include +#include +#include + +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 m_attachmentWidget; + + attachments::Attachment m_attachment; + attachments::OpenMode m_mode; +}; diff --git a/src/gui/entry/attachments/ImageAttachmentsView.cpp b/src/gui/entry/attachments/ImageAttachmentsView.cpp new file mode 100644 index 000000000..fea7a2d5f --- /dev/null +++ b/src/gui/entry/attachments/ImageAttachmentsView.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "ImageAttachmentsView.h" + +#include +#include + +#include + +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::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::quiet_NaN(); +} diff --git a/src/gui/entry/attachments/ImageAttachmentsView.h b/src/gui/entry/attachments/ImageAttachmentsView.h new file mode 100644 index 000000000..3fa4f76b6 --- /dev/null +++ b/src/gui/entry/attachments/ImageAttachmentsView.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +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; +}; diff --git a/src/gui/entry/attachments/ImageAttachmentsWidget.cpp b/src/gui/entry/attachments/ImageAttachmentsWidget.cpp new file mode 100644 index 000000000..3d547433c --- /dev/null +++ b/src/gui/entry/attachments/ImageAttachmentsWidget.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "ImageAttachmentsWidget.h" + +#include "ui_ImageAttachmentsWidget.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +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::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; +} diff --git a/src/gui/entry/attachments/ImageAttachmentsWidget.h b/src/gui/entry/attachments/ImageAttachmentsWidget.h new file mode 100644 index 000000000..6c5ab95a3 --- /dev/null +++ b/src/gui/entry/attachments/ImageAttachmentsWidget.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "AttachmentTypes.h" + +#include +#include +#include + +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 m_ui; + attachments::Attachment m_attachment; + + QPointer m_scene; + QPointer m_zoomHelper; +}; diff --git a/src/gui/entry/attachments/ImageAttachmentsWidget.ui b/src/gui/entry/attachments/ImageAttachmentsWidget.ui new file mode 100644 index 000000000..d95f6ea9a --- /dev/null +++ b/src/gui/entry/attachments/ImageAttachmentsWidget.ui @@ -0,0 +1,90 @@ + + + ImageAttachmentsWidget + + + + 0 + 0 + 400 + 300 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Zoom: + + + + + + + + 0 + 0 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + + ImageAttachmentsView + QGraphicsView +
gui/entry/attachments/ImageAttachmentsView.h
+
+
+ + +
diff --git a/src/gui/entry/attachments/TextAttachmentsEditWidget.cpp b/src/gui/entry/attachments/TextAttachmentsEditWidget.cpp new file mode 100644 index 000000000..1c0bcd223 --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsEditWidget.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "TextAttachmentsEditWidget.h" +#include "ui_TextAttachmentsEditWidget.h" + +#include +#include +#include + +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); +} diff --git a/src/gui/entry/attachments/TextAttachmentsEditWidget.h b/src/gui/entry/attachments/TextAttachmentsEditWidget.h new file mode 100644 index 000000000..0879d9251 --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsEditWidget.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "AttachmentTypes.h" + +#include +#include + +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 m_ui; + + attachments::Attachment m_attachment; + attachments::OpenMode m_mode; +}; diff --git a/src/gui/entry/attachments/TextAttachmentsEditWidget.ui b/src/gui/entry/attachments/TextAttachmentsEditWidget.ui new file mode 100644 index 000000000..e73894d15 --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsEditWidget.ui @@ -0,0 +1,66 @@ + + + TextAttachmentsEditWidget + + + + 0 + 0 + 400 + 300 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Preview + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + diff --git a/src/gui/entry/attachments/TextAttachmentsPreviewWidget.cpp b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.cpp new file mode 100644 index 000000000..4775f289e --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "TextAttachmentsPreviewWidget.h" +#include "ui_TextAttachmentsPreviewWidget.h" + +#include + +#include +#include +#include +#include +#include + +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(); + 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::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 +} diff --git a/src/gui/entry/attachments/TextAttachmentsPreviewWidget.h b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.h new file mode 100644 index 000000000..c7d20eb9d --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "AttachmentTypes.h" + +#include +#include + +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 m_ui; + + attachments::Attachment m_attachment; +}; diff --git a/src/gui/entry/attachments/TextAttachmentsPreviewWidget.ui b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.ui new file mode 100644 index 000000000..2ee97aa22 --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsPreviewWidget.ui @@ -0,0 +1,70 @@ + + + TextAttachmentsPreviewWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Type: + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + diff --git a/src/gui/entry/attachments/TextAttachmentsWidget.cpp b/src/gui/entry/attachments/TextAttachmentsWidget.cpp new file mode 100644 index 000000000..d0935c6af --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsWidget.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#include "TextAttachmentsWidget.h" +#include "TextAttachmentsEditWidget.h" +#include "TextAttachmentsPreviewWidget.h" + +#include "ui_TextAttachmentsWidget.h" + +#include +#include +#include + +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(); +} diff --git a/src/gui/entry/attachments/TextAttachmentsWidget.h b/src/gui/entry/attachments/TextAttachmentsWidget.h new file mode 100644 index 000000000..f4709823f --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsWidget.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "AttachmentTypes.h" + +#include +#include +#include + +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 m_ui; + QPointer m_splitter; + QPointer m_editWidget; + QPointer m_previewWidget; + QPointer m_previewUpdateTimer; + + attachments::Attachment m_attachment; + attachments::OpenMode m_mode; +}; diff --git a/src/gui/entry/attachments/TextAttachmentsWidget.ui b/src/gui/entry/attachments/TextAttachmentsWidget.ui new file mode 100644 index 000000000..9c18a7c60 --- /dev/null +++ b/src/gui/entry/attachments/TextAttachmentsWidget.ui @@ -0,0 +1,36 @@ + + + TextAttachmentsWidget + + + + 0 + 0 + 732 + 432 + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp index 27a468929..a373e1523 100644 --- a/tests/TestTools.cpp +++ b/tests/TestTools.cpp @@ -18,7 +18,9 @@ #include "TestTools.h" #include "core/Clock.h" +#include "core/Tools.h" +#include #include #include #include @@ -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)); + } +} diff --git a/tests/TestTools.h b/tests/TestTools.h index 5f4b6b6e0..728849bd3 100644 --- a/tests/TestTools.h +++ b/tests/TestTools.h @@ -18,7 +18,7 @@ #ifndef KEEPASSX_TESTTOOLS_H #define KEEPASSX_TESTTOOLS_H -#include "core/Tools.h" +#include 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 diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt index 2734cf582..30f255778 100644 --- a/tests/gui/CMakeLists.txt +++ b/tests/gui/CMakeLists.txt @@ -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() diff --git a/tests/gui/attachments/TestAttachmentWidget.cpp b/tests/gui/attachments/TestAttachmentWidget.cpp new file mode 100644 index 000000000..a0d17fcac --- /dev/null +++ b/tests/gui/attachments/TestAttachmentWidget.cpp @@ -0,0 +1,94 @@ +#include "TestAttachmentWidget.h" + +#include + +#include +#include +#include + +#include +#include +#include + +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 = "

test

"}, + 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("verticalLayout"); + QVERIFY(layout); + + QCOMPARE(layout->count(), 1); + + auto item = layout->itemAt(0); + QVERIFY(item); + + QVERIFY(qobject_cast(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("verticalLayout"); + QVERIFY(layout); + + QCOMPARE(layout->count(), 1); + + auto item = layout->itemAt(0); + QVERIFY(item); + + QVERIFY(qobject_cast(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("verticalLayout"); + QVERIFY(layout); + + QCOMPARE(layout->count(), 1); + + auto item = layout->itemAt(0); + QVERIFY(item); + + auto label = qobject_cast(item->widget()); + QVERIFY(label); + + QVERIFY(!label->text().isEmpty()); + + auto actualAttachment = m_attachmentWidget->getAttachment(); + QCOMPARE(actualAttachment.name, Attachment.name); + QCOMPARE(actualAttachment.data, Attachment.data); +} diff --git a/tests/gui/attachments/TestAttachmentWidget.h b/tests/gui/attachments/TestAttachmentWidget.h new file mode 100644 index 000000000..4367234cd --- /dev/null +++ b/tests/gui/attachments/TestAttachmentWidget.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include + +class TestAttachmentsWidget : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testTextAttachment(); + void testImageAttachment(); + void testUnknownAttachment(); + +private: + QScopedPointer m_attachmentWidget; +}; diff --git a/tests/gui/attachments/TestAttachmentsGui.cpp b/tests/gui/attachments/TestAttachmentsGui.cpp new file mode 100644 index 000000000..c29626df5 --- /dev/null +++ b/tests/gui/attachments/TestAttachmentsGui.cpp @@ -0,0 +1,46 @@ +#include + +#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 +#include + +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; +} \ No newline at end of file diff --git a/tests/gui/attachments/TestEditEntryAttachmentsDialog.cpp b/tests/gui/attachments/TestEditEntryAttachmentsDialog.cpp new file mode 100644 index 000000000..0353b6a6f --- /dev/null +++ b/tests/gui/attachments/TestEditEntryAttachmentsDialog.cpp @@ -0,0 +1,95 @@ +#include "TestEditEntryAttachmentsDialog.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +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("verticalLayout"); + QVERIFY2(layout, "QVBoxLayout not found"); + QCOMPARE(layout->count(), 2); + + auto widget = qobject_cast(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("verticalLayout"); + QVERIFY2(layout, "QVBoxLayout not found"); + QCOMPARE(layout->count(), 2); + + auto widget = qobject_cast(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(); + QVERIFY2(buttonsBox, "ButtonsBox not found"); + + for (auto button : buttonsBox->buttons()) { + QTest::mouseClick(button, Qt::LeftButton); + } + + QCOMPARE(acceptButton.count(), 1); + QCOMPARE(closeButton.count(), 1); +} diff --git a/tests/gui/attachments/TestEditEntryAttachmentsDialog.h b/tests/gui/attachments/TestEditEntryAttachmentsDialog.h new file mode 100644 index 000000000..76f3ac2ff --- /dev/null +++ b/tests/gui/attachments/TestEditEntryAttachmentsDialog.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "EditEntryAttachmentsDialog.h" + +#include +#include + +class TestEditEntryAttachmentsDialog : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testSetAttachment(); + void testSetAttachmentTwice(); + + void testBottonsBox(); + +private: + QScopedPointer m_editDialog{}; +}; diff --git a/tests/gui/attachments/TestImageAttachmentsView.cpp b/tests/gui/attachments/TestImageAttachmentsView.cpp new file mode 100644 index 000000000..0d3ffed00 --- /dev/null +++ b/tests/gui/attachments/TestImageAttachmentsView.cpp @@ -0,0 +1,76 @@ +#include "TestImageAttachmentsView.h" + +#include + +#include +#include +#include +#include +#include + +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); +} diff --git a/tests/gui/attachments/TestImageAttachmentsView.h b/tests/gui/attachments/TestImageAttachmentsView.h new file mode 100644 index 000000000..06f8c58d3 --- /dev/null +++ b/tests/gui/attachments/TestImageAttachmentsView.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include + +class TestImageAttachmentsView : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testEmitWheelEvent(); + void testEnableFit(); + void testDisableFit(); + +private: + QScopedPointer m_view{}; +}; diff --git a/tests/gui/attachments/TestImageAttachmentsWidget.cpp b/tests/gui/attachments/TestImageAttachmentsWidget.cpp new file mode 100644 index 000000000..fc2d14e2d --- /dev/null +++ b/tests/gui/attachments/TestImageAttachmentsWidget.cpp @@ -0,0 +1,244 @@ +#include "TestImageAttachmentsWidget.h" + +#include +#include + +#include +#include +#include +#include +#include + +void TestImageAttachmentsWidget::initTestCase() +{ + m_widget.reset(new ImageAttachmentsWidget()); + m_zoomCombobox = m_widget->findChild("zoomComboBox"); + QVERIFY(m_zoomCombobox); + + m_imageAttachmentsView = m_widget->findChild("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); +} diff --git a/tests/gui/attachments/TestImageAttachmentsWidget.h b/tests/gui/attachments/TestImageAttachmentsWidget.h new file mode 100644 index 000000000..6f27be2f6 --- /dev/null +++ b/tests/gui/attachments/TestImageAttachmentsWidget.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include +#include + +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 m_widget{}; + QPointer m_zoomCombobox{}; + QPointer m_imageAttachmentsView{}; +}; diff --git a/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.cpp b/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.cpp new file mode 100644 index 000000000..ca0d6ce20 --- /dev/null +++ b/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.cpp @@ -0,0 +1,97 @@ +#include "TestPreviewEntryAttachmentsDialog.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +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("verticalLayout"); + QVERIFY2(layout, "QVBoxLayout not found"); + QCOMPARE(layout->count(), 2); + + auto widget = qobject_cast(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("verticalLayout"); + QVERIFY2(layout, "QVBoxLayout not found"); + QCOMPARE(layout->count(), 2); + + auto widget = qobject_cast(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("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); +} diff --git a/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.h b/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.h new file mode 100644 index 000000000..7b5899b9e --- /dev/null +++ b/tests/gui/attachments/TestPreviewEntryAttachmentsDialog.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include "PreviewEntryAttachmentsDialog.h" + +#include + +#include + +class TestPreviewEntryAttachmentsDialog : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testSetAttachment(); + void testSetAttachmentTwice(); + + void testBottonsBox(); + +private: + QScopedPointer m_previewDialog{}; +}; diff --git a/tests/gui/attachments/TestTextAttachmentsEditWidget.cpp b/tests/gui/attachments/TestTextAttachmentsEditWidget.cpp new file mode 100644 index 000000000..963533bd7 --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsEditWidget.cpp @@ -0,0 +1,49 @@ +#include "TestTextAttachmentsEditWidget.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +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("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("previewPushButton"); + QVERIFY(previewButton); + + QTest::mouseClick(previewButton, Qt::LeftButton); + + QCOMPARE(previwButtonClickedSignal.count(), 1); +} diff --git a/tests/gui/attachments/TestTextAttachmentsEditWidget.h b/tests/gui/attachments/TestTextAttachmentsEditWidget.h new file mode 100644 index 000000000..3d89f2a2e --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsEditWidget.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include + +class TestTextAttachmentsEditWidget : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testEmitTextChanged(); + void testEmitPreviewButtonClicked(); + +private: + QScopedPointer m_widget{}; +}; diff --git a/tests/gui/attachments/TestTextAttachmentsPreviewWidget.cpp b/tests/gui/attachments/TestTextAttachmentsPreviewWidget.cpp new file mode 100644 index 000000000..0ddea53e3 --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsPreviewWidget.cpp @@ -0,0 +1,40 @@ +#include "TestTextAttachmentsPreviewWidget.h" + +#include + +#include +#include + +void TestTextAttachmentsPreviewWidget::initTestCase() +{ + m_widget.reset(new TextAttachmentsPreviewWidget()); +} + +void TestTextAttachmentsPreviewWidget::testDetectMimeByFile() +{ + const auto combobox = m_widget->findChild("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 +} diff --git a/tests/gui/attachments/TestTextAttachmentsPreviewWidget.h b/tests/gui/attachments/TestTextAttachmentsPreviewWidget.h new file mode 100644 index 000000000..8e34a8d9b --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsPreviewWidget.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include + +class TestTextAttachmentsPreviewWidget : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testDetectMimeByFile(); + +private: + QScopedPointer m_widget{}; +}; diff --git a/tests/gui/attachments/TestTextAttachmentsWidget.cpp b/tests/gui/attachments/TestTextAttachmentsWidget.cpp new file mode 100644 index 000000000..bba6f9840 --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsWidget.cpp @@ -0,0 +1,224 @@ +#include "TestTextAttachmentsWidget.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +void TestTextAttachmentsWidget::initTestCase() +{ + m_textWidget.reset(new TextAttachmentsWidget()); +} + +void TestTextAttachmentsWidget::testInitTextWidget() +{ + auto splitter = m_textWidget->findChild(); + QVERIFY2(splitter, "Splitter not found"); + + QCOMPARE(splitter->count(), 2); + QVERIFY2(qobject_cast(splitter->widget(0)), "EditTextWidget not found"); + QVERIFY2(qobject_cast(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(); + 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(splitter->widget(0)); + QVERIFY(widget); + auto attachments = widget->getAttachment(); + + QCOMPARE(attachments.name, Test.name); + QCOMPARE(attachments.data, Test.data); + + auto previewWidget = qobject_cast(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(); + 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(splitter->widget(0)); + QVERIFY(widget); + auto attachments = widget->getAttachment(); + + QCOMPARE(attachments.name, Test.name); + QCOMPARE(attachments.data, Test.data); + + auto previewWidget = qobject_cast(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(); + QVERIFY2(splitter, "Splitter not found"); + QCOMPARE(splitter->sizes().size(), 2); + + auto editWidget = qobject_cast(splitter->widget(0)); + QVERIFY2(editWidget, "Edit widget not found"); + + auto textEdit = editWidget->findChild(); + 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(); + QVERIFY2(splitter, "Splitter not found"); + QCOMPARE(splitter->sizes().size(), 2); + + auto editWidget = qobject_cast(splitter->widget(0)); + QVERIFY2(editWidget, "Edit widget not found"); + + auto textEdit = editWidget->findChild(); + 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(); + 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(); + QVERIFY2(splitter, "Splitter not found"); + QCOMPARE(splitter->sizes().size(), 2); + + splitter->setSizes({1, 1}); + + QCoreApplication::processEvents(); + + auto editWidget = qobject_cast(splitter->widget(0)); + QVERIFY2(editWidget, "Edit widget not found"); + + auto textEdit = editWidget->findChild(); + 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(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(); + QVERIFY2(splitter, "Splitter not found"); + QCOMPARE(splitter->sizes().size(), 2); + + auto editWidget = qobject_cast(splitter->widget(0)); + QVERIFY2(editWidget, "Edit widget not found"); + QVERIFY(editWidget->isVisible()); + + auto previewButton = editWidget->findChild("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); +} diff --git a/tests/gui/attachments/TestTextAttachmentsWidget.h b/tests/gui/attachments/TestTextAttachmentsWidget.h new file mode 100644 index 000000000..3c8198dfa --- /dev/null +++ b/tests/gui/attachments/TestTextAttachmentsWidget.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * 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 . + */ + +#pragma once + +#include + +#include +#include + +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 m_textWidget; +};