Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/topics/DatabaseOperations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]

Expand Down
12 changes: 12 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3233,6 +3233,18 @@ Would you like to correct it?</source>
<source>Background color selection</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle attribute display on the entry preview panel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show this attribute on the entry preview panel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pin</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditEntryWidgetAutoType</name>
Expand Down
1 change: 1 addition & 0 deletions src/core/CustomData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{};
Expand Down
1 change: 1 addition & 0 deletions src/core/CustomData.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include "core/Totp.h"

#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QRegularExpression>
#include <QStringBuilder>
#include <QStringView>
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions src/core/Entry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
88 changes: 88 additions & 0 deletions src/gui/EntryPreviewWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
#include "keeshare/KeeShare.h"
#include "keeshare/KeeShareSettings.h"

#include <QHBoxLayout>
#include <QLabel>
#include <QScrollBar>
#include <QTabWidget>
#include <QToolButton>
namespace
{
constexpr int GeneralTabIndex = 0;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/gui/EntryPreviewWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ private slots:
void updateEntryHeaderLine();
void updateEntryTotp();
void updateEntryGeneralTab();
void updateEntryPinnedAttributes();
void updateEntryAdvancedTab();
void updateEntryAutotypeTab();
void setUsernameVisible(bool state);
Expand All @@ -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);

Expand Down
37 changes: 32 additions & 5 deletions src/gui/EntryPreviewWidget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QWidget" name="entryGeneralWidget" native="true">
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,1" columnstretch="0,1,0,0,0,1">
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,1" columnstretch="0,1,0,0,0,1">
<property name="leftMargin">
<number>0</number>
</property>
Expand Down Expand Up @@ -392,7 +392,7 @@
</property>
</widget>
</item>
<item row="3" column="1" colspan="5">
<item row="4" column="1" colspan="5">
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,0">
<property name="spacing">
<number>6</number>
Expand Down Expand Up @@ -440,7 +440,7 @@
</item>
</layout>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="entryTagsTitleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
Expand Down Expand Up @@ -516,7 +516,7 @@
</item>
</layout>
</item>
<item row="2" column="1" colspan="5">
<item row="3" column="1" colspan="5">
<widget class="TagsEdit" name="entryTagsList" native="true">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
Expand Down Expand Up @@ -592,7 +592,7 @@
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="entryNotesTitleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
Expand Down Expand Up @@ -636,6 +636,33 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="6">
<widget class="QWidget" name="entryPinnedAttributesWidget" native="true">
<layout class="QFormLayout" name="entryPinnedAttributesLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>6</number>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</layout>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="entryUrlTitleLabel">
<property name="sizePolicy">
Expand Down
Loading
Loading