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));