mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-14 00:39:53 -05:00
Enhance Tags / Saved Searches
* Rename "Database Tags" to "Searches and Tags" * Separate searching for all entries and resetting the search * Support selecting multiple tags to search against * Fix using escaped quotes in search terms * Make tag searching more precise * Support `is:expired-#` to search for entries expiring within # days. Exclude recycled entries from expired search. * Don't list tags from entries that are recycled * Force hide tag auto-completion menu when tag editing widget is hidden. On rare occasions the focus out signal is not called when the tag view is hidden (entry edit is closed), this resolves that problem. * Remove spaces from before and after tags to prevent seemingly duplicate tags from being created. * Also fix some awkward signal/slot dances that were setup over time with the entry view and preview widget. Allow changing tags for multiple entries through context menu * Closes #8277 - show context menu with currently available tags in database and checks those that are set on one or more selected entries. When a tag is selected it is either set or unset on all entries depending on its checked state. * Add ability to save searches and recall them from the "Searches and Tags" view * Add ability to remove a tag from all entries from the "Searches and Tags" view * Cleanup tag handling and widgets
This commit is contained in:
parent
5b923aee1a
commit
ee55143c4a
@ -2249,10 +2249,6 @@ This is definitely a bug, please report it to the developers.</source>
|
|||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>DatabaseWidget</name>
|
<name>DatabaseWidget</name>
|
||||||
<message>
|
|
||||||
<source>Database Tags</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Searching…</source>
|
<source>Searching…</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
@ -2417,6 +2413,22 @@ Disable safe saves and try again?</source>
|
|||||||
<numerusform></numerusform>
|
<numerusform></numerusform>
|
||||||
</translation>
|
</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Searches and Tags</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Enter a unique name or overwrite an existing search from the list:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Save</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Save Search</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>EditEntryWidget</name>
|
<name>EditEntryWidget</name>
|
||||||
@ -5403,6 +5415,21 @@ We recommend you use the AppImage available on our downloads page.</source>
|
|||||||
<source>You must restart the application to apply this setting. Would you like to restart now?</source>
|
<source>You must restart the application to apply this setting. Would you like to restart now?</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Tags</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>No Tags</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message numerus="yes">
|
||||||
|
<source>%1 Entry(s)</source>
|
||||||
|
<translation type="unfinished">
|
||||||
|
<numerusform></numerusform>
|
||||||
|
<numerusform></numerusform>
|
||||||
|
</translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>ManageDatabase</name>
|
<name>ManageDatabase</name>
|
||||||
@ -8372,6 +8399,10 @@ Kernel: %3 %4</source>
|
|||||||
<source>Limit search to selected group</source>
|
<source>Limit search to selected group</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Save Search</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>SettingsClientModel</name>
|
<name>SettingsClientModel</name>
|
||||||
@ -8584,10 +8615,6 @@ Kernel: %3 %4</source>
|
|||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>TagModel</name>
|
<name>TagModel</name>
|
||||||
<message>
|
|
||||||
<source>All</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Expired</source>
|
<source>Expired</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
@ -8596,6 +8623,33 @@ Kernel: %3 %4</source>
|
|||||||
<source>Weak Passwords</source>
|
<source>Weak Passwords</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>All Entries</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Clear Search</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>TagView</name>
|
||||||
|
<message>
|
||||||
|
<source>Remove Search</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Remove Tag</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Confirm Remove Tag</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Remove tag "%1" from all entries in this database?</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>TotpDialog</name>
|
<name>TotpDialog</name>
|
||||||
|
@ -150,6 +150,7 @@ set(keepassx_SOURCES
|
|||||||
gui/group/GroupModel.cpp
|
gui/group/GroupModel.cpp
|
||||||
gui/group/GroupView.cpp
|
gui/group/GroupView.cpp
|
||||||
gui/tag/TagModel.cpp
|
gui/tag/TagModel.cpp
|
||||||
|
gui/tag/TagView.cpp
|
||||||
gui/tag/TagsEdit.cpp
|
gui/tag/TagsEdit.cpp
|
||||||
gui/databasekey/KeyComponentWidget.cpp
|
gui/databasekey/KeyComponentWidget.cpp
|
||||||
gui/databasekey/PasswordEditWidget.cpp
|
gui/databasekey/PasswordEditWidget.cpp
|
||||||
|
@ -701,8 +701,8 @@ void Database::updateTagList()
|
|||||||
// Search groups recursively looking for tags
|
// Search groups recursively looking for tags
|
||||||
// Use a set to prevent adding duplicates
|
// Use a set to prevent adding duplicates
|
||||||
QSet<QString> tagSet;
|
QSet<QString> tagSet;
|
||||||
for (const auto group : m_rootGroup->groupsRecursive(true)) {
|
for (auto entry : m_rootGroup->entriesRecursive()) {
|
||||||
for (const auto entry : group->entries()) {
|
if (!entry->isRecycled()) {
|
||||||
for (auto tag : entry->tagList()) {
|
for (auto tag : entry->tagList()) {
|
||||||
tagSet.insert(tag);
|
tagSet.insert(tag);
|
||||||
}
|
}
|
||||||
@ -714,6 +714,17 @@ void Database::updateTagList()
|
|||||||
emit tagListUpdated();
|
emit tagListUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Database::removeTag(const QString& tag)
|
||||||
|
{
|
||||||
|
if (!m_rootGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto entry : m_rootGroup->entriesRecursive()) {
|
||||||
|
entry->removeTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const QUuid& Database::cipher() const
|
const QUuid& Database::cipher() const
|
||||||
{
|
{
|
||||||
return m_data.cipher;
|
return m_data.cipher;
|
||||||
|
@ -129,6 +129,7 @@ public:
|
|||||||
|
|
||||||
const QStringList& commonUsernames() const;
|
const QStringList& commonUsernames() const;
|
||||||
const QStringList& tagList() const;
|
const QStringList& tagList() const;
|
||||||
|
void removeTag(const QString& tag);
|
||||||
|
|
||||||
QSharedPointer<const CompositeKey> key() const;
|
QSharedPointer<const CompositeKey> key() const;
|
||||||
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
||||||
|
@ -187,15 +187,12 @@ QString Entry::overrideUrl() const
|
|||||||
|
|
||||||
QString Entry::tags() const
|
QString Entry::tags() const
|
||||||
{
|
{
|
||||||
return m_data.tags;
|
return m_data.tags.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList Entry::tagList() const
|
QStringList Entry::tagList() const
|
||||||
{
|
{
|
||||||
static QRegExp rx("(\\,|\\t|\\;)");
|
return m_data.tags;
|
||||||
auto taglist = tags().split(rx, QString::SkipEmptyParts);
|
|
||||||
std::sort(taglist.begin(), taglist.end());
|
|
||||||
return taglist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimeInfo& Entry::timeInfo() const
|
const TimeInfo& Entry::timeInfo() const
|
||||||
@ -654,7 +651,42 @@ void Entry::setOverrideUrl(const QString& url)
|
|||||||
|
|
||||||
void Entry::setTags(const QString& tags)
|
void Entry::setTags(const QString& tags)
|
||||||
{
|
{
|
||||||
set(m_data.tags, tags);
|
static QRegExp rx("(\\,|\\t|\\;)");
|
||||||
|
auto taglist = tags.split(rx, QString::SkipEmptyParts);
|
||||||
|
// Trim whitespace before/after tag text
|
||||||
|
for (auto itr = taglist.begin(); itr != taglist.end(); ++itr) {
|
||||||
|
*itr = itr->trimmed();
|
||||||
|
}
|
||||||
|
// Remove duplicates
|
||||||
|
auto tagSet = QSet<QString>::fromList(taglist);
|
||||||
|
taglist = tagSet.toList();
|
||||||
|
// Sort alphabetically
|
||||||
|
taglist.sort();
|
||||||
|
set(m_data.tags, taglist);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entry::addTag(const QString& tag)
|
||||||
|
{
|
||||||
|
auto cleanTag = tag.trimmed();
|
||||||
|
cleanTag.remove(QRegExp("(\\,|\\t|\\;)"));
|
||||||
|
|
||||||
|
auto taglist = m_data.tags;
|
||||||
|
if (!taglist.contains(cleanTag)) {
|
||||||
|
taglist.append(cleanTag);
|
||||||
|
taglist.sort();
|
||||||
|
set(m_data.tags, taglist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entry::removeTag(const QString& tag)
|
||||||
|
{
|
||||||
|
auto cleanTag = tag.trimmed();
|
||||||
|
cleanTag.remove(QRegExp("(\\,|\\t|\\;)"));
|
||||||
|
|
||||||
|
auto taglist = m_data.tags;
|
||||||
|
if (taglist.removeAll(tag) > 0) {
|
||||||
|
set(m_data.tags, taglist);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entry::setTimeInfo(const TimeInfo& timeInfo)
|
void Entry::setTimeInfo(const TimeInfo& timeInfo)
|
||||||
|
@ -58,7 +58,7 @@ struct EntryData
|
|||||||
QString foregroundColor;
|
QString foregroundColor;
|
||||||
QString backgroundColor;
|
QString backgroundColor;
|
||||||
QString overrideUrl;
|
QString overrideUrl;
|
||||||
QString tags;
|
QStringList tags;
|
||||||
bool autoTypeEnabled;
|
bool autoTypeEnabled;
|
||||||
int autoTypeObfuscation;
|
int autoTypeObfuscation;
|
||||||
QString defaultAutoTypeSequence;
|
QString defaultAutoTypeSequence;
|
||||||
@ -158,6 +158,9 @@ public:
|
|||||||
void setPreviousParentGroup(const Group* group);
|
void setPreviousParentGroup(const Group* group);
|
||||||
void setPreviousParentGroupUuid(const QUuid& uuid);
|
void setPreviousParentGroupUuid(const QUuid& uuid);
|
||||||
|
|
||||||
|
void addTag(const QString& tag);
|
||||||
|
void removeTag(const QString& tag);
|
||||||
|
|
||||||
QList<Entry*> historyItems();
|
QList<Entry*> historyItems();
|
||||||
const QList<Entry*>& historyItems() const;
|
const QList<Entry*>& historyItems() const;
|
||||||
void addHistoryItem(Entry* entry);
|
void addHistoryItem(Entry* entry);
|
||||||
|
@ -25,8 +25,6 @@
|
|||||||
EntrySearcher::EntrySearcher(bool caseSensitive, bool skipProtected)
|
EntrySearcher::EntrySearcher(bool caseSensitive, bool skipProtected)
|
||||||
: m_caseSensitive(caseSensitive)
|
: m_caseSensitive(caseSensitive)
|
||||||
, m_skipProtected(skipProtected)
|
, m_skipProtected(skipProtected)
|
||||||
, m_termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re")
|
|
||||||
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,11 +195,16 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Field::Tag:
|
case Field::Tag:
|
||||||
found = term.regex.match(entry->tags()).hasMatch();
|
found = entry->tagList().indexOf(term.regex) != -1;
|
||||||
break;
|
break;
|
||||||
case Field::Is:
|
case Field::Is:
|
||||||
if (term.word.compare("expired", Qt::CaseInsensitive) == 0) {
|
if (term.word.startsWith("expired", Qt::CaseInsensitive)) {
|
||||||
found = entry->isExpired();
|
auto days = 0;
|
||||||
|
auto parts = term.word.split("-", QString::SkipEmptyParts);
|
||||||
|
if (parts.length() >= 2) {
|
||||||
|
days = parts[1].toInt();
|
||||||
|
}
|
||||||
|
found = entry->willExpireInDays(days) && !entry->isRecycled();
|
||||||
break;
|
break;
|
||||||
} else if (term.word.compare("weak", Qt::CaseInsensitive) == 0) {
|
} else if (term.word.compare("weak", Qt::CaseInsensitive) == 0) {
|
||||||
if (!entry->excludeFromReports() && !entry->password().isEmpty() && !entry->isExpired()) {
|
if (!entry->excludeFromReports() && !entry->password().isEmpty() && !entry->isExpired()) {
|
||||||
@ -220,8 +223,7 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
|||||||
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|
found = term.regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
|
||||||
|| term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|
|| term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch()
|
||||||
|| term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|
|| term.regex.match(entry->resolvePlaceholder(entry->url())).hasMatch()
|
||||||
|| term.regex.match(entry->resolvePlaceholder(entry->tags())).hasMatch()
|
|| entry->tagList().indexOf(term.regex) != -1 || term.regex.match(entry->notes()).hasMatch();
|
||||||
|| term.regex.match(entry->notes()).hasMatch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// negate the result if exclude:
|
// negate the result if exclude:
|
||||||
@ -246,23 +248,26 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
|
|||||||
{QStringLiteral("notes"), Field::Notes},
|
{QStringLiteral("notes"), Field::Notes},
|
||||||
{QStringLiteral("pw"), Field::Password},
|
{QStringLiteral("pw"), Field::Password},
|
||||||
{QStringLiteral("password"), Field::Password},
|
{QStringLiteral("password"), Field::Password},
|
||||||
{QStringLiteral("title"), Field::Title},
|
{QStringLiteral("title"), Field::Title}, // title before tag to capture t:<word>
|
||||||
{QStringLiteral("t"), Field::Title},
|
{QStringLiteral("username"), Field::Username}, // username before url to capture u:<word>
|
||||||
{QStringLiteral("u"), Field::Username}, // u: stands for username rather than url
|
|
||||||
{QStringLiteral("url"), Field::Url},
|
{QStringLiteral("url"), Field::Url},
|
||||||
{QStringLiteral("username"), Field::Username},
|
|
||||||
{QStringLiteral("group"), Field::Group},
|
{QStringLiteral("group"), Field::Group},
|
||||||
{QStringLiteral("tag"), Field::Tag},
|
{QStringLiteral("tag"), Field::Tag},
|
||||||
{QStringLiteral("is"), Field::Is}};
|
{QStringLiteral("is"), Field::Is}};
|
||||||
|
|
||||||
|
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
|
||||||
|
static QRegularExpression termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re");
|
||||||
|
|
||||||
m_searchTerms.clear();
|
m_searchTerms.clear();
|
||||||
auto results = m_termParser.globalMatch(searchString);
|
auto results = termParser.globalMatch(searchString);
|
||||||
while (results.hasNext()) {
|
while (results.hasNext()) {
|
||||||
auto result = results.next();
|
auto result = results.next();
|
||||||
SearchTerm term{};
|
SearchTerm term{};
|
||||||
|
|
||||||
// Quoted string group
|
// Quoted string group
|
||||||
term.word = result.captured(3);
|
term.word = result.captured(3);
|
||||||
|
// Unescape quotes
|
||||||
|
term.word.replace("\\\"", "\"");
|
||||||
|
|
||||||
// If empty, use the unquoted string group
|
// If empty, use the unquoted string group
|
||||||
if (term.word.isEmpty()) {
|
if (term.word.isEmpty()) {
|
||||||
|
@ -71,7 +71,6 @@ private:
|
|||||||
|
|
||||||
bool m_caseSensitive;
|
bool m_caseSensitive;
|
||||||
bool m_skipProtected;
|
bool m_skipProtected;
|
||||||
QRegularExpression m_termParser;
|
|
||||||
QList<SearchTerm> m_searchTerms;
|
QList<SearchTerm> m_searchTerms;
|
||||||
|
|
||||||
friend class TestEntrySearcher;
|
friend class TestEntrySearcher;
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
const int Metadata::DefaultHistoryMaxItems = 10;
|
const int Metadata::DefaultHistoryMaxItems = 10;
|
||||||
const int Metadata::DefaultHistoryMaxSize = 6 * 1024 * 1024;
|
const int Metadata::DefaultHistoryMaxSize = 6 * 1024 * 1024;
|
||||||
@ -487,3 +488,26 @@ void Metadata::setSettingsChanged(const QDateTime& value)
|
|||||||
Q_ASSERT(value.timeSpec() == Qt::UTC);
|
Q_ASSERT(value.timeSpec() == Qt::UTC);
|
||||||
m_settingsChanged = value;
|
m_settingsChanged = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Metadata::addSavedSearch(const QString& name, const QString& searchtext)
|
||||||
|
{
|
||||||
|
auto searches = savedSearches();
|
||||||
|
searches.insert(name, searchtext);
|
||||||
|
auto json = QJsonDocument::fromVariant(searches);
|
||||||
|
m_customData->set("KPXC_SavedSearch", json.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Metadata::deleteSavedSearch(const QString& name)
|
||||||
|
{
|
||||||
|
auto searches = savedSearches();
|
||||||
|
searches.remove(name);
|
||||||
|
auto json = QJsonDocument::fromVariant(searches);
|
||||||
|
m_customData->set("KPXC_SavedSearch", json.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Metadata::savedSearches()
|
||||||
|
{
|
||||||
|
auto searches = m_customData->value("KPXC_SavedSearch");
|
||||||
|
auto json = QJsonDocument::fromJson(searches.toUtf8());
|
||||||
|
return json.toVariant().toMap();
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
#include "core/CustomData.h"
|
#include "core/CustomData.h"
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
@ -150,6 +151,9 @@ public:
|
|||||||
void setHistoryMaxItems(int value);
|
void setHistoryMaxItems(int value);
|
||||||
void setHistoryMaxSize(int value);
|
void setHistoryMaxSize(int value);
|
||||||
void setUpdateDatetime(bool value);
|
void setUpdateDatetime(bool value);
|
||||||
|
void addSavedSearch(const QString& name, const QString& searchtext);
|
||||||
|
void deleteSavedSearch(const QString& name);
|
||||||
|
QVariantMap savedSearches();
|
||||||
/*
|
/*
|
||||||
* Copy all attributes from other except:
|
* Copy all attributes from other except:
|
||||||
* - Group pointers/uuids
|
* - Group pointers/uuids
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QPlainTextEdit>
|
#include <QPlainTextEdit>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
@ -50,7 +51,7 @@
|
|||||||
#include "gui/group/EditGroupWidget.h"
|
#include "gui/group/EditGroupWidget.h"
|
||||||
#include "gui/group/GroupView.h"
|
#include "gui/group/GroupView.h"
|
||||||
#include "gui/reports/ReportsDialog.h"
|
#include "gui/reports/ReportsDialog.h"
|
||||||
#include "gui/tag/TagModel.h"
|
#include "gui/tag/TagView.h"
|
||||||
#include "keeshare/KeeShare.h"
|
#include "keeshare/KeeShare.h"
|
||||||
|
|
||||||
#ifdef WITH_XC_NETWORKING
|
#ifdef WITH_XC_NETWORKING
|
||||||
@ -82,7 +83,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
|
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
|
||||||
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
|
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
|
||||||
, m_groupView(new GroupView(m_db.data(), this))
|
, m_groupView(new GroupView(m_db.data(), this))
|
||||||
, m_tagView(new QListView(this))
|
, m_tagView(new TagView(this))
|
||||||
, m_saveAttempts(0)
|
, m_saveAttempts(0)
|
||||||
, m_entrySearcher(new EntrySearcher(false))
|
, m_entrySearcher(new EntrySearcher(false))
|
||||||
{
|
{
|
||||||
@ -97,20 +98,15 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
hbox->addWidget(m_mainSplitter);
|
hbox->addWidget(m_mainSplitter);
|
||||||
m_mainWidget->setLayout(mainLayout);
|
m_mainWidget->setLayout(mainLayout);
|
||||||
|
|
||||||
// Setup tags view and place under groups
|
// Setup searches and tags view and place under groups
|
||||||
auto tagModel = new TagModel(m_db);
|
|
||||||
m_tagView->setObjectName("tagView");
|
m_tagView->setObjectName("tagView");
|
||||||
m_tagView->setModel(tagModel);
|
m_tagView->setDatabase(m_db);
|
||||||
m_tagView->setFrameStyle(QFrame::NoFrame);
|
connect(m_tagView, SIGNAL(activated(QModelIndex)), this, SLOT(filterByTag()));
|
||||||
m_tagView->setSelectionMode(QListView::SingleSelection);
|
connect(m_tagView, SIGNAL(clicked(QModelIndex)), this, SLOT(filterByTag()));
|
||||||
m_tagView->setSelectionBehavior(QListView::SelectRows);
|
|
||||||
m_tagView->setCurrentIndex(tagModel->index(0));
|
|
||||||
connect(m_tagView, SIGNAL(activated(QModelIndex)), this, SLOT(filterByTag(QModelIndex)));
|
|
||||||
connect(m_tagView, SIGNAL(clicked(QModelIndex)), this, SLOT(filterByTag(QModelIndex)));
|
|
||||||
|
|
||||||
auto tagsWidget = new QWidget();
|
auto tagsWidget = new QWidget();
|
||||||
auto tagsLayout = new QVBoxLayout();
|
auto tagsLayout = new QVBoxLayout();
|
||||||
auto tagsTitle = new QLabel(tr("Database Tags"));
|
auto tagsTitle = new QLabel(tr("Searches and Tags"));
|
||||||
tagsTitle->setProperty("title", true);
|
tagsTitle->setProperty("title", true);
|
||||||
tagsWidget->setObjectName("tagWidget");
|
tagsWidget->setObjectName("tagWidget");
|
||||||
tagsWidget->setLayout(tagsLayout);
|
tagsWidget->setLayout(tagsLayout);
|
||||||
@ -206,13 +202,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
|
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
|
||||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
|
connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
|
||||||
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
|
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
|
||||||
connect(m_entryView, &EntryView::entrySelectionChanged, this, [this](Entry * currentEntry) {
|
|
||||||
if (currentEntry) {
|
|
||||||
m_previewView->setEntry(currentEntry);
|
|
||||||
} else {
|
|
||||||
m_previewView->setGroup(groupView()->currentGroup());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
|
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
|
||||||
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
|
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
|
||||||
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
|
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
|
||||||
@ -431,8 +420,7 @@ void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
|
|||||||
m_db = std::move(db);
|
m_db = std::move(db);
|
||||||
connectDatabaseSignals();
|
connectDatabaseSignals();
|
||||||
m_groupView->changeDatabase(m_db);
|
m_groupView->changeDatabase(m_db);
|
||||||
auto tagModel = new TagModel(m_db);
|
m_tagView->setDatabase(m_db);
|
||||||
m_tagView->setModel(tagModel);
|
|
||||||
|
|
||||||
// Restore the new parent group pointer, if not found default to the root group
|
// Restore the new parent group pointer, if not found default to the root group
|
||||||
// this prevents data loss when merging a database while creating a new entry
|
// this prevents data loss when merging a database while creating a new entry
|
||||||
@ -690,11 +678,23 @@ void DatabaseWidget::copyAttribute(QAction* action)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::filterByTag(const QModelIndex& index)
|
void DatabaseWidget::filterByTag()
|
||||||
{
|
{
|
||||||
m_tagView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select);
|
QStringList searchTerms;
|
||||||
const auto model = static_cast<TagModel*>(m_tagView->model());
|
const auto selections = m_tagView->selectionModel()->selectedIndexes();
|
||||||
emit requestSearch(model->data(index, Qt::UserRole).toString());
|
for (const auto& index : selections) {
|
||||||
|
searchTerms << index.data(Qt::UserRole).toString();
|
||||||
|
}
|
||||||
|
emit requestSearch(searchTerms.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseWidget::setTag(QAction* action)
|
||||||
|
{
|
||||||
|
auto tag = action->text();
|
||||||
|
auto state = action->isChecked();
|
||||||
|
for (auto entry : m_entryView->selectedEntries()) {
|
||||||
|
state ? entry->addTag(tag) : entry->removeTag(tag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::showTotpKeyQrCode()
|
void DatabaseWidget::showTotpKeyQrCode()
|
||||||
@ -1128,22 +1128,13 @@ void DatabaseWidget::loadDatabase(bool accepted)
|
|||||||
// Only show expired entries if first unlock and option is enabled
|
// Only show expired entries if first unlock and option is enabled
|
||||||
if (m_groupBeforeLock.isNull() && config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock).toBool()) {
|
if (m_groupBeforeLock.isNull() && config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock).toBool()) {
|
||||||
int expirationOffset = config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays).toInt();
|
int expirationOffset = config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays).toInt();
|
||||||
QList<Entry*> expiredEntries;
|
requestSearch(QString("is:expired-%1").arg(expirationOffset));
|
||||||
for (auto entry : m_db->rootGroup()->entriesRecursive()) {
|
QTimer::singleShot(150, this, [=] {
|
||||||
if (entry->willExpireInDays(expirationOffset) && !entry->excludeFromReports() && !entry->isRecycled()) {
|
|
||||||
expiredEntries << entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expiredEntries.isEmpty()) {
|
|
||||||
m_entryView->displaySearch(expiredEntries);
|
|
||||||
m_entryView->setFirstEntryActive();
|
|
||||||
m_searchingLabel->setText(
|
m_searchingLabel->setText(
|
||||||
expirationOffset == 0
|
expirationOffset == 0
|
||||||
? tr("Expired entries")
|
? tr("Expired entries")
|
||||||
: tr("Entries expiring within %1 day(s)", "", expirationOffset).arg(expirationOffset));
|
: tr("Entries expiring within %1 day(s)", "", expirationOffset).arg(expirationOffset));
|
||||||
m_searchingLabel->setVisible(true);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_groupBeforeLock = QUuid();
|
m_groupBeforeLock = QUuid();
|
||||||
@ -1449,6 +1440,40 @@ void DatabaseWidget::search(const QString& searchtext)
|
|||||||
emit searchModeActivated();
|
emit searchModeActivated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DatabaseWidget::saveSearch(const QString& searchtext)
|
||||||
|
{
|
||||||
|
if (!m_db->isInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the existing searches and prepend an empty string to allow
|
||||||
|
// the user to input a new search name without seeing the first one
|
||||||
|
QStringList searches(m_db->metadata()->savedSearches().keys());
|
||||||
|
searches.prepend("");
|
||||||
|
|
||||||
|
QInputDialog dialog(this);
|
||||||
|
connect(this, &DatabaseWidget::databaseLockRequested, &dialog, &QInputDialog::reject);
|
||||||
|
|
||||||
|
dialog.setComboBoxEditable(true);
|
||||||
|
dialog.setComboBoxItems(searches);
|
||||||
|
dialog.setOkButtonText(tr("Save"));
|
||||||
|
dialog.setLabelText(tr("Enter a unique name or overwrite an existing search from the list:"));
|
||||||
|
dialog.setWindowTitle(tr("Save Search"));
|
||||||
|
dialog.exec();
|
||||||
|
|
||||||
|
auto name = dialog.textValue();
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
m_db->metadata()->addSavedSearch(name, searchtext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DatabaseWidget::deleteSearch(const QString& name)
|
||||||
|
{
|
||||||
|
if (m_db->isInitialized()) {
|
||||||
|
m_db->metadata()->deleteSavedSearch(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
||||||
{
|
{
|
||||||
m_entrySearcher->setCaseSensitive(state);
|
m_entrySearcher->setCaseSensitive(state);
|
||||||
@ -1539,6 +1564,8 @@ void DatabaseWidget::onEntryChanged(Entry* entry)
|
|||||||
{
|
{
|
||||||
if (entry) {
|
if (entry) {
|
||||||
m_previewView->setEntry(entry);
|
m_previewView->setEntry(entry);
|
||||||
|
} else {
|
||||||
|
m_previewView->setGroup(groupView()->currentGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit entrySelectionChanged();
|
emit entrySelectionChanged();
|
||||||
|
@ -49,6 +49,7 @@ class QSplitter;
|
|||||||
class QLabel;
|
class QLabel;
|
||||||
class MessageWidget;
|
class MessageWidget;
|
||||||
class EntryPreviewWidget;
|
class EntryPreviewWidget;
|
||||||
|
class TagView;
|
||||||
|
|
||||||
namespace Ui
|
namespace Ui
|
||||||
{
|
{
|
||||||
@ -175,7 +176,8 @@ public slots:
|
|||||||
void copyURL();
|
void copyURL();
|
||||||
void copyNotes();
|
void copyNotes();
|
||||||
void copyAttribute(QAction* action);
|
void copyAttribute(QAction* action);
|
||||||
void filterByTag(const QModelIndex& index);
|
void filterByTag();
|
||||||
|
void setTag(QAction* action);
|
||||||
void showTotp();
|
void showTotp();
|
||||||
void showTotpKeyQrCode();
|
void showTotpKeyQrCode();
|
||||||
void copyTotp();
|
void copyTotp();
|
||||||
@ -218,6 +220,8 @@ public slots:
|
|||||||
|
|
||||||
// Search related slots
|
// Search related slots
|
||||||
void search(const QString& searchtext);
|
void search(const QString& searchtext);
|
||||||
|
void saveSearch(const QString& searchtext);
|
||||||
|
void deleteSearch(const QString& name);
|
||||||
void setSearchCaseSensitive(bool state);
|
void setSearchCaseSensitive(bool state);
|
||||||
void setSearchLimitGroup(bool state);
|
void setSearchLimitGroup(bool state);
|
||||||
void endSearch();
|
void endSearch();
|
||||||
@ -283,7 +287,7 @@ private:
|
|||||||
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
|
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
|
||||||
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
|
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
|
||||||
QPointer<GroupView> m_groupView;
|
QPointer<GroupView> m_groupView;
|
||||||
QPointer<QListView> m_tagView;
|
QPointer<TagView> m_tagView;
|
||||||
QPointer<EntryView> m_entryView;
|
QPointer<EntryView> m_entryView;
|
||||||
|
|
||||||
QScopedPointer<Group> m_newGroup;
|
QScopedPointer<Group> m_newGroup;
|
||||||
|
@ -115,48 +115,72 @@ void EntryPreviewWidget::clear()
|
|||||||
|
|
||||||
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
|
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
|
||||||
{
|
{
|
||||||
|
disconnect(m_currentEntry);
|
||||||
|
disconnect(m_currentGroup);
|
||||||
|
|
||||||
|
m_currentEntry = selectedEntry;
|
||||||
|
m_currentGroup = nullptr;
|
||||||
|
|
||||||
if (!selectedEntry) {
|
if (!selectedEntry) {
|
||||||
hide();
|
hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentEntry = selectedEntry;
|
connect(selectedEntry, &Entry::modified, this, &EntryPreviewWidget::refresh);
|
||||||
|
refresh();
|
||||||
updateEntryHeaderLine();
|
|
||||||
updateEntryTotp();
|
|
||||||
updateEntryGeneralTab();
|
|
||||||
updateEntryAdvancedTab();
|
|
||||||
updateEntryAutotypeTab();
|
|
||||||
|
|
||||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
|
||||||
|
|
||||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
|
||||||
const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
|
||||||
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
|
||||||
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryPreviewWidget::setGroup(Group* selectedGroup)
|
void EntryPreviewWidget::setGroup(Group* selectedGroup)
|
||||||
{
|
{
|
||||||
|
disconnect(m_currentEntry);
|
||||||
|
disconnect(m_currentGroup);
|
||||||
|
|
||||||
|
m_currentEntry = nullptr;
|
||||||
|
m_currentGroup = selectedGroup;
|
||||||
|
|
||||||
if (!selectedGroup) {
|
if (!selectedGroup) {
|
||||||
hide();
|
hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentGroup = selectedGroup;
|
connect(m_currentGroup, &Group::modified, this, &EntryPreviewWidget::refresh);
|
||||||
updateGroupHeaderLine();
|
refresh();
|
||||||
updateGroupGeneralTab();
|
}
|
||||||
|
|
||||||
|
void EntryPreviewWidget::refresh()
|
||||||
|
{
|
||||||
|
if (m_currentEntry) {
|
||||||
|
updateEntryHeaderLine();
|
||||||
|
updateEntryTotp();
|
||||||
|
updateEntryGeneralTab();
|
||||||
|
updateEntryAdvancedTab();
|
||||||
|
updateEntryAutotypeTab();
|
||||||
|
|
||||||
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||||
|
|
||||||
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
||||||
|
const int tabIndex =
|
||||||
|
m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
||||||
|
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
||||||
|
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
||||||
|
} else if (m_currentGroup) {
|
||||||
|
updateGroupHeaderLine();
|
||||||
|
updateGroupGeneralTab();
|
||||||
|
|
||||||
#if defined(WITH_XC_KEESHARE)
|
#if defined(WITH_XC_KEESHARE)
|
||||||
updateGroupSharingTab();
|
updateGroupSharingTab();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||||
|
|
||||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
||||||
const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
const int tabIndex =
|
||||||
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
||||||
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
||||||
|
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
||||||
|
} else {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryPreviewWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
void EntryPreviewWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
||||||
@ -240,6 +264,8 @@ void EntryPreviewWidget::setNotesVisible(QTextEdit* notesWidget, const QString&
|
|||||||
} else {
|
} else {
|
||||||
if (!notes.isEmpty()) {
|
if (!notes.isEmpty()) {
|
||||||
notesWidget->setPlainText(QString("\u25cf").repeated(6));
|
notesWidget->setPlainText(QString("\u25cf").repeated(6));
|
||||||
|
} else {
|
||||||
|
notesWidget->setPlainText("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ public slots:
|
|||||||
void setEntry(Entry* selectedEntry);
|
void setEntry(Entry* selectedEntry);
|
||||||
void setGroup(Group* selectedGroup);
|
void setGroup(Group* selectedGroup);
|
||||||
void setDatabaseMode(DatabaseWidget::Mode mode);
|
void setDatabaseMode(DatabaseWidget::Mode mode);
|
||||||
|
void refresh();
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
@ -129,6 +129,7 @@ MainWindow::MainWindow()
|
|||||||
m_entryContextMenu->addAction(m_ui->actionEntryCopyPassword);
|
m_entryContextMenu->addAction(m_ui->actionEntryCopyPassword);
|
||||||
m_entryContextMenu->addAction(m_ui->menuEntryCopyAttribute->menuAction());
|
m_entryContextMenu->addAction(m_ui->menuEntryCopyAttribute->menuAction());
|
||||||
m_entryContextMenu->addAction(m_ui->menuEntryTotp->menuAction());
|
m_entryContextMenu->addAction(m_ui->menuEntryTotp->menuAction());
|
||||||
|
m_entryContextMenu->addAction(m_ui->menuTags->menuAction());
|
||||||
m_entryContextMenu->addSeparator();
|
m_entryContextMenu->addSeparator();
|
||||||
m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
|
m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
|
||||||
m_entryContextMenu->addSeparator();
|
m_entryContextMenu->addSeparator();
|
||||||
@ -240,6 +241,11 @@ MainWindow::MainWindow()
|
|||||||
m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*)));
|
m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*)));
|
||||||
connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu()));
|
connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu()));
|
||||||
|
|
||||||
|
m_setTagsMenuActions = new QActionGroup(m_ui->menuTags);
|
||||||
|
m_setTagsMenuActions->setExclusive(false);
|
||||||
|
m_actionMultiplexer.connect(m_setTagsMenuActions, SIGNAL(triggered(QAction*)), SLOT(setTag(QAction*)));
|
||||||
|
connect(m_ui->menuTags, &QMenu::aboutToShow, this, &MainWindow::updateSetTagsMenu);
|
||||||
|
|
||||||
Qt::Key globalAutoTypeKey = static_cast<Qt::Key>(config()->get(Config::GlobalAutoTypeKey).toInt());
|
Qt::Key globalAutoTypeKey = static_cast<Qt::Key>(config()->get(Config::GlobalAutoTypeKey).toInt());
|
||||||
Qt::KeyboardModifiers globalAutoTypeModifiers =
|
Qt::KeyboardModifiers globalAutoTypeModifiers =
|
||||||
static_cast<Qt::KeyboardModifiers>(config()->get(Config::GlobalAutoTypeModifiers).toInt());
|
static_cast<Qt::KeyboardModifiers>(config()->get(Config::GlobalAutoTypeModifiers).toInt());
|
||||||
@ -791,6 +797,38 @@ void MainWindow::updateCopyAttributesMenu()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::updateSetTagsMenu()
|
||||||
|
{
|
||||||
|
// Remove all existing actions
|
||||||
|
m_ui->menuTags->clear();
|
||||||
|
|
||||||
|
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
|
||||||
|
if (dbWidget) {
|
||||||
|
// Enumerate tags applied to the selected entries
|
||||||
|
QSet<QString> selectedTags;
|
||||||
|
for (auto entry : dbWidget->entryView()->selectedEntries()) {
|
||||||
|
for (auto tag : entry->tagList()) {
|
||||||
|
selectedTags.insert(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add known database tags as actions and set checked if
|
||||||
|
// a selected entry has that tag
|
||||||
|
for (auto tag : dbWidget->database()->tagList()) {
|
||||||
|
auto action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
|
||||||
|
action->setCheckable(true);
|
||||||
|
action->setChecked(selectedTags.contains(tag));
|
||||||
|
m_setTagsMenuActions->addAction(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tags exist in the database then show a tip to the user
|
||||||
|
if (m_ui->menuTags->isEmpty()) {
|
||||||
|
auto action = m_ui->menuTags->addAction(tr("No Tags"));
|
||||||
|
action->setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::openRecentDatabase(QAction* action)
|
void MainWindow::openRecentDatabase(QAction* action)
|
||||||
{
|
{
|
||||||
openDatabase(action->data().toString());
|
openDatabase(action->data().toString());
|
||||||
@ -870,6 +908,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
|
|||||||
m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes());
|
m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes());
|
||||||
m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected);
|
m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected);
|
||||||
m_ui->menuEntryTotp->setEnabled(singleEntrySelected);
|
m_ui->menuEntryTotp->setEnabled(singleEntrySelected);
|
||||||
|
m_ui->menuTags->setEnabled(entriesSelected);
|
||||||
m_ui->actionEntryAutoType->setEnabled(singleEntrySelected);
|
m_ui->actionEntryAutoType->setEnabled(singleEntrySelected);
|
||||||
m_ui->actionEntryAutoType->menu()->setEnabled(singleEntrySelected);
|
m_ui->actionEntryAutoType->menu()->setEnabled(singleEntrySelected);
|
||||||
m_ui->actionEntryAutoTypeSequence->setText(
|
m_ui->actionEntryAutoTypeSequence->setText(
|
||||||
|
@ -130,6 +130,7 @@ private slots:
|
|||||||
void clearLastDatabases();
|
void clearLastDatabases();
|
||||||
void updateLastDatabasesMenu();
|
void updateLastDatabasesMenu();
|
||||||
void updateCopyAttributesMenu();
|
void updateCopyAttributesMenu();
|
||||||
|
void updateSetTagsMenu();
|
||||||
void showEntryContextMenu(const QPoint& globalPos);
|
void showEntryContextMenu(const QPoint& globalPos);
|
||||||
void showGroupContextMenu(const QPoint& globalPos);
|
void showGroupContextMenu(const QPoint& globalPos);
|
||||||
void applySettingsChanges();
|
void applySettingsChanges();
|
||||||
@ -172,6 +173,7 @@ private:
|
|||||||
QPointer<QMenu> m_entryNewContextMenu;
|
QPointer<QMenu> m_entryNewContextMenu;
|
||||||
QPointer<QActionGroup> m_lastDatabasesActions;
|
QPointer<QActionGroup> m_lastDatabasesActions;
|
||||||
QPointer<QActionGroup> m_copyAdditionalAttributeActions;
|
QPointer<QActionGroup> m_copyAdditionalAttributeActions;
|
||||||
|
QPointer<QActionGroup> m_setTagsMenuActions;
|
||||||
QPointer<InactivityTimer> m_inactivityTimer;
|
QPointer<InactivityTimer> m_inactivityTimer;
|
||||||
QPointer<InactivityTimer> m_touchIDinactivityTimer;
|
QPointer<InactivityTimer> m_touchIDinactivityTimer;
|
||||||
int m_countDefaultAttributes;
|
int m_countDefaultAttributes;
|
||||||
|
@ -316,6 +316,11 @@
|
|||||||
<addaction name="actionEntryTotpQRCode"/>
|
<addaction name="actionEntryTotpQRCode"/>
|
||||||
<addaction name="actionEntrySetupTotp"/>
|
<addaction name="actionEntrySetupTotp"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuTags">
|
||||||
|
<property name="title">
|
||||||
|
<string>Tags</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
<addaction name="actionEntryNew"/>
|
<addaction name="actionEntryNew"/>
|
||||||
<addaction name="actionEntryEdit"/>
|
<addaction name="actionEntryEdit"/>
|
||||||
<addaction name="actionEntryClone"/>
|
<addaction name="actionEntryClone"/>
|
||||||
@ -328,6 +333,7 @@
|
|||||||
<addaction name="actionEntryCopyPassword"/>
|
<addaction name="actionEntryCopyPassword"/>
|
||||||
<addaction name="menuEntryCopyAttribute"/>
|
<addaction name="menuEntryCopyAttribute"/>
|
||||||
<addaction name="menuEntryTotp"/>
|
<addaction name="menuEntryTotp"/>
|
||||||
|
<addaction name="menuTags"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionEntryAutoType"/>
|
<addaction name="actionEntryAutoType"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
@ -46,6 +46,7 @@ SearchWidget::SearchWidget(QWidget* parent)
|
|||||||
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
|
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
|
||||||
connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp()));
|
connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp()));
|
||||||
connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu()));
|
connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu()));
|
||||||
|
connect(m_ui->saveIcon, &QAction::triggered, this, [this] { emit saveSearch(m_ui->searchEdit->text()); });
|
||||||
connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
|
connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
|
||||||
connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
|
connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
|
||||||
connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
|
connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
|
||||||
@ -70,6 +71,10 @@ SearchWidget::SearchWidget(QWidget* parent)
|
|||||||
m_ui->helpIcon->setIcon(icons()->icon("system-help"));
|
m_ui->helpIcon->setIcon(icons()->icon("system-help"));
|
||||||
m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition);
|
m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition);
|
||||||
|
|
||||||
|
m_ui->saveIcon->setIcon(icons()->icon("document-save"));
|
||||||
|
m_ui->searchEdit->addAction(m_ui->saveIcon, QLineEdit::TrailingPosition);
|
||||||
|
m_ui->saveIcon->setVisible(false);
|
||||||
|
|
||||||
// Fix initial visibility of actions (bug in Qt)
|
// Fix initial visibility of actions (bug in Qt)
|
||||||
for (QToolButton* toolButton : m_ui->searchEdit->findChildren<QToolButton*>()) {
|
for (QToolButton* toolButton : m_ui->searchEdit->findChildren<QToolButton*>()) {
|
||||||
toolButton->setVisible(toolButton->defaultAction()->isVisible());
|
toolButton->setVisible(toolButton->defaultAction()->isVisible());
|
||||||
@ -126,6 +131,7 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
|||||||
{
|
{
|
||||||
// Connects basically only to the current DatabaseWidget, but allows to switch between instances!
|
// Connects basically only to the current DatabaseWidget, but allows to switch between instances!
|
||||||
mx.connect(this, SIGNAL(search(QString)), SLOT(search(QString)));
|
mx.connect(this, SIGNAL(search(QString)), SLOT(search(QString)));
|
||||||
|
mx.connect(this, SIGNAL(saveSearch(QString)), SLOT(saveSearch(QString)));
|
||||||
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
|
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
|
||||||
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
|
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
|
||||||
mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
|
mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
|
||||||
@ -165,6 +171,7 @@ void SearchWidget::startSearch()
|
|||||||
m_searchTimer->stop();
|
m_searchTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_ui->saveIcon->setVisible(true);
|
||||||
search(m_ui->searchEdit->text());
|
search(m_ui->searchEdit->text());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +215,7 @@ void SearchWidget::focusSearch()
|
|||||||
void SearchWidget::clearSearch()
|
void SearchWidget::clearSearch()
|
||||||
{
|
{
|
||||||
m_ui->searchEdit->clear();
|
m_ui->searchEdit->clear();
|
||||||
|
m_ui->saveIcon->setVisible(false);
|
||||||
emit searchCanceled();
|
emit searchCanceled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ signals:
|
|||||||
void downPressed();
|
void downPressed();
|
||||||
void enterPressed();
|
void enterPressed();
|
||||||
void lostFocus();
|
void lostFocus();
|
||||||
|
void saveSearch(const QString& text);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void databaseChanged(DatabaseWidget* dbWidget = nullptr);
|
void databaseChanged(DatabaseWidget* dbWidget = nullptr);
|
||||||
|
@ -56,6 +56,11 @@
|
|||||||
<string>Search Help</string>
|
<string>Search Help</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="saveIcon">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save Search</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>searchEdit</tabstop>
|
<tabstop>searchEdit</tabstop>
|
||||||
|
@ -320,8 +320,8 @@
|
|||||||
<tabstop>usernameComboBox</tabstop>
|
<tabstop>usernameComboBox</tabstop>
|
||||||
<tabstop>passwordEdit</tabstop>
|
<tabstop>passwordEdit</tabstop>
|
||||||
<tabstop>urlEdit</tabstop>
|
<tabstop>urlEdit</tabstop>
|
||||||
<tabstop>tagsList</tabstop>
|
|
||||||
<tabstop>fetchFaviconButton</tabstop>
|
<tabstop>fetchFaviconButton</tabstop>
|
||||||
|
<tabstop>tagsList</tabstop>
|
||||||
<tabstop>expireCheck</tabstop>
|
<tabstop>expireCheck</tabstop>
|
||||||
<tabstop>expireDatePicker</tabstop>
|
<tabstop>expireDatePicker</tabstop>
|
||||||
<tabstop>expirePresets</tabstop>
|
<tabstop>expirePresets</tabstop>
|
||||||
|
@ -263,6 +263,15 @@ Entry* EntryView::currentEntry()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<Entry*> EntryView::selectedEntries()
|
||||||
|
{
|
||||||
|
QList<Entry*> list;
|
||||||
|
for (auto row : selectionModel()->selectedRows()) {
|
||||||
|
list.append(m_model->entryFromIndex(m_sortModel->mapToSource(row)));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
int EntryView::numberOfSelectedEntries()
|
int EntryView::numberOfSelectedEntries()
|
||||||
{
|
{
|
||||||
return selectionModel()->selectedRows().size();
|
return selectionModel()->selectedRows().size();
|
||||||
|
@ -38,6 +38,7 @@ public:
|
|||||||
void setModel(QAbstractItemModel* model) override;
|
void setModel(QAbstractItemModel* model) override;
|
||||||
Entry* currentEntry();
|
Entry* currentEntry();
|
||||||
void setCurrentEntry(Entry* entry);
|
void setCurrentEntry(Entry* entry);
|
||||||
|
QList<Entry*> selectedEntries();
|
||||||
Entry* entryFromIndex(const QModelIndex& index);
|
Entry* entryFromIndex(const QModelIndex& index);
|
||||||
QModelIndex indexFromEntry(Entry* entry);
|
QModelIndex indexFromEntry(Entry* entry);
|
||||||
int currentEntryIndex();
|
int currentEntryIndex();
|
||||||
|
@ -18,12 +18,19 @@
|
|||||||
#include "TagModel.h"
|
#include "TagModel.h"
|
||||||
|
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
|
#include "gui/MessageBox.h"
|
||||||
|
|
||||||
TagModel::TagModel(QSharedPointer<Database> db, QObject* parent)
|
#include <QApplication>
|
||||||
|
#include <QMenu>
|
||||||
|
|
||||||
|
TagModel::TagModel(QObject* parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
{
|
{
|
||||||
setDatabase(db);
|
m_defaultSearches << qMakePair(tr("Clear Search"), QString("")) << qMakePair(tr("All Entries"), QString("*"))
|
||||||
|
<< qMakePair(tr("Expired"), QString("is:expired"))
|
||||||
|
<< qMakePair(tr("Weak Passwords"), QString("is:weak"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TagModel::~TagModel()
|
TagModel::~TagModel()
|
||||||
@ -32,12 +39,19 @@ TagModel::~TagModel()
|
|||||||
|
|
||||||
void TagModel::setDatabase(QSharedPointer<Database> db)
|
void TagModel::setDatabase(QSharedPointer<Database> db)
|
||||||
{
|
{
|
||||||
|
if (m_db) {
|
||||||
|
disconnect(m_db.data());
|
||||||
|
}
|
||||||
|
|
||||||
m_db = db;
|
m_db = db;
|
||||||
if (!m_db) {
|
if (!m_db) {
|
||||||
m_tagList.clear();
|
m_tagList.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(m_db.data(), SIGNAL(tagListUpdated()), SLOT(updateTagList()));
|
connect(m_db.data(), SIGNAL(tagListUpdated()), SLOT(updateTagList()));
|
||||||
|
connect(m_db->metadata()->customData(), SIGNAL(modified()), SLOT(updateTagList()));
|
||||||
|
|
||||||
updateTagList();
|
updateTagList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +59,35 @@ void TagModel::updateTagList()
|
|||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_tagList.clear();
|
m_tagList.clear();
|
||||||
m_tagList << tr("All") << tr("Expired") << tr("Weak Passwords") << m_db->tagList();
|
|
||||||
|
m_tagList << m_defaultSearches;
|
||||||
|
|
||||||
|
auto savedSearches = m_db->metadata()->savedSearches();
|
||||||
|
for (auto search : savedSearches.keys()) {
|
||||||
|
m_tagList << qMakePair(search, savedSearches[search].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_tagListStart = m_tagList.size();
|
||||||
|
for (auto tag : m_db->tagList()) {
|
||||||
|
auto escapedTag = tag;
|
||||||
|
escapedTag.replace("\"", "\\\"");
|
||||||
|
m_tagList << qMakePair(tag, QString("tag:\"%1\"").arg(escapedTag));
|
||||||
|
}
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagModel::TagType TagModel::itemType(const QModelIndex& index)
|
||||||
|
{
|
||||||
|
int row = index.row();
|
||||||
|
if (row < m_defaultSearches.size()) {
|
||||||
|
return TagType::DEFAULT_SEARCH;
|
||||||
|
} else if (row < m_tagListStart) {
|
||||||
|
return TagType::SAVED_SEARCH;
|
||||||
|
}
|
||||||
|
return TagType::TAG;
|
||||||
|
}
|
||||||
|
|
||||||
int TagModel::rowCount(const QModelIndex& parent) const
|
int TagModel::rowCount(const QModelIndex& parent) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(parent);
|
Q_UNUSED(parent);
|
||||||
@ -61,29 +100,23 @@ QVariant TagModel::data(const QModelIndex& index, int role) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto row = index.row();
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case Qt::DecorationRole:
|
case Qt::DecorationRole:
|
||||||
if (index.row() <= 2) {
|
if (row < m_tagListStart) {
|
||||||
return icons()->icon("tag-search");
|
return icons()->icon("database-search");
|
||||||
}
|
}
|
||||||
return icons()->icon("tag");
|
return icons()->icon("tag");
|
||||||
case Qt::DisplayRole:
|
case Qt::DisplayRole:
|
||||||
return m_tagList.at(index.row());
|
return m_tagList.at(row).first;
|
||||||
case Qt::UserRole:
|
case Qt::UserRole:
|
||||||
if (index.row() == 0) {
|
return m_tagList.at(row).second;
|
||||||
return "";
|
case Qt::UserRole + 1:
|
||||||
} else if (index.row() == 1) {
|
if (row == (m_defaultSearches.size() - 1)) {
|
||||||
return "is:expired";
|
return true;
|
||||||
} else if (index.row() == 2) {
|
|
||||||
return "is:weak";
|
|
||||||
}
|
}
|
||||||
return QString("tag:%1").arg(m_tagList.at(index.row()));
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const QStringList& TagModel::tags() const
|
|
||||||
{
|
|
||||||
return m_tagList;
|
|
||||||
}
|
|
||||||
|
@ -28,21 +28,30 @@ class TagModel : public QAbstractListModel
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TagModel(QSharedPointer<Database> db, QObject* parent = nullptr);
|
explicit TagModel(QObject* parent = nullptr);
|
||||||
~TagModel() override;
|
~TagModel() override;
|
||||||
|
|
||||||
void setDatabase(QSharedPointer<Database> db);
|
void setDatabase(QSharedPointer<Database> db);
|
||||||
const QStringList& tags() const;
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
enum TagType
|
||||||
|
{
|
||||||
|
DEFAULT_SEARCH,
|
||||||
|
SAVED_SEARCH,
|
||||||
|
TAG
|
||||||
|
};
|
||||||
|
TagType itemType(const QModelIndex& index);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateTagList();
|
void updateTagList();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSharedPointer<Database> m_db;
|
QSharedPointer<Database> m_db;
|
||||||
QStringList m_tagList;
|
QList<QPair<QString, QString>> m_defaultSearches;
|
||||||
|
QList<QPair<QString, QString>> m_tagList;
|
||||||
|
int m_tagListStart = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TAGMODEL_H
|
#endif // KEEPASSX_TAGMODEL_H
|
||||||
|
98
src/gui/tag/TagView.cpp
Normal file
98
src/gui/tag/TagView.cpp
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 2 or (at your option)
|
||||||
|
* version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TagView.h"
|
||||||
|
|
||||||
|
#include "TagModel.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
|
#include "gui/Icons.h"
|
||||||
|
#include "gui/MessageBox.h"
|
||||||
|
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
|
class TagItemDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TagItemDelegate(QObject* parent)
|
||||||
|
: QStyledItemDelegate(parent){};
|
||||||
|
|
||||||
|
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
||||||
|
{
|
||||||
|
QStyledItemDelegate::paint(painter, option, index);
|
||||||
|
if (index.data(Qt::UserRole + 1).toBool()) {
|
||||||
|
QRect bounds = option.rect;
|
||||||
|
bounds.setY(bounds.bottom());
|
||||||
|
painter->fillRect(bounds, option.palette.mid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TagView::TagView(QWidget* parent)
|
||||||
|
: QListView(parent)
|
||||||
|
, m_model(new TagModel(this))
|
||||||
|
{
|
||||||
|
setModel(m_model);
|
||||||
|
setFrameStyle(QFrame::NoFrame);
|
||||||
|
setSelectionMode(QListView::ExtendedSelection);
|
||||||
|
setSelectionBehavior(QListView::SelectRows);
|
||||||
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||||
|
setItemDelegate(new TagItemDelegate(this));
|
||||||
|
|
||||||
|
connect(this, &QListView::customContextMenuRequested, this, &TagView::contextMenuRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagView::setDatabase(QSharedPointer<Database> db)
|
||||||
|
{
|
||||||
|
m_db = db;
|
||||||
|
m_model->setDatabase(db);
|
||||||
|
setCurrentIndex(m_model->index(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagView::contextMenuRequested(const QPoint& pos)
|
||||||
|
{
|
||||||
|
auto index = indexAt(pos);
|
||||||
|
if (!index.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto type = m_model->itemType(index);
|
||||||
|
if (type == TagModel::SAVED_SEARCH) {
|
||||||
|
// Allow deleting saved searches
|
||||||
|
QMenu menu;
|
||||||
|
auto action = menu.exec({new QAction(icons()->icon("trash"), tr("Remove Search"))}, mapToGlobal(pos));
|
||||||
|
if (action) {
|
||||||
|
m_db->metadata()->deleteSavedSearch(index.data(Qt::DisplayRole).toString());
|
||||||
|
}
|
||||||
|
} else if (type == TagModel::TAG) {
|
||||||
|
// Allow removing tags from all entries in a database
|
||||||
|
QMenu menu;
|
||||||
|
auto action = menu.exec({new QAction(icons()->icon("trash"), tr("Remove Tag"))}, mapToGlobal(pos));
|
||||||
|
if (action) {
|
||||||
|
auto tag = index.data(Qt::DisplayRole).toString();
|
||||||
|
auto ans = MessageBox::question(this,
|
||||||
|
tr("Confirm Remove Tag"),
|
||||||
|
tr("Remove tag \"%1\" from all entries in this database?").arg(tag),
|
||||||
|
MessageBox::Remove | MessageBox::Cancel);
|
||||||
|
if (ans == MessageBox::Remove) {
|
||||||
|
m_db->removeTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/gui/tag/TagView.h
Normal file
47
src/gui/tag/TagView.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 2 or (at your option)
|
||||||
|
* version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSXC_TAGVIEW_H
|
||||||
|
#define KEEPASSXC_TAGVIEW_H
|
||||||
|
|
||||||
|
#include <QListView>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
class QAbstractListModel;
|
||||||
|
class TagModel;
|
||||||
|
|
||||||
|
class TagView : public QListView
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TagView(QWidget* parent = nullptr);
|
||||||
|
void setDatabase(QSharedPointer<Database> db);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void contextMenuRequested(const QPoint& pos);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSharedPointer<Database> m_db;
|
||||||
|
QPointer<TagModel> m_model;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_ENTRYVIEW_H
|
@ -401,6 +401,7 @@ struct TagsEdit::Impl
|
|||||||
// and ensures Invariant-1.
|
// and ensures Invariant-1.
|
||||||
void editNewTag(int i)
|
void editNewTag(int i)
|
||||||
{
|
{
|
||||||
|
currentText() = currentText().trimmed();
|
||||||
tags.insert(std::next(std::begin(tags), static_cast<std::ptrdiff_t>(i)), Tag());
|
tags.insert(std::next(std::begin(tags), static_cast<std::ptrdiff_t>(i)), Tag());
|
||||||
if (editing_index >= i) {
|
if (editing_index >= i) {
|
||||||
++editing_index;
|
++editing_index;
|
||||||
@ -646,6 +647,12 @@ void TagsEdit::focusOutEvent(QFocusEvent*)
|
|||||||
viewport()->update();
|
viewport()->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TagsEdit::hideEvent(QHideEvent* event)
|
||||||
|
{
|
||||||
|
Q_UNUSED(event)
|
||||||
|
impl->completer->popup()->hide();
|
||||||
|
}
|
||||||
|
|
||||||
void TagsEdit::paintEvent(QPaintEvent*)
|
void TagsEdit::paintEvent(QPaintEvent*)
|
||||||
{
|
{
|
||||||
QPainter p(viewport());
|
QPainter p(viewport());
|
||||||
|
@ -68,6 +68,7 @@ protected:
|
|||||||
void focusOutEvent(QFocusEvent* event) override;
|
void focusOutEvent(QFocusEvent* event) override;
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
void mouseMoveEvent(QMouseEvent* event) override;
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
void hideEvent(QHideEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isAcceptableInput(QKeyEvent const* event) const;
|
bool isAcceptableInput(QKeyEvent const* event) const;
|
||||||
|
@ -205,7 +205,7 @@ void TestEntrySearcher::testSearchTermParser()
|
|||||||
QCOMPARE(terms[0].exclude, true);
|
QCOMPARE(terms[0].exclude, true);
|
||||||
|
|
||||||
QCOMPARE(terms[1].field, EntrySearcher::Field::Undefined);
|
QCOMPARE(terms[1].field, EntrySearcher::Field::Undefined);
|
||||||
QCOMPARE(terms[1].word, QString("quoted \\\"string\\\""));
|
QCOMPARE(terms[1].word, QString("quoted \"string\""));
|
||||||
QCOMPARE(terms[1].exclude, false);
|
QCOMPARE(terms[1].exclude, false);
|
||||||
|
|
||||||
QCOMPARE(terms[2].field, EntrySearcher::Field::Username);
|
QCOMPARE(terms[2].field, EntrySearcher::Field::Username);
|
||||||
|
Loading…
Reference in New Issue
Block a user