From 71afee1893225abeecab85040d3b19bafe0b56fa Mon Sep 17 00:00:00 2001 From: Eric Lemesre Date: Thu, 6 Aug 2026 19:31:25 +0200 Subject: [PATCH] Add pinned attributes to entry preview panel Allow pinning additional attributes (custom strings) so they show on the entry preview panel below the password field. Pinned attributes are stored per entry in CustomData under the KPXC_PINNED_ATTRIBUTES key as a compact JSON array, leaving the KDBX schema untouched. * Add a "Pin" checkbox next to "Protect" in the entry edit Advanced tab * Show pinned attributes on the preview General tab; protected values are masked with a reveal toggle, double click copies the value * Keep the pinned list in sync on attribute rename and removal; pinned names that no longer exist are silently ignored * Add TestEntry::testPinnedAttributes, extend TestGui::testEditEntry (pin, preview display, masking, rename and removal sync) * Document the feature in the user guide Co-Authored-By: Claude Fable 5 --- docs/topics/DatabaseOperations.adoc | 2 + share/translations/keepassxc_en.ts | 12 ++++ src/core/CustomData.cpp | 1 + src/core/CustomData.h | 1 + src/core/Entry.cpp | 51 ++++++++++++++ src/core/Entry.h | 4 ++ src/gui/EntryPreviewWidget.cpp | 88 ++++++++++++++++++++++++ src/gui/EntryPreviewWidget.h | 2 + src/gui/EntryPreviewWidget.ui | 37 ++++++++-- src/gui/entry/EditEntryWidget.cpp | 45 +++++++++++- src/gui/entry/EditEntryWidget.h | 2 + src/gui/entry/EditEntryWidgetAdvanced.ui | 20 ++++++ tests/TestEntry.cpp | 39 +++++++++++ tests/TestEntry.h | 1 + tests/gui/TestGui.cpp | 60 ++++++++++++++++ 15 files changed, 359 insertions(+), 6 deletions(-) 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 ac5863d8ac..fa3fe53429 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 d35f5c8cd9..de2cd52d78 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 be140e91f0..8db36e39d8 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -144,6 +144,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 b2af308d5a..8cc01d02f6 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" @@ -107,6 +108,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 c74512281c..ad4eb21930 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -32,6 +32,7 @@ private slots: void testHistoryItemDeletion(); void testHistoryItemCustomData(); 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));