mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-04-07 06:27:39 +03:00
Autocomplete usernames based on most frequent in database
* Fixes #3126 * Limit autocompletion to the top ten used usernames - Load common usernames when database is opened - Transition from QLineEdit to QComboBox for usernames - Dropdown menu of the combobox lets user choose a common username - Common usernames are autocompleted via inline completion - Common usernames are sorted by frequency (first) and name (second)
This commit is contained in:
parent
a22e8a1f40
commit
f85642741d
13 changed files with 134 additions and 10 deletions
|
@ -179,7 +179,6 @@ void Config::init(const QString& fileName)
|
|||
m_defaults.insert("SearchLimitGroup", false);
|
||||
m_defaults.insert("MinimizeOnCopy", false);
|
||||
m_defaults.insert("MinimizeOnOpenUrl", false);
|
||||
m_defaults.insert("UseGroupIconOnEntryCreation", false);
|
||||
m_defaults.insert("AutoTypeEntryTitleMatch", true);
|
||||
m_defaults.insert("AutoTypeEntryURLMatch", true);
|
||||
m_defaults.insert("AutoTypeDelay", 25);
|
||||
|
|
|
@ -54,6 +54,7 @@ Database::Database()
|
|||
|
||||
connect(m_metadata, SIGNAL(metadataModified()), this, SLOT(markAsModified()));
|
||||
connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified()));
|
||||
connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames()));
|
||||
|
||||
m_modified = false;
|
||||
m_emitModified = true;
|
||||
|
@ -149,6 +150,8 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey>
|
|||
setFilePath(filePath);
|
||||
dbFile.close();
|
||||
|
||||
updateCommonUsernames();
|
||||
|
||||
setInitialized(ok);
|
||||
markAsClean();
|
||||
|
||||
|
@ -525,6 +528,17 @@ void Database::addDeletedObject(const QUuid& uuid)
|
|||
addDeletedObject(delObj);
|
||||
}
|
||||
|
||||
QList<QString> Database::commonUsernames()
|
||||
{
|
||||
return m_commonUsernames;
|
||||
}
|
||||
|
||||
void Database::updateCommonUsernames(int topN)
|
||||
{
|
||||
m_commonUsernames.clear();
|
||||
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
|
||||
}
|
||||
|
||||
const QUuid& Database::cipher() const
|
||||
{
|
||||
return m_data.cipher;
|
||||
|
|
|
@ -106,6 +106,8 @@ public:
|
|||
bool containsDeletedObject(const DeletedObject& uuid) const;
|
||||
void setDeletedObjects(const QList<DeletedObject>& delObjs);
|
||||
|
||||
QList<QString> commonUsernames();
|
||||
|
||||
bool hasKey() const;
|
||||
QSharedPointer<const CompositeKey> key() const;
|
||||
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
||||
|
@ -131,6 +133,7 @@ public:
|
|||
public slots:
|
||||
void markAsModified();
|
||||
void markAsClean();
|
||||
void updateCommonUsernames(int topN = 10);
|
||||
|
||||
signals:
|
||||
void filePathChanged(const QString& oldPath, const QString& newPath);
|
||||
|
@ -184,6 +187,8 @@ private:
|
|||
bool m_modified = false;
|
||||
bool m_emitModified;
|
||||
|
||||
QList<QString> m_commonUsernames;
|
||||
|
||||
QUuid m_uuid;
|
||||
static QHash<QUuid, QPointer<Database>> s_uuidMap;
|
||||
static QHash<QString, QPointer<Database>> s_filePathMap;
|
||||
|
|
|
@ -341,6 +341,11 @@ bool Entry::isExpired() const
|
|||
return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < Clock::currentDateTimeUtc();
|
||||
}
|
||||
|
||||
bool Entry::isAttributeReference(const QString& key) const
|
||||
{
|
||||
return m_attributes->isReference(key);
|
||||
}
|
||||
|
||||
bool Entry::isAttributeReferenceOf(const QString& key, const QUuid& uuid) const
|
||||
{
|
||||
if (!m_attributes->isReference(key)) {
|
||||
|
|
|
@ -111,6 +111,7 @@ public:
|
|||
|
||||
bool hasTotp() const;
|
||||
bool isExpired() const;
|
||||
bool isAttributeReference(const QString& key) const;
|
||||
bool isAttributeReferenceOf(const QString& key, const QUuid& uuid) const;
|
||||
void replaceReferencesWithValues(const Entry* other);
|
||||
bool hasReferences() const;
|
||||
|
|
|
@ -813,6 +813,42 @@ QSet<QUuid> Group::customIconsRecursive() const
|
|||
return result;
|
||||
}
|
||||
|
||||
QList<QString> Group::usernamesRecursive(int topN) const
|
||||
{
|
||||
// Collect all usernames and sort for easy counting
|
||||
QHash<QString, int> countedUsernames;
|
||||
for (const auto* entry : entriesRecursive()) {
|
||||
const auto username = entry->username();
|
||||
if (!username.isEmpty() && !entry->isAttributeReference(EntryAttributes::UserNameKey)) {
|
||||
countedUsernames.insert(username, ++countedUsernames[username]);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort username/frequency pairs by frequency and name
|
||||
QList<QPair<QString, int>> sortedUsernames;
|
||||
for (const auto& key : countedUsernames.keys()) {
|
||||
sortedUsernames.append({key, countedUsernames[key]});
|
||||
}
|
||||
|
||||
auto comparator = [](const QPair<QString, int>& arg1, const QPair<QString, int>& arg2) {
|
||||
if (arg1.second == arg2.second) {
|
||||
return arg1.first < arg2.first;
|
||||
}
|
||||
return arg1.second > arg2.second;
|
||||
};
|
||||
|
||||
std::sort(sortedUsernames.begin(), sortedUsernames.end(), comparator);
|
||||
|
||||
// Take first topN usernames if set
|
||||
QList<QString> usernames;
|
||||
int actualUsernames = topN < 0 ? sortedUsernames.size() : std::min(topN, sortedUsernames.size());
|
||||
for (int i = 0; i < actualUsernames; i++) {
|
||||
usernames.append(sortedUsernames[i].first);
|
||||
}
|
||||
|
||||
return usernames;
|
||||
}
|
||||
|
||||
Group* Group::findGroupByUuid(const QUuid& uuid)
|
||||
{
|
||||
if (uuid.isNull()) {
|
||||
|
|
|
@ -158,6 +158,7 @@ public:
|
|||
QList<const Group*> groupsRecursive(bool includeSelf) const;
|
||||
QList<Group*> groupsRecursive(bool includeSelf);
|
||||
QSet<QUuid> customIconsRecursive() const;
|
||||
QList<QString> usernamesRecursive(int topN = -1) const;
|
||||
|
||||
Group* clone(Entry::CloneFlags entryFlags = DefaultEntryCloneFlags,
|
||||
CloneFlags groupFlags = DefaultCloneFlags) const;
|
||||
|
|
|
@ -85,6 +85,8 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
|
|||
, m_autoTypeAssocModel(new AutoTypeAssociationsModel(this))
|
||||
, m_autoTypeDefaultSequenceGroup(new QButtonGroup(this))
|
||||
, m_autoTypeWindowSequenceGroup(new QButtonGroup(this))
|
||||
, m_usernameCompleter(new QCompleter(this))
|
||||
, m_usernameCompleterModel(new QStringListModel(this))
|
||||
{
|
||||
setupMain();
|
||||
setupAdvanced();
|
||||
|
@ -129,6 +131,12 @@ void EditEntryWidget::setupMain()
|
|||
m_mainUi->setupUi(m_mainWidget);
|
||||
addPage(tr("Entry"), FilePath::instance()->icon("actions", "document-edit"), m_mainWidget);
|
||||
|
||||
m_mainUi->usernameComboBox->setEditable(true);
|
||||
m_usernameCompleter->setCompletionMode(QCompleter::InlineCompletion);
|
||||
m_usernameCompleter->setCaseSensitivity(Qt::CaseSensitive);
|
||||
m_usernameCompleter->setModel(m_usernameCompleterModel);
|
||||
m_mainUi->usernameComboBox->setCompleter(m_usernameCompleter);
|
||||
|
||||
m_mainUi->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
|
||||
m_mainUi->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator"));
|
||||
#ifdef WITH_XC_NETWORKING
|
||||
|
@ -273,7 +281,7 @@ void EditEntryWidget::setupEntryUpdate()
|
|||
{
|
||||
// Entry tab
|
||||
connect(m_mainUi->titleEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
connect(m_mainUi->usernameEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
connect(m_mainUi->usernameComboBox->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
connect(m_mainUi->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
connect(m_mainUi->passwordRepeatEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
|
||||
|
@ -707,7 +715,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
|||
m_customData->copyDataFrom(entry->customData());
|
||||
|
||||
m_mainUi->titleEdit->setReadOnly(m_history);
|
||||
m_mainUi->usernameEdit->setReadOnly(m_history);
|
||||
m_mainUi->usernameComboBox->lineEdit()->setReadOnly(m_history);
|
||||
m_mainUi->urlEdit->setReadOnly(m_history);
|
||||
m_mainUi->passwordEdit->setReadOnly(m_history);
|
||||
m_mainUi->passwordRepeatEdit->setReadOnly(m_history);
|
||||
|
@ -742,7 +750,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
|||
m_historyWidget->setEnabled(!m_history);
|
||||
|
||||
m_mainUi->titleEdit->setText(entry->title());
|
||||
m_mainUi->usernameEdit->setText(entry->username());
|
||||
m_mainUi->usernameComboBox->lineEdit()->setText(entry->username());
|
||||
m_mainUi->urlEdit->setText(entry->url());
|
||||
m_mainUi->passwordEdit->setText(entry->password());
|
||||
m_mainUi->passwordRepeatEdit->setText(entry->password());
|
||||
|
@ -751,6 +759,13 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
|||
m_mainUi->expirePresets->setEnabled(!m_history);
|
||||
m_mainUi->togglePasswordButton->setChecked(config()->get("security/passwordscleartext").toBool());
|
||||
|
||||
QList<QString> commonUsernames = m_db->commonUsernames();
|
||||
m_usernameCompleterModel->setStringList(commonUsernames);
|
||||
QString usernameToRestore = m_mainUi->usernameComboBox->lineEdit()->text();
|
||||
m_mainUi->usernameComboBox->clear();
|
||||
m_mainUi->usernameComboBox->addItems(commonUsernames);
|
||||
m_mainUi->usernameComboBox->lineEdit()->setText(usernameToRestore);
|
||||
|
||||
m_mainUi->notesEdit->setPlainText(entry->notes());
|
||||
|
||||
m_advancedUi->attachmentsWidget->setEntryAttachments(entry->attachments());
|
||||
|
@ -910,7 +925,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
|
|||
entry->attachments()->copyDataFrom(m_advancedUi->attachmentsWidget->entryAttachments());
|
||||
entry->customData()->copyDataFrom(m_customData.data());
|
||||
entry->setTitle(m_mainUi->titleEdit->text().replace(newLineRegex, " "));
|
||||
entry->setUsername(m_mainUi->usernameEdit->text().replace(newLineRegex, " "));
|
||||
entry->setUsername(m_mainUi->usernameComboBox->lineEdit()->text().replace(newLineRegex, " "));
|
||||
entry->setUrl(m_mainUi->urlEdit->text().replace(newLineRegex, " "));
|
||||
entry->setPassword(m_mainUi->passwordEdit->text());
|
||||
entry->setExpires(m_mainUi->expireCheck->isChecked());
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#define KEEPASSX_EDITENTRYWIDGET_H
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QCompleter>
|
||||
#include <QModelIndex>
|
||||
#include <QPointer>
|
||||
#include <QScopedPointer>
|
||||
|
@ -175,6 +176,8 @@ private:
|
|||
AutoTypeAssociationsModel* const m_autoTypeAssocModel;
|
||||
QButtonGroup* const m_autoTypeDefaultSequenceGroup;
|
||||
QButtonGroup* const m_autoTypeWindowSequenceGroup;
|
||||
QCompleter* const m_usernameCompleter;
|
||||
QStringListModel* const m_usernameCompleterModel;
|
||||
|
||||
Q_DISABLE_COPY(EditEntryWidget)
|
||||
};
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
<widget class="QLineEdit" name="titleEdit"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="usernameEdit"/>
|
||||
<widget class="QComboBox" name="usernameComboBox"/>
|
||||
</item>
|
||||
<item row="7" column="0" alignment="Qt::AlignRight">
|
||||
<widget class="QCheckBox" name="expireCheck">
|
||||
|
@ -191,7 +191,7 @@
|
|||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>titleEdit</tabstop>
|
||||
<tabstop>usernameEdit</tabstop>
|
||||
<tabstop>usernameComboBox</tabstop>
|
||||
<tabstop>passwordEdit</tabstop>
|
||||
<tabstop>passwordRepeatEdit</tabstop>
|
||||
<tabstop>togglePasswordButton</tabstop>
|
||||
|
|
|
@ -1181,3 +1181,29 @@ void TestGroup::testApplyGroupIconRecursively()
|
|||
QVERIFY(subsubgroup->iconNumber() == iconForGroups);
|
||||
QVERIFY(subsubgroupEntry->iconNumber() == iconForEntries);
|
||||
}
|
||||
|
||||
void TestGroup::testUsernamesRecursive()
|
||||
{
|
||||
Database* database = new Database();
|
||||
|
||||
// Create a subgroup
|
||||
Group* subgroup = new Group();
|
||||
subgroup->setName("Subgroup");
|
||||
subgroup->setParent(database->rootGroup());
|
||||
|
||||
// Generate entries in the root group and the subgroup
|
||||
Entry* rootGroupEntry = database->rootGroup()->addEntryWithPath("Root group entry");
|
||||
rootGroupEntry->setUsername("Name1");
|
||||
|
||||
Entry* subgroupEntry = subgroup->addEntryWithPath("Subgroup entry");
|
||||
subgroupEntry->setUsername("Name2");
|
||||
|
||||
Entry* subgroupEntryReusingUsername = subgroup->addEntryWithPath("Another subgroup entry");
|
||||
subgroupEntryReusingUsername->setUsername("Name2");
|
||||
|
||||
QList<QString> usernames = database->rootGroup()->usernamesRecursive();
|
||||
QCOMPARE(usernames.size(), 2);
|
||||
QVERIFY(usernames.contains("Name1"));
|
||||
QVERIFY(usernames.contains("Name2"));
|
||||
QVERIFY(usernames.indexOf("Name2") < usernames.indexOf("Name1"));
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ private slots:
|
|||
void testChildrenSort();
|
||||
void testHierarchy();
|
||||
void testApplyGroupIconRecursively();
|
||||
void testUsernamesRecursive();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTGROUP_H
|
||||
|
|
|
@ -507,7 +507,7 @@ void TestGui::testEditEntry()
|
|||
QVERIFY(okButton);
|
||||
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
|
||||
titleEdit->setText("multiline\ntitle");
|
||||
editEntryWidget->findChild<QLineEdit*>("usernameEdit")->setText("multiline\nusername");
|
||||
editEntryWidget->findChild<QComboBox*>("usernameComboBox")->lineEdit()->setText("multiline\nusername");
|
||||
editEntryWidget->findChild<QLineEdit*>("passwordEdit")->setText("multiline\npassword");
|
||||
editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit")->setText("multiline\npassword");
|
||||
editEntryWidget->findChild<QLineEdit*>("urlEdit")->setText("multiline\nurl");
|
||||
|
@ -594,6 +594,10 @@ void TestGui::testAddEntry()
|
|||
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
|
||||
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
|
||||
QTest::keyClicks(titleEdit, "test");
|
||||
auto* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
|
||||
QVERIFY(usernameComboBox);
|
||||
QTest::mouseClick(usernameComboBox, Qt::LeftButton);
|
||||
QTest::keyClicks(usernameComboBox, "AutocompletionUsername");
|
||||
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
|
||||
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
|
||||
|
||||
|
@ -602,17 +606,31 @@ void TestGui::testAddEntry()
|
|||
Entry* entry = entryView->entryFromIndex(item);
|
||||
|
||||
QCOMPARE(entry->title(), QString("test"));
|
||||
QCOMPARE(entry->username(), QString("AutocompletionUsername"));
|
||||
QCOMPARE(entry->historyItems().size(), 0);
|
||||
|
||||
m_db->updateCommonUsernames();
|
||||
|
||||
// Add entry "something 2"
|
||||
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
|
||||
QTest::keyClicks(titleEdit, "something 2");
|
||||
QTest::mouseClick(usernameComboBox, Qt::LeftButton);
|
||||
QTest::keyClicks(usernameComboBox, "Auto");
|
||||
QTest::keyPress(usernameComboBox, Qt::Key_Right);
|
||||
auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit");
|
||||
auto* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit");
|
||||
QTest::keyClicks(passwordEdit, "something 2");
|
||||
QTest::keyClicks(passwordRepeatEdit, "something 2");
|
||||
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
|
||||
|
||||
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode);
|
||||
item = entryView->model()->index(1, 1);
|
||||
entry = entryView->entryFromIndex(item);
|
||||
|
||||
QCOMPARE(entry->title(), QString("something 2"));
|
||||
QCOMPARE(entry->username(), QString("AutocompletionUsername"));
|
||||
QCOMPARE(entry->historyItems().size(), 0);
|
||||
|
||||
// Add entry "something 5" but click cancel button (does NOT add entry)
|
||||
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
|
||||
QTest::keyClicks(titleEdit, "something 5");
|
||||
|
@ -1063,8 +1081,8 @@ void TestGui::testEntryPlaceholders()
|
|||
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
|
||||
auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
|
||||
QTest::keyClicks(titleEdit, "test");
|
||||
QLineEdit* usernameEdit = editEntryWidget->findChild<QLineEdit*>("usernameEdit");
|
||||
QTest::keyClicks(usernameEdit, "john");
|
||||
QComboBox* usernameComboBox = editEntryWidget->findChild<QComboBox*>("usernameComboBox");
|
||||
QTest::keyClicks(usernameComboBox, "john");
|
||||
QLineEdit* urlEdit = editEntryWidget->findChild<QLineEdit*>("urlEdit");
|
||||
QTest::keyClicks(urlEdit, "{TITLE}.{USERNAME}");
|
||||
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue