From 99c8936568c64a065052bf842a569e5fb8645335 Mon Sep 17 00:00:00 2001 From: Kuznetsov Oleg Date: Sun, 12 Jan 2025 07:08:19 +0300 Subject: [PATCH] Add New/Preview Entry Attachments dialog and functionality (#11637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #11506 Closes #3383 * This change adds a new opportunity to add attachments that don’t require a real file in the file system. * Add a new dialog window to add and preview attachments and integrate it into the EntryAttachmentsWidget. * Attachment preview support for images and plain text files. Additional enhancements: * Fix sizing of attachment columns * Add padding to attachment table items * Fix targeting of preview widget styling to not impact unintended children --- share/translations/keepassxc_en.ts | 69 ++++++++-- src/CMakeLists.txt | 2 + src/core/Tools.cpp | 28 ++++ src/core/Tools.h | 9 ++ src/gui/EntryPreviewWidget.ui | 21 +++ src/gui/entry/EntryAttachmentsDialog.ui | 55 ++++++++ src/gui/entry/EntryAttachmentsWidget.cpp | 101 +++++++++++--- src/gui/entry/EntryAttachmentsWidget.h | 5 +- src/gui/entry/EntryAttachmentsWidget.ui | 75 ++++++++--- src/gui/entry/NewEntryAttachmentsDialog.cpp | 92 +++++++++++++ src/gui/entry/NewEntryAttachmentsDialog.h | 48 +++++++ .../entry/PreviewEntryAttachmentsDialog.cpp | 123 ++++++++++++++++++ src/gui/entry/PreviewEntryAttachmentsDialog.h | 59 +++++++++ src/gui/styles/base/basestyle.qss | 7 +- tests/TestTools.cpp | 67 ++++++++++ tests/TestTools.h | 1 + 16 files changed, 716 insertions(+), 46 deletions(-) create mode 100644 src/gui/entry/EntryAttachmentsDialog.ui create mode 100644 src/gui/entry/NewEntryAttachmentsDialog.cpp create mode 100644 src/gui/entry/NewEntryAttachmentsDialog.h create mode 100644 src/gui/entry/PreviewEntryAttachmentsDialog.cpp create mode 100644 src/gui/entry/PreviewEntryAttachmentsDialog.h diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index afa047327c..2492166182 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3697,6 +3697,21 @@ This may cause the affected plugins to malfunction. + + EntryAttachmentsDialog + + Form + + + + File name + + + + File contents... + + + EntryAttachmentsModel @@ -3734,14 +3749,6 @@ This may cause the affected plugins to malfunction. Remove - - Rename selected attachment - - - - Rename - - Open selected attachment @@ -3851,6 +3858,18 @@ Error: %1 Would you like to overwrite the existing attachment? + + New + + + + Preview + + + + Failed to preview an attachment: Attachment not found + + EntryAttributesModel @@ -6039,6 +6058,25 @@ We recommend you use the AppImage available on our downloads page. + + NewEntryAttachmentsDialog + + Attachment name cannot be empty + + + + Attachment with the same name already exists + + + + Save attachment + + + + New entry attachment + + + NixUtils @@ -6785,6 +6823,21 @@ Do you want to overwrite it? + + PreviewEntryAttachmentsDialog + + Preview entry attachment + + + + No preview available + + + + Image format not supported + + + QMessageBox diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fb9a26ce22..cfcb3f9de7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -130,6 +130,8 @@ set(keepassx_SOURCES gui/entry/EntryAttachmentsModel.cpp gui/entry/EntryAttachmentsWidget.cpp gui/entry/EntryAttributesModel.cpp + gui/entry/NewEntryAttachmentsDialog.cpp + gui/entry/PreviewEntryAttachmentsDialog.cpp gui/entry/EntryHistoryModel.cpp gui/entry/EntryModel.cpp gui/entry/EntryView.cpp diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index dc62c6a179..52037ea9aa 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -477,4 +477,32 @@ namespace Tools return pattern; } + + MimeType toMimeType(const QString& mimeName) + { + static QStringList textFormats = { + "text/", + "application/json", + "application/xml", + "application/soap+xml", + "application/x-yaml", + "application/protobuf", + }; + static QStringList imageFormats = {"image/"}; + + static auto isCompatible = [](const QString& format, const QStringList& list) { + return std::any_of( + list.cbegin(), list.cend(), [&format](const auto& item) { return format.startsWith(item); }); + }; + + if (isCompatible(mimeName, imageFormats)) { + return MimeType::Image; + } + + if (isCompatible(mimeName, textFormats)) { + return MimeType::PlainText; + } + + return MimeType::Unknown; + } } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 61d93ffbdb..168eeca615 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -118,6 +118,15 @@ namespace Tools QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties = {"objectName"}); QString substituteBackupFilePath(QString pattern, const QString& databasePath); + + enum class MimeType : uint8_t + { + Image, + PlainText, + Unknown + }; + + MimeType toMimeType(const QString& mimeName); } // namespace Tools #endif // KEEPASSX_TOOLS_H diff --git a/src/gui/EntryPreviewWidget.ui b/src/gui/EntryPreviewWidget.ui index 9b4e499605..b2cdecbbab 100644 --- a/src/gui/EntryPreviewWidget.ui +++ b/src/gui/EntryPreviewWidget.ui @@ -288,6 +288,9 @@ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + true + @@ -325,6 +328,9 @@ Qt::TextBrowserInteraction + + true + @@ -409,6 +415,9 @@ true + + true + @@ -482,6 +491,9 @@ true + + true + @@ -494,6 +506,9 @@ Tags list + + true + @@ -516,6 +531,9 @@ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + true + @@ -1109,6 +1127,9 @@ true + + true + diff --git a/src/gui/entry/EntryAttachmentsDialog.ui b/src/gui/entry/EntryAttachmentsDialog.ui new file mode 100644 index 0000000000..2b13ea0be1 --- /dev/null +++ b/src/gui/entry/EntryAttachmentsDialog.ui @@ -0,0 +1,55 @@ + + + EntryAttachmentsDialog + + + + 0 + 0 + 402 + 300 + + + + Form + + + + + + File name + + + + + + + true + + + color: #FF9696 + + + + + + + + + + File contents... + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp index d8634f2753..168277ab5b 100644 --- a/src/gui/entry/EntryAttachmentsWidget.cpp +++ b/src/gui/entry/EntryAttachmentsWidget.cpp @@ -16,16 +16,19 @@ */ #include "EntryAttachmentsWidget.h" + +#include "EntryAttachmentsModel.h" +#include "NewEntryAttachmentsDialog.h" +#include "PreviewEntryAttachmentsDialog.h" #include "ui_EntryAttachmentsWidget.h" -#include +#include #include #include #include #include #include "EntryAttachmentsModel.h" -#include "core/Config.h" #include "core/EntryAttachments.h" #include "core/Tools.h" #include "gui/FileDialog.h" @@ -46,12 +49,12 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent) m_ui->attachmentsView->viewport()->installEventFilter(this); m_ui->attachmentsView->setModel(m_attachmentsModel); - m_ui->attachmentsView->verticalHeader()->hide(); - m_ui->attachmentsView->horizontalHeader()->setStretchLastSection(true); - m_ui->attachmentsView->horizontalHeader()->resizeSection(EntryAttachmentsModel::NameColumn, 400); - m_ui->attachmentsView->setSelectionBehavior(QAbstractItemView::SelectRows); - m_ui->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection); - m_ui->attachmentsView->setEditTriggers(QAbstractItemView::SelectedClicked); + m_ui->attachmentsView->horizontalHeader()->setMinimumSectionSize(70); + m_ui->attachmentsView->horizontalHeader()->setSectionResizeMode(EntryAttachmentsModel::NameColumn, + QHeaderView::Stretch); + m_ui->attachmentsView->horizontalHeader()->setSectionResizeMode(EntryAttachmentsModel::SizeColumn, + QHeaderView::ResizeToContents); + m_ui->attachmentsView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); connect(this, SIGNAL(buttonsVisibleChanged(bool)), this, SLOT(updateButtonsVisible())); connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled())); @@ -64,12 +67,13 @@ EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent) // clang-format on connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool))); - connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex))); + connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(previewSelectedAttachment())); connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments())); connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments())); connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments())); + connect(m_ui->newAttachmentButton, SIGNAL(clicked()), SLOT(newAttachments())); + connect(m_ui->previewAttachmentButton, SIGNAL(clicked()), SLOT(previewSelectedAttachment())); connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments())); - connect(m_ui->renameAttachmentButton, SIGNAL(clicked()), SLOT(renameSelectedAttachments())); updateButtonsVisible(); updateButtonsEnabled(); @@ -165,6 +169,57 @@ void EntryAttachmentsWidget::insertAttachments() emit widgetUpdated(); } +void EntryAttachmentsWidget::newAttachments() +{ + Q_ASSERT(m_entryAttachments); + Q_ASSERT(!isReadOnly()); + if (isReadOnly()) { + return; + } + + NewEntryAttachmentsDialog newEntryDialog(m_entryAttachments, this); + if (newEntryDialog.exec() == QDialog::Accepted) { + emit widgetUpdated(); + } +} + +void EntryAttachmentsWidget::previewSelectedAttachment() +{ + Q_ASSERT(m_entryAttachments); + + const auto index = m_ui->attachmentsView->selectionModel()->selectedIndexes().first(); + if (!index.isValid()) { + qWarning() << tr("Failed to preview an attachment: Attachment not found"); + return; + } + + // Set selection to the first + m_ui->attachmentsView->setCurrentIndex(index); + + auto name = m_attachmentsModel->keyByIndex(index); + auto data = m_entryAttachments->value(name); + + PreviewEntryAttachmentsDialog previewDialog(this); + previewDialog.setAttachment(name, data); + + connect(&previewDialog, SIGNAL(openAttachment(QString)), SLOT(openSelectedAttachments())); + connect(&previewDialog, SIGNAL(saveAttachment(QString)), SLOT(saveSelectedAttachments())); + // Refresh the preview if the attachment changes + connect(m_entryAttachments, + &EntryAttachments::keyModified, + &previewDialog, + [&previewDialog, &name, this](const QString& key) { + if (key == name) { + previewDialog.setAttachment(name, m_entryAttachments->value(name)); + } + }); + + previewDialog.exec(); + + // Set focus back to the widget to allow keyboard navigation + setFocus(); +} + void EntryAttachmentsWidget::removeSelectedAttachments() { Q_ASSERT(m_entryAttachments); @@ -194,12 +249,6 @@ void EntryAttachmentsWidget::removeSelectedAttachments() } } -void EntryAttachmentsWidget::renameSelectedAttachments() -{ - Q_ASSERT(m_entryAttachments); - m_ui->attachmentsView->edit(m_ui->attachmentsView->selectionModel()->selectedIndexes().first()); -} - void EntryAttachmentsWidget::saveSelectedAttachments() { Q_ASSERT(m_entryAttachments); @@ -289,7 +338,7 @@ void EntryAttachmentsWidget::openSelectedAttachments() if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) { const QString filename = m_attachmentsModel->keyByIndex(index); errors.append(QString("%1 - %2").arg(filename, errorMessage)); - }; + } } if (!errors.isEmpty()) { @@ -302,18 +351,32 @@ void EntryAttachmentsWidget::updateButtonsEnabled() const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection(); m_ui->addAttachmentButton->setEnabled(!m_readOnly); + m_ui->newAttachmentButton->setEnabled(!m_readOnly); m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly); - m_ui->renameAttachmentButton->setEnabled(hasSelection && !m_readOnly); m_ui->saveAttachmentButton->setEnabled(hasSelection); + m_ui->previewAttachmentButton->setEnabled(hasSelection); m_ui->openAttachmentButton->setEnabled(hasSelection); + + updateSpacers(); +} + +void EntryAttachmentsWidget::updateSpacers() +{ + if (m_buttonsVisible && !m_readOnly) { + m_ui->previewVSpacer->changeSize(20, 40, QSizePolicy::Fixed, QSizePolicy::Expanding); + } else { + m_ui->previewVSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); + } } void EntryAttachmentsWidget::updateButtonsVisible() { m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly); + m_ui->newAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly); m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly); - m_ui->renameAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly); + + updateSpacers(); } bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage) diff --git a/src/gui/entry/EntryAttachmentsWidget.h b/src/gui/entry/EntryAttachmentsWidget.h index 9d64ed31b8..fc0e1980e6 100644 --- a/src/gui/entry/EntryAttachmentsWidget.h +++ b/src/gui/entry/EntryAttachmentsWidget.h @@ -57,8 +57,9 @@ public slots: private slots: void insertAttachments(); + void newAttachments(); + void previewSelectedAttachment(); void removeSelectedAttachments(); - void renameSelectedAttachments(); void saveSelectedAttachments(); void openAttachment(const QModelIndex& index); void openSelectedAttachments(); @@ -67,6 +68,8 @@ private slots: void attachmentModifiedExternally(const QString& key, const QString& filePath); private: + void updateSpacers(); + bool insertAttachments(const QStringList& fileNames, QString& errorMessage); QStringList confirmAttachmentSelection(const QStringList& filenames); diff --git a/src/gui/entry/EntryAttachmentsWidget.ui b/src/gui/entry/EntryAttachmentsWidget.ui index e685813b3d..5b6de67aa2 100644 --- a/src/gui/entry/EntryAttachmentsWidget.ui +++ b/src/gui/entry/EntryAttachmentsWidget.ui @@ -7,7 +7,7 @@ 0 0 337 - 289 + 258 @@ -34,11 +34,20 @@ QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + QAbstractItemView::SelectRows + + + false + + + false + - + 0 @@ -52,41 +61,48 @@ 0 - + false - - Add new attachment - - Add + New - + false - Remove selected attachment + Add new attachment - Remove + Add - + + + Qt::Vertical + + + + 20 + 40 + + + + + + false - - Rename selected attachment - - Rename + Preview @@ -129,6 +145,35 @@ + + + + false + + + Remove selected attachment + + + Remove + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 0 + + + + diff --git a/src/gui/entry/NewEntryAttachmentsDialog.cpp b/src/gui/entry/NewEntryAttachmentsDialog.cpp new file mode 100644 index 0000000000..b8da3b791f --- /dev/null +++ b/src/gui/entry/NewEntryAttachmentsDialog.cpp @@ -0,0 +1,92 @@ +/* + * 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->clear(); + m_ui->dialogButtons->addButton(QDialogButtonBox::Ok); + m_ui->dialogButtons->addButton(QDialogButtonBox::Cancel); + + connect(m_ui->dialogButtons, SIGNAL(accepted()), this, SLOT(saveAttachment())); + connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject())); + connect(m_ui->titleEdit, SIGNAL(textChanged(const QString&)), this, SLOT(fileNameTextChanged(const QString&))); + + fileNameTextChanged(m_ui->titleEdit->text()); +} + +NewEntryAttachmentsDialog::~NewEntryAttachmentsDialog() = default; + +bool NewEntryAttachmentsDialog::validateFileName(const QString& fileName, QString& error) const +{ + if (fileName.isEmpty()) { + error = tr("Attachment name cannot be empty"); + return false; + } + + if (m_attachments->hasKey(fileName)) { + error = tr("Attachment with the same name already exists"); + return false; + } + + return true; +} + +void NewEntryAttachmentsDialog::saveAttachment() +{ + auto fileName = m_ui->titleEdit->text(); + auto text = m_ui->attachmentTextEdit->toPlainText().toUtf8(); + + QString error; + if (validateFileName(fileName, error)) { + QMessageBox::warning(this, tr("Save attachment"), error); + return; + } + + m_attachments->set(fileName, text); + + accept(); +} + +void NewEntryAttachmentsDialog::fileNameTextChanged(const QString& fileName) +{ + QString error; + bool valid = validateFileName(fileName, error); + + m_ui->errorLabel->setText(error); + m_ui->errorLabel->setVisible(!valid); + + auto okButton = m_ui->dialogButtons->button(QDialogButtonBox::Ok); + if (okButton) { + okButton->setDisabled(!valid); + } +} diff --git a/src/gui/entry/NewEntryAttachmentsDialog.h b/src/gui/entry/NewEntryAttachmentsDialog.h new file mode 100644 index 0000000000..651100f65b --- /dev/null +++ b/src/gui/entry/NewEntryAttachmentsDialog.h @@ -0,0 +1,48 @@ +/* + * 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 Ui +{ + class EntryAttachmentsDialog; +} + +class QByteArray; +class EntryAttachments; + +class NewEntryAttachmentsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewEntryAttachmentsDialog(QPointer attachments, QWidget* parent = nullptr); + ~NewEntryAttachmentsDialog() override; + +private slots: + void saveAttachment(); + void fileNameTextChanged(const QString& fileName); + +private: + bool validateFileName(const QString& fileName, QString& error) const; + + QPointer m_attachments; + QScopedPointer m_ui; +}; diff --git a/src/gui/entry/PreviewEntryAttachmentsDialog.cpp b/src/gui/entry/PreviewEntryAttachmentsDialog.cpp new file mode 100644 index 0000000000..6926effbb4 --- /dev/null +++ b/src/gui/entry/PreviewEntryAttachmentsDialog.cpp @@ -0,0 +1,123 @@ +/* + * 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 "PreviewEntryAttachmentsDialog.h" +#include "ui_EntryAttachmentsDialog.h" + +#include +#include +#include +#include +#include + +PreviewEntryAttachmentsDialog::PreviewEntryAttachmentsDialog(QWidget* parent) + : QDialog(parent) + , m_ui(new Ui::EntryAttachmentsDialog) +{ + m_ui->setupUi(this); + + setWindowTitle(tr("Preview entry attachment")); + // Disable the help button in the title bar + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + // Set to read-only + m_ui->titleEdit->setReadOnly(true); + m_ui->attachmentTextEdit->setReadOnly(true); + m_ui->errorLabel->setVisible(false); + + // Initialize dialog buttons + m_ui->dialogButtons->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Open | QDialogButtonBox::Save); + auto closeButton = m_ui->dialogButtons->button(QDialogButtonBox::Close); + closeButton->setDefault(true); + + connect(m_ui->dialogButtons, SIGNAL(rejected()), this, SLOT(reject())); + connect(m_ui->dialogButtons, &QDialogButtonBox::clicked, [this](QAbstractButton* button) { + auto pressedButton = m_ui->dialogButtons->standardButton(button); + if (pressedButton == QDialogButtonBox::Open) { + emit openAttachment(m_name); + } else if (pressedButton == QDialogButtonBox::Save) { + emit saveAttachment(m_name); + } + }); +} + +PreviewEntryAttachmentsDialog::~PreviewEntryAttachmentsDialog() = default; + +void PreviewEntryAttachmentsDialog::setAttachment(const QString& name, const QByteArray& data) +{ + m_name = name; + m_ui->titleEdit->setText(m_name); + + m_type = attachmentType(data); + m_data = data; + + update(); +} + +void PreviewEntryAttachmentsDialog::update() +{ + if (m_type == Tools::MimeType::Unknown) { + updateTextAttachment(tr("No preview available").toUtf8()); + } else if (m_type == Tools::MimeType::Image) { + updateImageAttachment(m_data); + } else if (m_type == Tools::MimeType::PlainText) { + updateTextAttachment(m_data); + } +} + +void PreviewEntryAttachmentsDialog::updateTextAttachment(const QByteArray& data) +{ + m_ui->attachmentTextEdit->setPlainText(QString::fromUtf8(data)); +} + +void PreviewEntryAttachmentsDialog::updateImageAttachment(const QByteArray& data) +{ + QImage image{}; + if (!image.loadFromData(data)) { + updateTextAttachment(tr("Image format not supported").toUtf8()); + return; + } + + m_ui->attachmentTextEdit->clear(); + auto cursor = m_ui->attachmentTextEdit->textCursor(); + + // Scale the image to the contents rect minus another set of margins to avoid scrollbars + auto margins = m_ui->attachmentTextEdit->contentsMargins(); + auto size = m_ui->attachmentTextEdit->contentsRect().size(); + size.setWidth(size.width() - margins.left() - margins.right()); + size.setHeight(size.height() - margins.top() - margins.bottom()); + image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + + cursor.insertImage(image); +} + +Tools::MimeType PreviewEntryAttachmentsDialog::attachmentType(const QByteArray& data) const +{ + QMimeDatabase mimeDb{}; + const auto mime = mimeDb.mimeTypeForData(data); + + return Tools::toMimeType(mime.name()); +} + +void PreviewEntryAttachmentsDialog::resizeEvent(QResizeEvent* event) +{ + QDialog::resizeEvent(event); + + if (m_type == Tools::MimeType::Image) { + update(); + } +} diff --git a/src/gui/entry/PreviewEntryAttachmentsDialog.h b/src/gui/entry/PreviewEntryAttachmentsDialog.h new file mode 100644 index 0000000000..b01d1e7ddd --- /dev/null +++ b/src/gui/entry/PreviewEntryAttachmentsDialog.h @@ -0,0 +1,59 @@ +/* + * 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 "core/Tools.h" + +#include +#include + +namespace Ui +{ + class EntryAttachmentsDialog; +} + +class PreviewEntryAttachmentsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PreviewEntryAttachmentsDialog(QWidget* parent = nullptr); + ~PreviewEntryAttachmentsDialog() override; + + void setAttachment(const QString& name, const QByteArray& data); + +signals: + void openAttachment(const QString& name); + void saveAttachment(const QString& name); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private: + Tools::MimeType attachmentType(const QByteArray& data) const; + + void update(); + void updateTextAttachment(const QByteArray& data); + void updateImageAttachment(const QByteArray& data); + + QScopedPointer m_ui; + + QString m_name; + QByteArray m_data; + Tools::MimeType m_type{Tools::MimeType::Unknown}; +}; diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss index 8d40281a38..34cc283dd2 100644 --- a/src/gui/styles/base/basestyle.qss +++ b/src/gui/styles/base/basestyle.qss @@ -21,7 +21,9 @@ QCheckBox, QRadioButton { spacing: 10px; } -ReportsDialog QTableView::item { +ReportsDialog QTableView::item, +EntryAttachmentsWidget QTableView::item +{ padding: 4px; } @@ -30,8 +32,7 @@ DatabaseWidget, DatabaseWidget #groupView, DatabaseWidget #tagView { border: none; } -EntryPreviewWidget QLineEdit, EntryPreviewWidget QTextEdit, -EntryPreviewWidget TagsEdit +EntryPreviewWidget *[blendIn="true"] { background-color: palette(window); border: none; diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp index fd15128035..27a468929c 100644 --- a/tests/TestTools.cpp +++ b/tests/TestTools.cpp @@ -272,3 +272,70 @@ void TestTools::testArrayContainsValues() const auto result3 = Tools::getMissingValuesFromList(numberValues, QList({6, 7, 8})); QCOMPARE(result3.length(), 3); } + +void TestTools::testMimeTypes() +{ + const QStringList TextMimeTypes = { + "text/plain", // Plain text + "text/html", // HTML documents + "text/css", // CSS stylesheets + "text/javascript", // JavaScript files + "text/markdown", // Markdown documents + "text/xml", // XML documents + "text/rtf", // Rich Text Format + "text/vcard", // vCard files + "text/tab-separated-values", // Tab-separated values + "application/json", // JSON data + "application/xml", // XML data + "application/soap+xml", // SOAP messages + "application/x-yaml", // YAML data + "application/protobuf", // Protocol Buffers + }; + + const QStringList ImageMimeTypes = { + "image/jpeg", // JPEG images + "image/png", // PNG images + "image/gif", // GIF images + "image/bmp", // BMP images + "image/webp", // WEBP images + "image/svg+xml" // SVG images + }; + + const QStringList UnknownMimeTypes = { + "audio/mpeg", // MPEG audio files + "video/mp4", // MP4 video files + "application/pdf", // PDF documents + "application/zip", // ZIP archives + "application/x-tar", // TAR archives + "application/x-rar-compressed", // RAR archives + "application/x-7z-compressed", // 7z archives + "application/x-shockwave-flash", // Adobe Flash files + "application/vnd.ms-excel", // Microsoft Excel files + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // Microsoft Excel (OpenXML) files + "application/vnd.ms-powerpoint", // Microsoft PowerPoint files + "application/vnd.openxmlformats-officedocument.presentationml.presentation", // Microsoft PowerPoint (OpenXML) + // files + "application/msword", // Microsoft Word files + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // Microsoft Word (OpenXML) files + "application/vnd.oasis.opendocument.text", // OpenDocument Text + "application/vnd.oasis.opendocument.spreadsheet", // OpenDocument Spreadsheet + "application/vnd.oasis.opendocument.presentation", // OpenDocument Presentation + "application/x-httpd-php", // PHP files + "application/x-perl", // Perl scripts + "application/x-python", // Python scripts + "application/x-ruby", // Ruby scripts + "application/x-shellscript", // Shell scripts + }; + + for (const auto& mime : TextMimeTypes) { + QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::PlainText); + } + + for (const auto& mime : ImageMimeTypes) { + QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Image); + } + + for (const auto& mime : UnknownMimeTypes) { + QCOMPARE(Tools::toMimeType(mime), Tools::MimeType::Unknown); + } +} diff --git a/tests/TestTools.h b/tests/TestTools.h index e8a44b8b3c..5f4b6b6e09 100644 --- a/tests/TestTools.h +++ b/tests/TestTools.h @@ -37,6 +37,7 @@ private slots: void testConvertToRegex(); void testConvertToRegex_data(); void testArrayContainsValues(); + void testMimeTypes(); }; #endif // KEEPASSX_TESTTOOLS_H