diff --git a/docs/topics/DatabaseOperations.adoc b/docs/topics/DatabaseOperations.adoc
index 5f315cd459..26f7d80a83 100644
--- a/docs/topics/DatabaseOperations.adoc
+++ b/docs/topics/DatabaseOperations.adoc
@@ -214,6 +214,8 @@ A lot of applications and web sites now require providing additional information
To protect an attribute from being displayed by default, activate the _Protect_ checkbox *(A)*. To show the contents of the attribute while keeping it protected, press the _Reveal_ button *(B)*.
+To display an attribute on the entry preview panel below the password field, activate the _Pin_ checkbox. Protected attributes remain masked on the preview panel until revealed with the eye button; double click a value to copy it to the clipboard.
+
.Additional attributes example
image::edit_entry_attributes.png[]
diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 039fd83f9e..b490ee5296 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -3233,6 +3233,18 @@ Would you like to correct it?
Background color selection
+
+ Toggle attribute display on the entry preview panel
+
+
+
+ Show this attribute on the entry preview panel
+
+
+
+ Pin
+
+
EditEntryWidgetAutoType
diff --git a/src/core/CustomData.cpp b/src/core/CustomData.cpp
index 5376150abf..d85d51bc4b 100644
--- a/src/core/CustomData.cpp
+++ b/src/core/CustomData.cpp
@@ -27,6 +27,7 @@ const QString CustomData::ExcludeFromReportsLegacy = QStringLiteral("KnownBad");
const QString CustomData::FdoSecretsExposedGroup = QStringLiteral("FDO_SECRETS_EXPOSED_GROUP");
const QString CustomData::RandomSlug = QStringLiteral("KPXC_RANDOM_SLUG");
const QString CustomData::RemoteProgramSettings = QStringLiteral("KPXC_REMOTE_SYNC_SETTINGS");
+const QString CustomData::PinnedAttributes = QStringLiteral("KPXC_PINNED_ATTRIBUTES");
// Fallback item for return by reference
static const CustomData::CustomDataItem NULL_ITEM{};
diff --git a/src/core/CustomData.h b/src/core/CustomData.h
index 3ee4d05efe..73a2590c43 100644
--- a/src/core/CustomData.h
+++ b/src/core/CustomData.h
@@ -73,6 +73,7 @@ class CustomData : public ModifiableObject
static const QString FdoSecretsExposedGroup;
static const QString RandomSlug;
static const QString RemoteProgramSettings;
+ static const QString PinnedAttributes;
// Pre-KDBX 4.1
static const QString ExcludeFromReportsLegacy;
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index 34c96d0d59..82abd62a09 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -28,6 +28,8 @@
#include "core/Totp.h"
#include
+#include
+#include
#include
#include
#include
@@ -574,6 +576,55 @@ const CustomData* Entry::customData() const
return m_customData;
}
+QStringList Entry::pinnedAttributes() const
+{
+ return pinnedAttributes(m_customData);
+}
+
+// The pinned attribute list is stored in the entry CustomData under
+// CustomData::PinnedAttributes as a compact JSON array of attribute names,
+// e.g. ["Attr1","Attr2"]. Names may contain any character; unknown or
+// malformed content is ignored so other KDBX clients cannot break parsing.
+QStringList Entry::pinnedAttributes(const CustomData* customData)
+{
+ if (!customData || !customData->contains(CustomData::PinnedAttributes)) {
+ return {};
+ }
+
+ const auto doc = QJsonDocument::fromJson(customData->value(CustomData::PinnedAttributes).toUtf8());
+ if (!doc.isArray()) {
+ return {};
+ }
+
+ QStringList names;
+ for (const auto& value : doc.array()) {
+ if (value.isString() && !value.toString().isEmpty()) {
+ names << value.toString();
+ }
+ }
+ return names;
+}
+
+void Entry::setPinnedAttributes(CustomData* customData, const QStringList& names)
+{
+ if (!customData) {
+ return;
+ }
+
+ QStringList filtered = names;
+ filtered.removeAll(QString());
+ filtered.removeDuplicates();
+ if (filtered.isEmpty()) {
+ if (customData->contains(CustomData::PinnedAttributes)) {
+ customData->remove(CustomData::PinnedAttributes);
+ }
+ return;
+ }
+
+ const auto json = QJsonDocument(QJsonArray::fromStringList(filtered)).toJson(QJsonDocument::Compact);
+ customData->set(CustomData::PinnedAttributes, QString::fromUtf8(json));
+}
+
bool Entry::hasTotp() const
{
return !m_data.totpSettings.isNull();
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 4874f59376..9b66a06c28 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -143,6 +143,10 @@ class Entry : public ModifiableObject
CustomData* customData();
const CustomData* customData() const;
+ QStringList pinnedAttributes() const;
+ static QStringList pinnedAttributes(const CustomData* customData);
+ static void setPinnedAttributes(CustomData* customData, const QStringList& names);
+
void setUuid(const QUuid& uuid);
void setIcon(int iconNumber);
void setIcon(const QUuid& uuid);
diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp
index f587d4fa44..b937c010ce 100644
--- a/src/gui/EntryPreviewWidget.cpp
+++ b/src/gui/EntryPreviewWidget.cpp
@@ -28,8 +28,11 @@
#include "keeshare/KeeShare.h"
#include "keeshare/KeeShareSettings.h"
+#include
+#include
#include
#include
+#include
namespace
{
constexpr int GeneralTabIndex = 0;
@@ -116,6 +119,13 @@ bool EntryPreviewWidget::eventFilter(QObject* object, QEvent* event)
m_ui->entryTotpLabel->clearFocus();
return true;
}
+ } else if (event->type() == QEvent::MouseButtonDblClick) {
+ // Pinned attribute values expose their clear text through this property
+ const auto clearValue = object->property("clearValue");
+ if (clearValue.isValid()) {
+ emit copyTextRequested(clearValue.toString());
+ return true;
+ }
}
return QWidget::eventFilter(object, event);
}
@@ -401,6 +411,84 @@ void EntryPreviewWidget::updateEntryGeneralTab()
m_ui->entryExpirationLabel->setText(expires);
m_ui->entryTagsList->tags(m_currentEntry->tagList());
m_ui->entryTagsList->setReadOnly(true);
+
+ updateEntryPinnedAttributes();
+}
+
+void EntryPreviewWidget::updateEntryPinnedAttributes()
+{
+ Q_ASSERT(m_currentEntry);
+
+ auto layout = m_ui->entryPinnedAttributesLayout;
+ while (layout->count() > 0) {
+ auto item = layout->takeAt(0);
+ delete item->widget();
+ delete item;
+ }
+
+ const EntryAttributes* attributes = m_currentEntry->attributes();
+ const QStringList customKeys = attributes->customKeys();
+ QStringList pinned;
+ for (const QString& name : m_currentEntry->pinnedAttributes()) {
+ // Silently ignore pinned names that no longer exist as custom attributes
+ if (customKeys.contains(name)) {
+ pinned << name;
+ }
+ }
+
+ m_ui->entryPinnedAttributesWidget->setVisible(!pinned.isEmpty());
+
+ QFont font;
+ font.setBold(true);
+ for (const QString& name : pinned) {
+ auto titleLabel = new QLabel(name, m_ui->entryPinnedAttributesWidget);
+ titleLabel->setTextFormat(Qt::PlainText);
+ titleLabel->setFont(font);
+ const auto value = m_currentEntry->resolveMultiplePlaceholders(attributes->value(name));
+ layout->addRow(titleLabel, createPinnedValueWidget(value, attributes->isProtected(name)));
+ }
+}
+
+QWidget* EntryPreviewWidget::createPinnedValueWidget(const QString& clearValue, bool protect)
+{
+ auto container = new QWidget(m_ui->entryPinnedAttributesWidget);
+ auto layout = new QHBoxLayout(container);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(8);
+
+ auto valueLabel = new QLabel(container);
+ valueLabel->setTextFormat(Qt::PlainText);
+ valueLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ valueLabel->setProperty("clearValue", clearValue);
+ valueLabel->setToolTip(tr("Double click to copy value"));
+ valueLabel->installEventFilter(this);
+
+ if (protect) {
+ valueLabel->setText(QString("\u25cf").repeated(6));
+ valueLabel->setFont(Font::fixedFont());
+
+ auto button = new QToolButton(container);
+ button->setCheckable(true);
+ button->setChecked(false);
+ button->setIcon(icons()->onOffIcon("password-show", false));
+ button->setIconSize({12, 12});
+ connect(button, &QToolButton::clicked, this, [valueLabel, button](bool state) {
+ button->setIcon(icons()->onOffIcon("password-show", state));
+ if (state) {
+ valueLabel->setText(valueLabel->property("clearValue").toString());
+ valueLabel->setFont(Font::defaultFont());
+ } else {
+ valueLabel->setText(QString("\u25cf").repeated(6));
+ valueLabel->setFont(Font::fixedFont());
+ }
+ });
+ layout->addWidget(button);
+ } else {
+ valueLabel->setText(clearValue);
+ }
+
+ layout->addWidget(valueLabel, 1);
+ return container;
}
void EntryPreviewWidget::updateEntryAdvancedTab()
diff --git a/src/gui/EntryPreviewWidget.h b/src/gui/EntryPreviewWidget.h
index 3ead573b93..415b8dcfbf 100644
--- a/src/gui/EntryPreviewWidget.h
+++ b/src/gui/EntryPreviewWidget.h
@@ -55,6 +55,7 @@ private slots:
void updateEntryHeaderLine();
void updateEntryTotp();
void updateEntryGeneralTab();
+ void updateEntryPinnedAttributes();
void updateEntryAdvancedTab();
void updateEntryAutotypeTab();
void setUsernameVisible(bool state);
@@ -73,6 +74,7 @@ private slots:
private:
void setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled);
+ QWidget* createPinnedValueWidget(const QString& clearValue, bool protect);
static QString hierarchy(const Group* group, const QString& title);
diff --git a/src/gui/EntryPreviewWidget.ui b/src/gui/EntryPreviewWidget.ui
index b6d4cadcb0..003ce9e7a7 100644
--- a/src/gui/EntryPreviewWidget.ui
+++ b/src/gui/EntryPreviewWidget.ui
@@ -219,7 +219,7 @@
-
-
+
0
@@ -392,7 +392,7 @@
- -
+
-
6
@@ -440,7 +440,7 @@
- -
+
-
@@ -516,7 +516,7 @@
- -
+
-
Qt::ClickFocus
@@ -592,7 +592,7 @@
- -
+
-
@@ -636,6 +636,33 @@
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 8
+
+
+ 6
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
-
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 5270a3e1e1..ed8e82bee3 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -257,6 +257,8 @@ void EditEntryWidget::setupAdvanced()
connect(m_advancedUi->editAttributeButton, SIGNAL(clicked()), SLOT(editCurrentAttribute()));
connect(m_advancedUi->removeAttributeButton, SIGNAL(clicked()), SLOT(removeCurrentAttribute()));
connect(m_advancedUi->protectAttributeButton, SIGNAL(toggled(bool)), SLOT(protectCurrentAttribute(bool)));
+ connect(m_advancedUi->pinAttributeButton, SIGNAL(toggled(bool)), SLOT(pinCurrentAttribute(bool)));
+ connect(m_entryAttributes, SIGNAL(renamed(QString,QString)), SLOT(updatePinnedAttributeRename(QString,QString)));
connect(m_advancedUi->revealAttributeButton, SIGNAL(clicked(bool)), SLOT(toggleCurrentAttributeVisibility()));
connect(m_advancedUi->attributesView->selectionModel(),
SIGNAL(currentChanged(QModelIndex,QModelIndex)),
@@ -523,6 +525,7 @@ void EditEntryWidget::setupEntryUpdate()
// Advanced tab
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->pinAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->excludeReportsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
@@ -1471,7 +1474,12 @@ void EditEntryWidget::removeCurrentAttribute()
MessageBox::Cancel);
if (result == MessageBox::Remove) {
- m_entryAttributes->remove(m_attributesModel->keyByIndex(index));
+ QString key = m_attributesModel->keyByIndex(index);
+ m_entryAttributes->remove(key);
+ auto pinned = Entry::pinnedAttributes(m_customData.data());
+ if (pinned.removeAll(key) > 0) {
+ Entry::setPinnedAttributes(m_customData.data(), pinned);
+ }
setModified(true);
}
}
@@ -1500,11 +1508,14 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
{
// Block signals to prevent modified being set
m_advancedUi->protectAttributeButton->blockSignals(true);
+ m_advancedUi->pinAttributeButton->blockSignals(true);
m_advancedUi->attributesEdit->blockSignals(true);
m_advancedUi->revealAttributeButton->setText(tr("Reveal"));
if (index.isValid()) {
QString key = m_attributesModel->keyByIndex(index);
+ m_advancedUi->pinAttributeButton->setChecked(Entry::pinnedAttributes(m_customData.data()).contains(key));
+ m_advancedUi->pinAttributeButton->setEnabled(!m_history);
if (showProtected) {
m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press Reveal to view or edit"));
m_advancedUi->attributesEdit->setEnabled(false);
@@ -1527,11 +1538,14 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
m_advancedUi->revealAttributeButton->setEnabled(false);
m_advancedUi->protectAttributeButton->setChecked(false);
m_advancedUi->protectAttributeButton->setEnabled(false);
+ m_advancedUi->pinAttributeButton->setChecked(false);
+ m_advancedUi->pinAttributeButton->setEnabled(false);
m_advancedUi->editAttributeButton->setEnabled(false);
m_advancedUi->removeAttributeButton->setEnabled(false);
}
m_advancedUi->protectAttributeButton->blockSignals(false);
+ m_advancedUi->pinAttributeButton->blockSignals(false);
m_advancedUi->attributesEdit->blockSignals(false);
}
@@ -1553,6 +1567,35 @@ void EditEntryWidget::protectCurrentAttribute(bool state)
}
}
+void EditEntryWidget::pinCurrentAttribute(bool state)
+{
+ QModelIndex index = m_advancedUi->attributesView->currentIndex();
+ if (!m_history && index.isValid()) {
+ QString key = m_attributesModel->keyByIndex(index);
+ auto pinned = Entry::pinnedAttributes(m_customData.data());
+ if (state && !pinned.contains(key)) {
+ pinned.append(key);
+ } else if (!state) {
+ pinned.removeAll(key);
+ }
+ Entry::setPinnedAttributes(m_customData.data(), pinned);
+ }
+}
+
+void EditEntryWidget::updatePinnedAttributeRename(const QString& oldKey, const QString& newKey)
+{
+ auto pinned = Entry::pinnedAttributes(m_customData.data());
+ if (pinned.contains(oldKey)) {
+ for (auto& name : pinned) {
+ if (name == oldKey) {
+ name = newKey;
+ }
+ }
+ Entry::setPinnedAttributes(m_customData.data(), pinned);
+ setModified(true);
+ }
+}
+
void EditEntryWidget::toggleCurrentAttributeVisibility()
{
if (!m_advancedUi->attributesEdit->isEnabled()) {
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index fef2d1cfc6..0851924d54 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -106,6 +106,8 @@ private slots:
void removeCurrentAttribute();
void updateCurrentAttribute();
void protectCurrentAttribute(bool state);
+ void pinCurrentAttribute(bool state);
+ void updatePinnedAttributeRename(const QString& oldKey, const QString& newKey);
void toggleCurrentAttributeVisibility();
void updateAutoTypeEnabled();
void openAutotypeHelp();
diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui
index 044145226d..18ebb59acd 100644
--- a/src/gui/entry/EditEntryWidgetAdvanced.ui
+++ b/src/gui/entry/EditEntryWidgetAdvanced.ui
@@ -146,6 +146,25 @@
+ -
+
+
+ false
+
+
+ Toggle attribute display on the entry preview panel
+
+
+ Show this attribute on the entry preview panel
+
+
+ margin-left:50%;margin-right:50%
+
+
+ Pin
+
+
+
-
@@ -319,6 +338,7 @@
removeAttributeButton
editAttributeButton
protectAttributeButton
+ pinAttributeButton
revealAttributeButton
excludeReportsCheckBox
fgColorCheckBox
diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp
index bd729dc8d0..155f1cbeba 100644
--- a/tests/TestEntry.cpp
+++ b/tests/TestEntry.cpp
@@ -20,6 +20,7 @@
#include "TestEntry.h"
#include "core/Clock.h"
+#include "core/CustomData.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/TimeInfo.h"
@@ -83,6 +84,44 @@ void TestEntry::testCopyDataFrom()
QCOMPARE(entry2->autoTypeAssociations()->get(1).window, QString("3"));
}
+void TestEntry::testPinnedAttributes()
+{
+ QScopedPointer entry(new Entry());
+
+ // No key set yet
+ QVERIFY(entry->pinnedAttributes().isEmpty());
+
+ // Round-trip with names containing JSON separators, quotes and newlines
+ const QStringList names = {"Simple", "With,Comma", "With\"Quote", "With\nNewline", "Unicode é●"};
+ Entry::setPinnedAttributes(entry->customData(), names);
+ QVERIFY(entry->customData()->contains(CustomData::PinnedAttributes));
+ QCOMPARE(entry->pinnedAttributes(), names);
+ QCOMPARE(Entry::pinnedAttributes(entry->customData()), names);
+
+ // Empty names are filtered out
+ Entry::setPinnedAttributes(entry->customData(), {"", "Kept"});
+ QCOMPARE(entry->pinnedAttributes(), QStringList{"Kept"});
+
+ // Duplicates are removed while preserving order
+ Entry::setPinnedAttributes(entry->customData(), {"A", "B", "A"});
+ QCOMPARE(entry->pinnedAttributes(), (QStringList{"A", "B"}));
+
+ // Empty list removes the key entirely
+ Entry::setPinnedAttributes(entry->customData(), {});
+ QVERIFY(!entry->customData()->contains(CustomData::PinnedAttributes));
+ QVERIFY(entry->pinnedAttributes().isEmpty());
+
+ // Malformed JSON yields an empty list without crashing
+ entry->customData()->set(CustomData::PinnedAttributes, "not json at all");
+ QVERIFY(entry->pinnedAttributes().isEmpty());
+ entry->customData()->set(CustomData::PinnedAttributes, "{\"an\":\"object\"}");
+ QVERIFY(entry->pinnedAttributes().isEmpty());
+
+ // Null CustomData pointers are handled gracefully
+ QVERIFY(Entry::pinnedAttributes(nullptr).isEmpty());
+ Entry::setPinnedAttributes(nullptr, {"NoCrash"});
+}
+
void TestEntry::testClone()
{
QScopedPointer entryOrg(new Entry());
diff --git a/tests/TestEntry.h b/tests/TestEntry.h
index 953a7ce7b0..d86037dc68 100644
--- a/tests/TestEntry.h
+++ b/tests/TestEntry.h
@@ -30,6 +30,7 @@ private slots:
void initTestCase();
void testHistoryItemDeletion();
void testCopyDataFrom();
+ void testPinnedAttributes();
void testClone();
void testResolveUrl();
void testResolveUrlPlaceholders();
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index 2d6ee1d3fd..1454d8eeb4 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -22,6 +22,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -34,6 +35,7 @@
#include
#include
#include
+#include
#include "config-keepassx-tests.h"
#include "core/PasswordHealth.h"
@@ -729,6 +731,14 @@ void TestGui::testEditEntry()
QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED"));
QTest::mouseClick(editEntryWidget->findChild("revealAttributeButton"), Qt::LeftButton);
QCOMPARE(attrTextEdit->toPlainText(), attrText);
+
+ // Test pinning the attribute to the preview panel
+ auto* pinAttributeCheck = editEntryWidget->findChild("pinAttributeButton");
+ QVERIFY(pinAttributeCheck);
+ QVERIFY(pinAttributeCheck->isEnabled());
+ QCOMPARE(pinAttributeCheck->isChecked(), false);
+ QTest::mouseClick(pinAttributeCheck, Qt::LeftButton);
+ QVERIFY(pinAttributeCheck->isChecked());
editEntryWidget->switchToPage(EditEntryWidget::Page::Main);
// Save the edit (press OK)
@@ -744,6 +754,56 @@ void TestGui::testEditEntry()
QCOMPARE(entryItem.data(Qt::BackgroundRole), QVariant(bgColor));
QCOMPARE(entry->historyItems().size(), ++editCount);
+ // Confirm the pinned attribute is stored and shown on the preview panel
+ QCOMPARE(entry->pinnedAttributes(), QStringList{"New attribute"});
+ auto* previewWidget = m_dbWidget->findChild("previewWidget");
+ QVERIFY(previewWidget);
+ auto* pinnedWidget = previewWidget->findChild("entryPinnedAttributesWidget");
+ QVERIFY(pinnedWidget);
+ QVERIFY(pinnedWidget->isVisible());
+ QLabel* pinnedValueLabel = nullptr;
+ for (auto* label : pinnedWidget->findChildren()) {
+ if (label->property("clearValue").isValid()) {
+ pinnedValueLabel = label;
+ break;
+ }
+ }
+ QVERIFY(pinnedValueLabel);
+ QCOMPARE(pinnedValueLabel->property("clearValue").toString(), attrText);
+ // Protected attribute is masked until the reveal button is toggled
+ QCOMPARE(pinnedValueLabel->text(), QString("\u25cf").repeated(6));
+ auto* pinnedRevealButton = pinnedWidget->findChild();
+ QVERIFY(pinnedRevealButton);
+ QTest::mouseClick(pinnedRevealButton, Qt::LeftButton);
+ QCOMPARE(pinnedValueLabel->text(), attrText);
+
+ // Renaming a pinned attribute keeps it pinned under the new name
+ QTest::mouseClick(entryEditWidget, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
+ okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
+ QVERIFY(okButton);
+ editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
+ auto* attrListView = editEntryWidget->findChild("attributesView");
+ QVERIFY(attrListView->currentIndex().isValid());
+ QVERIFY(attrListView->model()->setData(attrListView->currentIndex(), "Renamed attribute", Qt::EditRole));
+ QTest::mouseClick(okButton, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+ QCOMPARE(entry->pinnedAttributes(), QStringList{"Renamed attribute"});
+
+ // Removing the attribute unpins it and hides the preview section
+ QTest::mouseClick(entryEditWidget, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode);
+ okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
+ QVERIFY(okButton);
+ editEntryWidget->switchToPage(EditEntryWidget::Page::Advanced);
+ QVERIFY(attrListView->currentIndex().isValid());
+ MessageBox::setNextAnswer(MessageBox::Remove);
+ QTest::mouseClick(editEntryWidget->findChild("removeAttributeButton"), Qt::LeftButton);
+ QTest::mouseClick(okButton, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
+ QVERIFY(entry->pinnedAttributes().isEmpty());
+ QVERIFY(!pinnedWidget->isVisible());
+
// Confirm modified indicator is showing
QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName));