mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Add tags feature
* show the tags in the entry preview * allow searching by tag * add a sidebar listing the tags in the database * filter entries by tag on click * Introduce a new TagsEdit widget that provides pill aesthetics, fast removal functionality and autocompletion * add tests for the tags feature * introduce the "is" tag for searching. Support for weak passwords and expired added.
This commit is contained in:
parent
56a1b465a1
commit
4a21cee98c
2
COPYING
2
COPYING
@ -191,6 +191,8 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg
|
|||||||
share/icons/application/scalable/actions/statistics.svg
|
share/icons/application/scalable/actions/statistics.svg
|
||||||
share/icons/application/scalable/actions/system-help.svg
|
share/icons/application/scalable/actions/system-help.svg
|
||||||
share/icons/application/scalable/actions/system-search.svg
|
share/icons/application/scalable/actions/system-search.svg
|
||||||
|
share/icons/application/scalable/actions/tag.svg
|
||||||
|
share/icons/application/scalable/actions/tag-search.svg
|
||||||
share/icons/application/scalable/actions/trash.svg
|
share/icons/application/scalable/actions/trash.svg
|
||||||
share/icons/application/scalable/actions/url-copy.svg
|
share/icons/application/scalable/actions/url-copy.svg
|
||||||
share/icons/application/scalable/actions/username-copy.svg
|
share/icons/application/scalable/actions/username-copy.svg
|
||||||
|
@ -27,6 +27,7 @@ set(EXCLUDED_FILES
|
|||||||
src/streams/qtiocompressor.\\*
|
src/streams/qtiocompressor.\\*
|
||||||
src/gui/KMessageWidget.\\*
|
src/gui/KMessageWidget.\\*
|
||||||
src/gui/MainWindowAdaptor.\\*
|
src/gui/MainWindowAdaptor.\\*
|
||||||
|
src/gui/tag/TagsEdit.\\*
|
||||||
tests/modeltest.\\*
|
tests/modeltest.\\*
|
||||||
# objective-c files
|
# objective-c files
|
||||||
src/core/ScreenLockListenerMac.\\*)
|
src/core/ScreenLockListenerMac.\\*)
|
||||||
|
1
share/icons/application/scalable/actions/tag-search.svg
Normal file
1
share/icons/application/scalable/actions/tag-search.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M22 13C22 13.53 21.79 14.04 21.41 14.41L21 14.83C20.91 11.97 18.84 9.62 16.11 9.11L11 4H4V11L9.11 16.11C9.62 18.84 11.97 20.91 14.83 21L14.41 21.41C14.04 21.79 13.53 22 13 22C12.47 22 11.97 21.79 11.59 21.42L2.59 12.42C2.21 12.04 2 11.53 2 11V4C2 2.9 2.9 2 4 2H11C11.53 2 12.04 2.21 12.41 2.58L21.41 11.58C21.79 11.96 22 12.47 22 13M5 6.5C5 7.33 5.67 8 6.5 8S8 7.33 8 6.5 7.33 5 6.5 5 5 5.67 5 6.5M15.11 10.61C12.61 10.61 10.61 12.61 10.61 15.11S12.61 19.61 15.11 19.61C16 19.61 16.8 19.36 17.5 18.93L20.61 22L22 20.61L18.92 17.5C19.36 16.82 19.61 16 19.61 15.11C19.61 12.61 17.61 10.61 15.11 10.61M15.11 12.61C16.5 12.61 17.61 13.73 17.61 15.11S16.5 17.61 15.11 17.61 12.61 16.5 12.61 15.11 13.73 12.61 15.11 12.61" /></svg>
|
After Width: | Height: | Size: 1010 B |
1
share/icons/application/scalable/actions/tag.svg
Normal file
1
share/icons/application/scalable/actions/tag.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M21.41 11.58L12.41 2.58A2 2 0 0 0 11 2H4A2 2 0 0 0 2 4V11A2 2 0 0 0 2.59 12.42L11.59 21.42A2 2 0 0 0 13 22A2 2 0 0 0 14.41 21.41L21.41 14.41A2 2 0 0 0 22 13A2 2 0 0 0 21.41 11.58M13 20L4 11V4H11L20 13M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5Z" /></svg>
|
After Width: | Height: | Size: 542 B |
1
share/icons/application/scalable/categories/label.svg
Normal file
1
share/icons/application/scalable/categories/label.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.63,5.84C17.27,5.33 16.67,5 16,5H5A2,2 0 0,0 3,7V17A2,2 0 0,0 5,19H16C16.67,19 17.27,18.66 17.63,18.15L22,12L17.63,5.84Z" /></svg>
|
After Width: | Height: | Size: 418 B |
@ -72,6 +72,8 @@
|
|||||||
<file>application/scalable/actions/system-help.svg</file>
|
<file>application/scalable/actions/system-help.svg</file>
|
||||||
<file>application/scalable/actions/system-search.svg</file>
|
<file>application/scalable/actions/system-search.svg</file>
|
||||||
<file>application/scalable/actions/system-software-update.svg</file>
|
<file>application/scalable/actions/system-software-update.svg</file>
|
||||||
|
<file>application/scalable/actions/tag.svg</file>
|
||||||
|
<file>application/scalable/actions/tag-search.svg</file>
|
||||||
<file>application/scalable/actions/trash.svg</file>
|
<file>application/scalable/actions/trash.svg</file>
|
||||||
<file>application/scalable/actions/url-copy.svg</file>
|
<file>application/scalable/actions/url-copy.svg</file>
|
||||||
<file>application/scalable/actions/user-guide.svg</file>
|
<file>application/scalable/actions/user-guide.svg</file>
|
||||||
|
@ -2398,6 +2398,10 @@ Disable safe saves and try again?</translation>
|
|||||||
<source>Perform Auto-Type into the previously active window?</source>
|
<source>Perform Auto-Type into the previously active window?</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Database Tags</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>EditEntryWidget</name>
|
<name>EditEntryWidget</name>
|
||||||
@ -2886,6 +2890,14 @@ Would you like to correct it?</source>
|
|||||||
<source>Edit Entry</source>
|
<source>Edit Entry</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Tags:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Tags list</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>EditEntryWidgetSSHAgent</name>
|
<name>EditEntryWidgetSSHAgent</name>
|
||||||
@ -3869,6 +3881,14 @@ Would you like to overwrite the existing attachment?</source>
|
|||||||
<source>Default Sequence</source>
|
<source>Default Sequence</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Tags</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Tags list</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>EntryURLModel</name>
|
<name>EntryURLModel</name>
|
||||||
@ -8501,6 +8521,21 @@ Please consider generating a new key file.</source>
|
|||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>TagModel</name>
|
||||||
|
<message>
|
||||||
|
<source>All</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Expired</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Weak Passwords</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>TotpDialog</name>
|
<name>TotpDialog</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -150,6 +150,8 @@ set(keepassx_SOURCES
|
|||||||
gui/group/EditGroupWidget.cpp
|
gui/group/EditGroupWidget.cpp
|
||||||
gui/group/GroupModel.cpp
|
gui/group/GroupModel.cpp
|
||||||
gui/group/GroupView.cpp
|
gui/group/GroupView.cpp
|
||||||
|
gui/tag/TagModel.cpp
|
||||||
|
gui/tag/TagsEdit.cpp
|
||||||
gui/databasekey/KeyComponentWidget.cpp
|
gui/databasekey/KeyComponentWidget.cpp
|
||||||
gui/databasekey/PasswordEditWidget.cpp
|
gui/databasekey/PasswordEditWidget.cpp
|
||||||
gui/databasekey/YubiKeyEditWidget.cpp
|
gui/databasekey/YubiKeyEditWidget.cpp
|
||||||
|
@ -117,6 +117,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
|
|||||||
{Config::GUI_ListViewState, {QS("GUI/ListViewState"), Local, {}}},
|
{Config::GUI_ListViewState, {QS("GUI/ListViewState"), Local, {}}},
|
||||||
{Config::GUI_SearchViewState, {QS("GUI/SearchViewState"), Local, {}}},
|
{Config::GUI_SearchViewState, {QS("GUI/SearchViewState"), Local, {}}},
|
||||||
{Config::GUI_SplitterState, {QS("GUI/SplitterState"), Local, {}}},
|
{Config::GUI_SplitterState, {QS("GUI/SplitterState"), Local, {}}},
|
||||||
|
{Config::GUI_GroupSplitterState, {QS("GUI/GroupSplitterState"), Local, {}}},
|
||||||
{Config::GUI_PreviewSplitterState, {QS("GUI/PreviewSplitterState"), Local, {}}},
|
{Config::GUI_PreviewSplitterState, {QS("GUI/PreviewSplitterState"), Local, {}}},
|
||||||
{Config::GUI_AutoTypeSelectDialogSize, {QS("GUI/AutoTypeSelectDialogSize"), Local, QSize(600, 250)}},
|
{Config::GUI_AutoTypeSelectDialogSize, {QS("GUI/AutoTypeSelectDialogSize"), Local, QSize(600, 250)}},
|
||||||
|
|
||||||
|
@ -98,6 +98,7 @@ public:
|
|||||||
GUI_SearchViewState,
|
GUI_SearchViewState,
|
||||||
GUI_PreviewSplitterState,
|
GUI_PreviewSplitterState,
|
||||||
GUI_SplitterState,
|
GUI_SplitterState,
|
||||||
|
GUI_GroupSplitterState,
|
||||||
GUI_AutoTypeSelectDialogSize,
|
GUI_AutoTypeSelectDialogSize,
|
||||||
GUI_CheckForUpdatesNextCheck,
|
GUI_CheckForUpdatesNextCheck,
|
||||||
|
|
||||||
|
@ -52,7 +52,11 @@ Database::Database()
|
|||||||
|
|
||||||
// other signals
|
// other signals
|
||||||
connect(m_metadata, &Metadata::modified, this, &Database::markAsModified);
|
connect(m_metadata, &Metadata::modified, this, &Database::markAsModified);
|
||||||
connect(this, &Database::databaseOpened, this, [this]() { updateCommonUsernames(); });
|
connect(this, &Database::databaseOpened, this, [this]() {
|
||||||
|
updateCommonUsernames();
|
||||||
|
updateTagList();
|
||||||
|
});
|
||||||
|
connect(this, &Database::modified, this, [this] { updateTagList(); });
|
||||||
connect(this, &Database::databaseSaved, this, [this]() { updateCommonUsernames(); });
|
connect(this, &Database::databaseSaved, this, [this]() { updateCommonUsernames(); });
|
||||||
connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
|
connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
|
||||||
|
|
||||||
@ -504,6 +508,7 @@ void Database::releaseData()
|
|||||||
|
|
||||||
m_deletedObjects.clear();
|
m_deletedObjects.clear();
|
||||||
m_commonUsernames.clear();
|
m_commonUsernames.clear();
|
||||||
|
m_tagList.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -700,17 +705,46 @@ void Database::addDeletedObject(const QUuid& uuid)
|
|||||||
addDeletedObject(delObj);
|
addDeletedObject(delObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> Database::commonUsernames()
|
const QStringList& Database::commonUsernames() const
|
||||||
{
|
{
|
||||||
return m_commonUsernames;
|
return m_commonUsernames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QStringList& Database::tagList() const
|
||||||
|
{
|
||||||
|
return m_tagList;
|
||||||
|
}
|
||||||
|
|
||||||
void Database::updateCommonUsernames(int topN)
|
void Database::updateCommonUsernames(int topN)
|
||||||
{
|
{
|
||||||
m_commonUsernames.clear();
|
m_commonUsernames.clear();
|
||||||
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
|
m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Database::updateTagList()
|
||||||
|
{
|
||||||
|
m_tagList.clear();
|
||||||
|
if (!m_rootGroup) {
|
||||||
|
emit tagListUpdated();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search groups recursively looking for tags
|
||||||
|
// Use a set to prevent adding duplicates
|
||||||
|
QSet<QString> tagSet;
|
||||||
|
for (const auto group : m_rootGroup->groupsRecursive(true)) {
|
||||||
|
for (const auto entry : group->entries()) {
|
||||||
|
for (auto tag : entry->tagList()) {
|
||||||
|
tagSet.insert(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_tagList = tagSet.toList();
|
||||||
|
m_tagList.sort();
|
||||||
|
emit tagListUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
const QUuid& Database::cipher() const
|
const QUuid& Database::cipher() const
|
||||||
{
|
{
|
||||||
return m_data.cipher;
|
return m_data.cipher;
|
||||||
|
@ -125,7 +125,8 @@ public:
|
|||||||
bool containsDeletedObject(const DeletedObject& uuid) const;
|
bool containsDeletedObject(const DeletedObject& uuid) const;
|
||||||
void setDeletedObjects(const QList<DeletedObject>& delObjs);
|
void setDeletedObjects(const QList<DeletedObject>& delObjs);
|
||||||
|
|
||||||
QList<QString> commonUsernames();
|
const QStringList& commonUsernames() const;
|
||||||
|
const QStringList& tagList() const;
|
||||||
|
|
||||||
QSharedPointer<const CompositeKey> key() const;
|
QSharedPointer<const CompositeKey> key() const;
|
||||||
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
bool setKey(const QSharedPointer<const CompositeKey>& key,
|
||||||
@ -151,6 +152,7 @@ public slots:
|
|||||||
void markAsModified();
|
void markAsModified();
|
||||||
void markAsClean();
|
void markAsClean();
|
||||||
void updateCommonUsernames(int topN = 10);
|
void updateCommonUsernames(int topN = 10);
|
||||||
|
void updateTagList();
|
||||||
void markNonDataChange();
|
void markNonDataChange();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@ -166,6 +168,7 @@ signals:
|
|||||||
void databaseSaved();
|
void databaseSaved();
|
||||||
void databaseDiscarded();
|
void databaseDiscarded();
|
||||||
void databaseFileChanged();
|
void databaseFileChanged();
|
||||||
|
void tagListUpdated();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct DatabaseData
|
struct DatabaseData
|
||||||
@ -228,7 +231,8 @@ private:
|
|||||||
bool m_hasNonDataChange = false;
|
bool m_hasNonDataChange = false;
|
||||||
QString m_keyError;
|
QString m_keyError;
|
||||||
|
|
||||||
QList<QString> m_commonUsernames;
|
QStringList m_commonUsernames;
|
||||||
|
QStringList m_tagList;
|
||||||
|
|
||||||
QUuid m_uuid;
|
QUuid m_uuid;
|
||||||
static QHash<QUuid, QPointer<Database>> s_uuidMap;
|
static QHash<QUuid, QPointer<Database>> s_uuidMap;
|
||||||
|
@ -190,6 +190,12 @@ QString Entry::tags() const
|
|||||||
return m_data.tags;
|
return m_data.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QStringList Entry::tagList() const
|
||||||
|
{
|
||||||
|
static QRegExp rx("(\\ |\\,|\\.|\\:|\\t|\\;)");
|
||||||
|
return tags().split(rx, QString::SkipEmptyParts);
|
||||||
|
}
|
||||||
|
|
||||||
const TimeInfo& Entry::timeInfo() const
|
const TimeInfo& Entry::timeInfo() const
|
||||||
{
|
{
|
||||||
return m_data.timeInfo;
|
return m_data.timeInfo;
|
||||||
@ -210,7 +216,7 @@ QString Entry::defaultAutoTypeSequence() const
|
|||||||
return m_data.defaultAutoTypeSequence;
|
return m_data.defaultAutoTypeSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
|
const QSharedPointer<PasswordHealth> Entry::passwordHealth()
|
||||||
{
|
{
|
||||||
if (!m_data.passwordHealth) {
|
if (!m_data.passwordHealth) {
|
||||||
m_data.passwordHealth.reset(new PasswordHealth(resolvePlaceholder(password())));
|
m_data.passwordHealth.reset(new PasswordHealth(resolvePlaceholder(password())));
|
||||||
@ -218,6 +224,14 @@ const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
|
|||||||
return m_data.passwordHealth;
|
return m_data.passwordHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QSharedPointer<PasswordHealth> Entry::passwordHealth() const
|
||||||
|
{
|
||||||
|
if (!m_data.passwordHealth) {
|
||||||
|
return QSharedPointer<PasswordHealth>::create(resolvePlaceholder(password()));
|
||||||
|
}
|
||||||
|
return m_data.passwordHealth;
|
||||||
|
}
|
||||||
|
|
||||||
bool Entry::excludeFromReports() const
|
bool Entry::excludeFromReports() const
|
||||||
{
|
{
|
||||||
return m_data.excludeFromReports
|
return m_data.excludeFromReports
|
||||||
|
@ -88,6 +88,7 @@ public:
|
|||||||
QString backgroundColor() const;
|
QString backgroundColor() const;
|
||||||
QString overrideUrl() const;
|
QString overrideUrl() const;
|
||||||
QString tags() const;
|
QString tags() const;
|
||||||
|
QStringList tagList() const;
|
||||||
const TimeInfo& timeInfo() const;
|
const TimeInfo& timeInfo() const;
|
||||||
bool autoTypeEnabled() const;
|
bool autoTypeEnabled() const;
|
||||||
int autoTypeObfuscation() const;
|
int autoTypeObfuscation() const;
|
||||||
@ -113,7 +114,8 @@ public:
|
|||||||
QUuid previousParentGroupUuid() const;
|
QUuid previousParentGroupUuid() const;
|
||||||
int size() const;
|
int size() const;
|
||||||
QString path() const;
|
QString path() const;
|
||||||
const QSharedPointer<PasswordHealth>& passwordHealth();
|
const QSharedPointer<PasswordHealth> passwordHealth();
|
||||||
|
const QSharedPointer<PasswordHealth> passwordHealth() const;
|
||||||
bool excludeFromReports() const;
|
bool excludeFromReports() const;
|
||||||
void setExcludeFromReports(bool state);
|
void setExcludeFromReports(bool state);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include "EntrySearcher.h"
|
#include "EntrySearcher.h"
|
||||||
|
|
||||||
|
#include "PasswordHealth.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
|||||||
auto hierarchy = entry->group()->hierarchy().join('/').prepend("/");
|
auto hierarchy = entry->group()->hierarchy().join('/').prepend("/");
|
||||||
|
|
||||||
// By default, empty term matches every entry.
|
// By default, empty term matches every entry.
|
||||||
// However when skipping protected fields, we will recject everything instead
|
// However when skipping protected fields, we will reject everything instead
|
||||||
bool found = !m_skipProtected;
|
bool found = !m_skipProtected;
|
||||||
for (const auto& term : m_searchTerms) {
|
for (const auto& term : m_searchTerms) {
|
||||||
switch (term.field) {
|
switch (term.field) {
|
||||||
@ -195,11 +196,31 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|
|||||||
found = term.regex.match(entry->group()->name()).hasMatch();
|
found = term.regex.match(entry->group()->name()).hasMatch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Field::Tag:
|
||||||
|
found = term.regex.match(entry->tags()).hasMatch();
|
||||||
|
break;
|
||||||
|
case Field::Is:
|
||||||
|
if (term.word.compare("expired", Qt::CaseInsensitive) == 0) {
|
||||||
|
found = entry->isExpired();
|
||||||
|
break;
|
||||||
|
} else if (term.word.compare("weak", Qt::CaseInsensitive) == 0) {
|
||||||
|
if (!entry->excludeFromReports() && !entry->password().isEmpty() && !entry->isExpired()) {
|
||||||
|
const auto quality = entry->passwordHealth()->quality();
|
||||||
|
if (quality == PasswordHealth::Quality::Bad || quality == PasswordHealth::Quality::Poor
|
||||||
|
|| quality == PasswordHealth::Quality::Weak) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found = false;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Terms without a specific field try to match title, username, url, and notes
|
// Terms without a specific field try to match title, username, url, and notes
|
||||||
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()
|
||||||
|| term.regex.match(entry->notes()).hasMatch();
|
|| term.regex.match(entry->notes()).hasMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,10 +247,13 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
|
|||||||
{QStringLiteral("pw"), Field::Password},
|
{QStringLiteral("pw"), Field::Password},
|
||||||
{QStringLiteral("password"), Field::Password},
|
{QStringLiteral("password"), Field::Password},
|
||||||
{QStringLiteral("title"), Field::Title},
|
{QStringLiteral("title"), Field::Title},
|
||||||
|
{QStringLiteral("t"), Field::Title},
|
||||||
{QStringLiteral("u"), Field::Username}, // u: stands for username rather than url
|
{QStringLiteral("u"), Field::Username}, // u: stands for username rather than url
|
||||||
{QStringLiteral("url"), Field::Url},
|
{QStringLiteral("url"), Field::Url},
|
||||||
{QStringLiteral("username"), Field::Username},
|
{QStringLiteral("username"), Field::Username},
|
||||||
{QStringLiteral("group"), Field::Group}};
|
{QStringLiteral("group"), Field::Group},
|
||||||
|
{QStringLiteral("tag"), Field::Tag},
|
||||||
|
{QStringLiteral("is"), Field::Is}};
|
||||||
|
|
||||||
m_searchTerms.clear();
|
m_searchTerms.clear();
|
||||||
auto results = m_termParser.globalMatch(searchString);
|
auto results = m_termParser.globalMatch(searchString);
|
||||||
|
@ -38,7 +38,9 @@ public:
|
|||||||
AttributeKV,
|
AttributeKV,
|
||||||
Attachment,
|
Attachment,
|
||||||
AttributeValue,
|
AttributeValue,
|
||||||
Group
|
Group,
|
||||||
|
Tag,
|
||||||
|
Is
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SearchTerm
|
struct SearchTerm
|
||||||
|
@ -50,6 +50,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 "keeshare/KeeShare.h"
|
#include "keeshare/KeeShare.h"
|
||||||
|
|
||||||
#ifdef WITH_XC_NETWORKING
|
#ifdef WITH_XC_NETWORKING
|
||||||
@ -65,6 +66,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
, m_db(std::move(db))
|
, m_db(std::move(db))
|
||||||
, m_mainWidget(new QWidget(this))
|
, m_mainWidget(new QWidget(this))
|
||||||
, m_mainSplitter(new QSplitter(m_mainWidget))
|
, m_mainSplitter(new QSplitter(m_mainWidget))
|
||||||
|
, m_groupSplitter(new QSplitter(this))
|
||||||
, m_messageWidget(new MessageWidget(this))
|
, m_messageWidget(new MessageWidget(this))
|
||||||
, m_previewView(new EntryPreviewWidget(this))
|
, m_previewView(new EntryPreviewWidget(this))
|
||||||
, m_previewSplitter(new QSplitter(m_mainWidget))
|
, m_previewSplitter(new QSplitter(m_mainWidget))
|
||||||
@ -79,7 +81,8 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
|
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
|
||||||
, 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(), m_mainSplitter))
|
, m_groupView(new GroupView(m_db.data(), this))
|
||||||
|
, m_tagView(new QListView(this))
|
||||||
, m_saveAttempts(0)
|
, m_saveAttempts(0)
|
||||||
, m_entrySearcher(new EntrySearcher(false))
|
, m_entrySearcher(new EntrySearcher(false))
|
||||||
{
|
{
|
||||||
@ -87,26 +90,51 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
|
|
||||||
m_messageWidget->setHidden(true);
|
m_messageWidget->setHidden(true);
|
||||||
|
|
||||||
auto* mainLayout = new QVBoxLayout();
|
auto mainLayout = new QVBoxLayout();
|
||||||
mainLayout->addWidget(m_messageWidget);
|
mainLayout->addWidget(m_messageWidget);
|
||||||
auto* hbox = new QHBoxLayout();
|
auto hbox = new QHBoxLayout();
|
||||||
mainLayout->addLayout(hbox);
|
mainLayout->addLayout(hbox);
|
||||||
hbox->addWidget(m_mainSplitter);
|
hbox->addWidget(m_mainSplitter);
|
||||||
m_mainWidget->setLayout(mainLayout);
|
m_mainWidget->setLayout(mainLayout);
|
||||||
|
|
||||||
auto* rightHandSideWidget = new QWidget(m_mainSplitter);
|
// Setup tags view and place under groups
|
||||||
auto* vbox = new QVBoxLayout();
|
auto tagModel = new TagModel(m_db);
|
||||||
vbox->setMargin(0);
|
m_tagView->setModel(tagModel);
|
||||||
vbox->addWidget(m_searchingLabel);
|
m_tagView->setFrameStyle(QFrame::NoFrame);
|
||||||
|
m_tagView->setSelectionMode(QListView::SingleSelection);
|
||||||
|
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 tagsLayout = new QVBoxLayout();
|
||||||
|
auto tagsTitle = new QLabel(tr("Database Tags"));
|
||||||
|
tagsTitle->setProperty("title", true);
|
||||||
|
tagsWidget->setLayout(tagsLayout);
|
||||||
|
tagsLayout->addWidget(tagsTitle);
|
||||||
|
tagsLayout->addWidget(m_tagView);
|
||||||
|
|
||||||
|
m_groupSplitter->setOrientation(Qt::Vertical);
|
||||||
|
m_groupSplitter->setChildrenCollapsible(true);
|
||||||
|
m_groupSplitter->addWidget(m_groupView);
|
||||||
|
m_groupSplitter->addWidget(tagsWidget);
|
||||||
|
m_groupSplitter->setStretchFactor(0, 70);
|
||||||
|
m_groupSplitter->setStretchFactor(1, 30);
|
||||||
|
|
||||||
|
auto rightHandSideWidget = new QWidget(m_mainSplitter);
|
||||||
|
auto rightHandSideVBox = new QVBoxLayout();
|
||||||
|
rightHandSideVBox->setMargin(0);
|
||||||
|
rightHandSideVBox->addWidget(m_searchingLabel);
|
||||||
#ifdef WITH_XC_KEESHARE
|
#ifdef WITH_XC_KEESHARE
|
||||||
vbox->addWidget(m_shareLabel);
|
rightHandSideVBox->addWidget(m_shareLabel);
|
||||||
#endif
|
#endif
|
||||||
vbox->addWidget(m_previewSplitter);
|
rightHandSideVBox->addWidget(m_previewSplitter);
|
||||||
rightHandSideWidget->setLayout(vbox);
|
rightHandSideWidget->setLayout(rightHandSideVBox);
|
||||||
m_entryView = new EntryView(rightHandSideWidget);
|
m_entryView = new EntryView(rightHandSideWidget);
|
||||||
|
|
||||||
m_mainSplitter->setChildrenCollapsible(false);
|
m_mainSplitter->setChildrenCollapsible(false);
|
||||||
m_mainSplitter->addWidget(m_groupView);
|
m_mainSplitter->addWidget(m_groupSplitter);
|
||||||
m_mainSplitter->addWidget(rightHandSideWidget);
|
m_mainSplitter->addWidget(rightHandSideWidget);
|
||||||
m_mainSplitter->setStretchFactor(0, 30);
|
m_mainSplitter->setStretchFactor(0, 30);
|
||||||
m_mainSplitter->setStretchFactor(1, 70);
|
m_mainSplitter->setStretchFactor(1, 70);
|
||||||
@ -165,8 +193,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
|||||||
addChildWidget(m_opVaultOpenWidget);
|
addChildWidget(m_opVaultOpenWidget);
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(mainSplitterSizesChanged()));
|
connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
|
||||||
connect(m_previewSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(previewSplitterSizesChanged()));
|
connect(m_groupSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
|
||||||
|
connect(m_previewSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
|
||||||
connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), m_previewView, SLOT(setDatabaseMode(DatabaseWidget::Mode)));
|
connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), m_previewView, SLOT(setDatabaseMode(DatabaseWidget::Mode)));
|
||||||
connect(m_previewView, SIGNAL(errorOccurred(QString)), SLOT(showErrorMessage(QString)));
|
connect(m_previewView, SIGNAL(errorOccurred(QString)), SLOT(showErrorMessage(QString)));
|
||||||
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
|
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
|
||||||
@ -298,24 +327,34 @@ bool DatabaseWidget::isEditWidgetModified() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<int> DatabaseWidget::mainSplitterSizes() const
|
QHash<Config::ConfigKey, QList<int>> DatabaseWidget::splitterSizes() const
|
||||||
{
|
{
|
||||||
return m_mainSplitter->sizes();
|
return {{Config::GUI_SplitterState, m_mainSplitter->sizes()},
|
||||||
|
{Config::GUI_PreviewSplitterState, m_previewSplitter->sizes()},
|
||||||
|
{Config::GUI_GroupSplitterState, m_groupSplitter->sizes()}};
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::setMainSplitterSizes(const QList<int>& sizes)
|
void DatabaseWidget::setSplitterSizes(const QHash<Config::ConfigKey, QList<int>>& sizes)
|
||||||
{
|
{
|
||||||
m_mainSplitter->setSizes(sizes);
|
for (auto itr = sizes.constBegin(); itr != sizes.constEnd(); ++itr) {
|
||||||
}
|
// Less than two sizes indicates an invalid value
|
||||||
|
if (itr.value().size() < 2) {
|
||||||
QList<int> DatabaseWidget::previewSplitterSizes() const
|
continue;
|
||||||
{
|
}
|
||||||
return m_previewSplitter->sizes();
|
switch (itr.key()) {
|
||||||
}
|
case Config::GUI_SplitterState:
|
||||||
|
m_mainSplitter->setSizes(itr.value());
|
||||||
void DatabaseWidget::setPreviewSplitterSizes(const QList<int>& sizes)
|
break;
|
||||||
{
|
case Config::GUI_PreviewSplitterState:
|
||||||
m_previewSplitter->setSizes(sizes);
|
m_previewSplitter->setSizes(itr.value());
|
||||||
|
break;
|
||||||
|
case Config::GUI_GroupSplitterState:
|
||||||
|
m_groupSplitter->setSizes(itr.value());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabaseWidget::setSearchStringForAutoType(const QString& search)
|
void DatabaseWidget::setSearchStringForAutoType(const QString& search)
|
||||||
@ -389,6 +428,8 @@ 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->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
|
||||||
@ -646,6 +687,13 @@ void DatabaseWidget::copyAttribute(QAction* action)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DatabaseWidget::filterByTag(const QModelIndex& index)
|
||||||
|
{
|
||||||
|
m_tagView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select);
|
||||||
|
const auto model = static_cast<TagModel*>(m_tagView->model());
|
||||||
|
emit requestSearch(model->data(index, Qt::UserRole).toString());
|
||||||
|
}
|
||||||
|
|
||||||
void DatabaseWidget::showTotpKeyQrCode()
|
void DatabaseWidget::showTotpKeyQrCode()
|
||||||
{
|
{
|
||||||
auto currentEntry = currentSelectedEntry();
|
auto currentEntry = currentSelectedEntry();
|
||||||
@ -1442,6 +1490,8 @@ void DatabaseWidget::endSearch()
|
|||||||
m_entryView->setFirstEntryActive();
|
m_entryView->setFirstEntryActive();
|
||||||
// Enforce preview view update (prevents stale information if focus group is empty)
|
// Enforce preview view update (prevents stale information if focus group is empty)
|
||||||
m_previewView->setEntry(currentSelectedEntry());
|
m_previewView->setEntry(currentSelectedEntry());
|
||||||
|
// Reset selection on tag view
|
||||||
|
m_tagView->selectionModel()->clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_searchingLabel->setVisible(false);
|
m_searchingLabel->setVisible(false);
|
||||||
@ -1512,9 +1562,12 @@ void DatabaseWidget::showEvent(QShowEvent* event)
|
|||||||
|
|
||||||
bool DatabaseWidget::focusNextPrevChild(bool next)
|
bool DatabaseWidget::focusNextPrevChild(bool next)
|
||||||
{
|
{
|
||||||
// [parent] <-> GroupView <-> EntryView <-> EntryPreview <-> [parent]
|
// [parent] <-> GroupView <-> TagView <-> EntryView <-> EntryPreview <-> [parent]
|
||||||
if (next) {
|
if (next) {
|
||||||
if (m_groupView->hasFocus()) {
|
if (m_groupView->hasFocus()) {
|
||||||
|
m_tagView->setFocus();
|
||||||
|
return true;
|
||||||
|
} else if (m_tagView->hasFocus()) {
|
||||||
m_entryView->setFocus();
|
m_entryView->setFocus();
|
||||||
return true;
|
return true;
|
||||||
} else if (m_entryView->hasFocus()) {
|
} else if (m_entryView->hasFocus()) {
|
||||||
@ -1526,6 +1579,9 @@ bool DatabaseWidget::focusNextPrevChild(bool next)
|
|||||||
m_entryView->setFocus();
|
m_entryView->setFocus();
|
||||||
return true;
|
return true;
|
||||||
} else if (m_entryView->hasFocus()) {
|
} else if (m_entryView->hasFocus()) {
|
||||||
|
m_tagView->setFocus();
|
||||||
|
return true;
|
||||||
|
} else if (m_tagView->hasFocus()) {
|
||||||
m_groupView->setFocus();
|
m_groupView->setFocus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1926,6 +1982,7 @@ bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName)
|
|||||||
// Lock out interactions
|
// Lock out interactions
|
||||||
m_entryView->setDisabled(true);
|
m_entryView->setDisabled(true);
|
||||||
m_groupView->setDisabled(true);
|
m_groupView->setDisabled(true);
|
||||||
|
m_tagView->setDisabled(true);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
Database::SaveAction saveAction = Database::Atomic;
|
Database::SaveAction saveAction = Database::Atomic;
|
||||||
@ -1967,6 +2024,7 @@ bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName)
|
|||||||
// Return control
|
// Return control
|
||||||
m_entryView->setDisabled(false);
|
m_entryView->setDisabled(false);
|
||||||
m_groupView->setDisabled(false);
|
m_groupView->setDisabled(false);
|
||||||
|
m_tagView->setDisabled(false);
|
||||||
|
|
||||||
if (focusWidget) {
|
if (focusWidget) {
|
||||||
focusWidget->setFocus();
|
focusWidget->setFocus();
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#define KEEPASSX_DATABASEWIDGET_H
|
#define KEEPASSX_DATABASEWIDGET_H
|
||||||
|
|
||||||
#include <QFileSystemWatcher>
|
#include <QFileSystemWatcher>
|
||||||
|
#include <QListView>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
|
|
||||||
#include "DatabaseOpenDialog.h"
|
#include "DatabaseOpenDialog.h"
|
||||||
@ -117,10 +118,8 @@ public:
|
|||||||
|
|
||||||
QByteArray entryViewState() const;
|
QByteArray entryViewState() const;
|
||||||
bool setEntryViewState(const QByteArray& state) const;
|
bool setEntryViewState(const QByteArray& state) const;
|
||||||
QList<int> mainSplitterSizes() const;
|
QHash<Config::ConfigKey, QList<int>> splitterSizes() const;
|
||||||
void setMainSplitterSizes(const QList<int>& sizes);
|
void setSplitterSizes(const QHash<Config::ConfigKey, QList<int>>& sizes);
|
||||||
QList<int> previewSplitterSizes() const;
|
|
||||||
void setPreviewSplitterSizes(const QList<int>& sizes);
|
|
||||||
void setSearchStringForAutoType(const QString& search);
|
void setSearchStringForAutoType(const QString& search);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@ -148,11 +147,11 @@ signals:
|
|||||||
void listModeActivated();
|
void listModeActivated();
|
||||||
void searchModeAboutToActivate();
|
void searchModeAboutToActivate();
|
||||||
void searchModeActivated();
|
void searchModeActivated();
|
||||||
void mainSplitterSizesChanged();
|
void splitterSizesChanged();
|
||||||
void previewSplitterSizesChanged();
|
|
||||||
void entryViewStateChanged();
|
void entryViewStateChanged();
|
||||||
void clearSearch();
|
void clearSearch();
|
||||||
void requestGlobalAutoType(const QString& search);
|
void requestGlobalAutoType(const QString& search);
|
||||||
|
void requestSearch(const QString& search);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
bool lock();
|
bool lock();
|
||||||
@ -176,6 +175,7 @@ public slots:
|
|||||||
void copyURL();
|
void copyURL();
|
||||||
void copyNotes();
|
void copyNotes();
|
||||||
void copyAttribute(QAction* action);
|
void copyAttribute(QAction* action);
|
||||||
|
void filterByTag(const QModelIndex& index);
|
||||||
void showTotp();
|
void showTotp();
|
||||||
void showTotpKeyQrCode();
|
void showTotpKeyQrCode();
|
||||||
void copyTotp();
|
void copyTotp();
|
||||||
@ -267,6 +267,7 @@ private:
|
|||||||
|
|
||||||
QPointer<QWidget> m_mainWidget;
|
QPointer<QWidget> m_mainWidget;
|
||||||
QPointer<QSplitter> m_mainSplitter;
|
QPointer<QSplitter> m_mainSplitter;
|
||||||
|
QPointer<QSplitter> m_groupSplitter;
|
||||||
QPointer<MessageWidget> m_messageWidget;
|
QPointer<MessageWidget> m_messageWidget;
|
||||||
QPointer<EntryPreviewWidget> m_previewView;
|
QPointer<EntryPreviewWidget> m_previewView;
|
||||||
QPointer<QSplitter> m_previewSplitter;
|
QPointer<QSplitter> m_previewSplitter;
|
||||||
@ -282,6 +283,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<EntryView> m_entryView;
|
QPointer<EntryView> m_entryView;
|
||||||
|
|
||||||
QScopedPointer<Group> m_newGroup;
|
QScopedPointer<Group> m_newGroup;
|
||||||
|
@ -26,8 +26,10 @@ DatabaseWidgetStateSync::DatabaseWidgetStateSync(QObject* parent)
|
|||||||
, m_activeDbWidget(nullptr)
|
, m_activeDbWidget(nullptr)
|
||||||
, m_blockUpdates(false)
|
, m_blockUpdates(false)
|
||||||
{
|
{
|
||||||
m_mainSplitterSizes = variantToIntList(config()->get(Config::GUI_SplitterState));
|
m_splitterSizes = {
|
||||||
m_previewSplitterSizes = variantToIntList(config()->get(Config::GUI_PreviewSplitterState));
|
{Config::GUI_SplitterState, variantToIntList(config()->get(Config::GUI_SplitterState))},
|
||||||
|
{Config::GUI_PreviewSplitterState, variantToIntList(config()->get(Config::GUI_PreviewSplitterState))},
|
||||||
|
{Config::GUI_GroupSplitterState, variantToIntList(config()->get(Config::GUI_GroupSplitterState))}};
|
||||||
m_listViewState = config()->get(Config::GUI_ListViewState).toByteArray();
|
m_listViewState = config()->get(Config::GUI_ListViewState).toByteArray();
|
||||||
m_searchViewState = config()->get(Config::GUI_SearchViewState).toByteArray();
|
m_searchViewState = config()->get(Config::GUI_SearchViewState).toByteArray();
|
||||||
|
|
||||||
@ -43,8 +45,11 @@ DatabaseWidgetStateSync::~DatabaseWidgetStateSync()
|
|||||||
*/
|
*/
|
||||||
void DatabaseWidgetStateSync::sync()
|
void DatabaseWidgetStateSync::sync()
|
||||||
{
|
{
|
||||||
config()->set(Config::GUI_SplitterState, intListToVariant(m_mainSplitterSizes));
|
config()->set(Config::GUI_SplitterState, intListToVariant(m_splitterSizes.value(Config::GUI_SplitterState)));
|
||||||
config()->set(Config::GUI_PreviewSplitterState, intListToVariant(m_previewSplitterSizes));
|
config()->set(Config::GUI_PreviewSplitterState,
|
||||||
|
intListToVariant(m_splitterSizes.value(Config::GUI_PreviewSplitterState)));
|
||||||
|
config()->set(Config::GUI_GroupSplitterState,
|
||||||
|
intListToVariant(m_splitterSizes.value(Config::GUI_GroupSplitterState)));
|
||||||
config()->set(Config::GUI_ListViewState, m_listViewState);
|
config()->set(Config::GUI_ListViewState, m_listViewState);
|
||||||
config()->set(Config::GUI_SearchViewState, m_searchViewState);
|
config()->set(Config::GUI_SearchViewState, m_searchViewState);
|
||||||
config()->sync();
|
config()->sync();
|
||||||
@ -61,13 +66,7 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget)
|
|||||||
if (m_activeDbWidget) {
|
if (m_activeDbWidget) {
|
||||||
m_blockUpdates = true;
|
m_blockUpdates = true;
|
||||||
|
|
||||||
if (!m_mainSplitterSizes.isEmpty()) {
|
m_activeDbWidget->setSplitterSizes(m_splitterSizes);
|
||||||
m_activeDbWidget->setMainSplitterSizes(m_mainSplitterSizes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_previewSplitterSizes.isEmpty()) {
|
|
||||||
m_activeDbWidget->setPreviewSplitterSizes(m_previewSplitterSizes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_activeDbWidget->isSearchActive()) {
|
if (m_activeDbWidget->isSearchActive()) {
|
||||||
restoreSearchView();
|
restoreSearchView();
|
||||||
@ -77,8 +76,7 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget)
|
|||||||
|
|
||||||
m_blockUpdates = false;
|
m_blockUpdates = false;
|
||||||
|
|
||||||
connect(m_activeDbWidget, SIGNAL(mainSplitterSizesChanged()), SLOT(updateSplitterSizes()));
|
connect(m_activeDbWidget, SIGNAL(splitterSizesChanged()), SLOT(updateSplitterSizes()));
|
||||||
connect(m_activeDbWidget, SIGNAL(previewSplitterSizesChanged()), SLOT(updateSplitterSizes()));
|
|
||||||
connect(m_activeDbWidget, SIGNAL(entryViewStateChanged()), SLOT(updateViewState()));
|
connect(m_activeDbWidget, SIGNAL(entryViewStateChanged()), SLOT(updateViewState()));
|
||||||
connect(m_activeDbWidget, SIGNAL(listModeActivated()), SLOT(restoreListView()));
|
connect(m_activeDbWidget, SIGNAL(listModeActivated()), SLOT(restoreListView()));
|
||||||
connect(m_activeDbWidget, SIGNAL(searchModeActivated()), SLOT(restoreSearchView()));
|
connect(m_activeDbWidget, SIGNAL(searchModeActivated()), SLOT(restoreSearchView()));
|
||||||
@ -138,12 +136,9 @@ void DatabaseWidgetStateSync::blockUpdates()
|
|||||||
|
|
||||||
void DatabaseWidgetStateSync::updateSplitterSizes()
|
void DatabaseWidgetStateSync::updateSplitterSizes()
|
||||||
{
|
{
|
||||||
if (m_blockUpdates) {
|
if (!m_blockUpdates) {
|
||||||
return;
|
m_splitterSizes = m_activeDbWidget->splitterSizes();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_mainSplitterSizes = m_activeDbWidget->mainSplitterSizes();
|
|
||||||
m_previewSplitterSizes = m_activeDbWidget->previewSplitterSizes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,8 +48,7 @@ private:
|
|||||||
QPointer<DatabaseWidget> m_activeDbWidget;
|
QPointer<DatabaseWidget> m_activeDbWidget;
|
||||||
|
|
||||||
bool m_blockUpdates;
|
bool m_blockUpdates;
|
||||||
QList<int> m_mainSplitterSizes;
|
QHash<Config::ConfigKey, QList<int>> m_splitterSizes;
|
||||||
QList<int> m_previewSplitterSizes;
|
|
||||||
|
|
||||||
QByteArray m_listViewState;
|
QByteArray m_listViewState;
|
||||||
QByteArray m_searchViewState;
|
QByteArray m_searchViewState;
|
||||||
|
@ -284,6 +284,8 @@ void EntryPreviewWidget::updateEntryGeneralTab()
|
|||||||
const QString expires =
|
const QString expires =
|
||||||
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
entryTime.expires() ? entryTime.expiryTime().toLocalTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
|
||||||
m_ui->entryExpirationLabel->setText(expires);
|
m_ui->entryExpirationLabel->setText(expires);
|
||||||
|
m_ui->entryTagsList->tags(m_currentEntry->tagList());
|
||||||
|
m_ui->entryTagsList->setReadOnly(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryPreviewWidget::updateEntryAdvancedTab()
|
void EntryPreviewWidget::updateEntryAdvancedTab()
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>566</width>
|
<width>596</width>
|
||||||
<height>247</height>
|
<height>261</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||||
@ -171,7 +171,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QTabWidget" name="entryTabWidget">
|
<widget class="QTabWidget" name="entryTabWidget">
|
||||||
<property name="currentIndex">
|
<property name="currentIndex">
|
||||||
<number>1</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="documentMode">
|
<property name="documentMode">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -386,6 +386,35 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="5" alignment="Qt::AlignTop">
|
||||||
|
<widget class="QLabel" name="entryTagsTitleLabel">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Tags</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="6" alignment="Qt::AlignTop">
|
||||||
|
<widget class="TagsEdit" name="entryTagsList" native="true">
|
||||||
|
<property name="accessibleName">
|
||||||
|
<string>Tags list</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<spacer name="entryLeftHorizontalSpacer_5">
|
<spacer name="entryLeftHorizontalSpacer_5">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
@ -418,7 +447,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="2" colspan="5">
|
<item row="2" column="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,0">
|
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,0">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>6</number>
|
<number>6</number>
|
||||||
@ -1183,6 +1212,11 @@
|
|||||||
<extends>QLabel</extends>
|
<extends>QLabel</extends>
|
||||||
<header>gui/widgets/ElidedLabel.h</header>
|
<header>gui/widgets/ElidedLabel.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>TagsEdit</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>gui/tag/TagsEdit.h</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>entryTotpButton</tabstop>
|
<tabstop>entryTotpButton</tabstop>
|
||||||
|
@ -1337,7 +1337,11 @@ bool MainWindow::focusNextPrevChild(bool next)
|
|||||||
// Search Widget <-> Tab Widget <-> DbWidget
|
// Search Widget <-> Tab Widget <-> DbWidget
|
||||||
if (next) {
|
if (next) {
|
||||||
if (m_searchWidget->hasFocus()) {
|
if (m_searchWidget->hasFocus()) {
|
||||||
m_ui->tabWidget->setFocus(Qt::TabFocusReason);
|
if (m_ui->tabWidget->count() > 1) {
|
||||||
|
m_ui->tabWidget->setFocus(Qt::TabFocusReason);
|
||||||
|
} else {
|
||||||
|
dbWidget->setFocus(Qt::TabFocusReason);
|
||||||
|
}
|
||||||
} else if (m_ui->tabWidget->hasFocus()) {
|
} else if (m_ui->tabWidget->hasFocus()) {
|
||||||
dbWidget->setFocus(Qt::TabFocusReason);
|
dbWidget->setFocus(Qt::TabFocusReason);
|
||||||
} else {
|
} else {
|
||||||
@ -1349,7 +1353,11 @@ bool MainWindow::focusNextPrevChild(bool next)
|
|||||||
} else if (m_ui->tabWidget->hasFocus()) {
|
} else if (m_ui->tabWidget->hasFocus()) {
|
||||||
focusSearchWidget();
|
focusSearchWidget();
|
||||||
} else {
|
} else {
|
||||||
m_ui->tabWidget->setFocus(Qt::BacktabFocusReason);
|
if (m_ui->tabWidget->count() > 1) {
|
||||||
|
m_ui->tabWidget->setFocus(Qt::BacktabFocusReason);
|
||||||
|
} else {
|
||||||
|
focusSearchWidget();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -130,6 +130,7 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
|||||||
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()));
|
||||||
mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
|
mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
|
||||||
|
mx.connect(SIGNAL(requestSearch(QString)), m_ui->searchEdit, SLOT(setText(QString)));
|
||||||
mx.connect(SIGNAL(clearSearch()), this, SLOT(clearSearch()));
|
mx.connect(SIGNAL(clearSearch()), this, SLOT(clearSearch()));
|
||||||
mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
|
mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
|
||||||
mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
|
mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
|
||||||
|
@ -855,6 +855,8 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
|||||||
m_mainUi->usernameComboBox->lineEdit()->setReadOnly(m_history);
|
m_mainUi->usernameComboBox->lineEdit()->setReadOnly(m_history);
|
||||||
m_mainUi->urlEdit->setReadOnly(m_history);
|
m_mainUi->urlEdit->setReadOnly(m_history);
|
||||||
m_mainUi->passwordEdit->setReadOnly(m_history);
|
m_mainUi->passwordEdit->setReadOnly(m_history);
|
||||||
|
m_mainUi->tagsList->tags(entry->tagList());
|
||||||
|
m_mainUi->tagsList->completion(m_db->tagList());
|
||||||
m_mainUi->expireCheck->setEnabled(!m_history);
|
m_mainUi->expireCheck->setEnabled(!m_history);
|
||||||
m_mainUi->expireDatePicker->setReadOnly(m_history);
|
m_mainUi->expireDatePicker->setReadOnly(m_history);
|
||||||
m_mainUi->notesEnabled->setChecked(!config()->get(Config::Security_HideNotes).toBool());
|
m_mainUi->notesEnabled->setChecked(!config()->get(Config::Security_HideNotes).toBool());
|
||||||
@ -1160,6 +1162,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
|
|||||||
entry->setPassword(m_mainUi->passwordEdit->text());
|
entry->setPassword(m_mainUi->passwordEdit->text());
|
||||||
entry->setExpires(m_mainUi->expireCheck->isChecked());
|
entry->setExpires(m_mainUi->expireCheck->isChecked());
|
||||||
entry->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC());
|
entry->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC());
|
||||||
|
entry->setTags(m_mainUi->tagsList->tags().toSet().toList().join(";")); // remove repeated tags
|
||||||
|
|
||||||
entry->setNotes(m_mainUi->notesEdit->toPlainText());
|
entry->setNotes(m_mainUi->notesEdit->toPlainText());
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
<property name="verticalSpacing">
|
<property name="verticalSpacing">
|
||||||
<number>8</number>
|
<number>8</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="6" column="1">
|
<item row="7" column="1">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPlainTextEdit" name="notesEdit">
|
<widget class="QPlainTextEdit" name="notesEdit">
|
||||||
@ -99,7 +99,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="notesEnabled">
|
<widget class="QCheckBox" name="notesEnabled">
|
||||||
@ -129,7 +129,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="6" column="1">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>8</number>
|
<number>8</number>
|
||||||
@ -252,7 +252,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="6" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
@ -272,6 +272,26 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="tagsLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Tags:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<widget class="TagsEdit" name="tagsList" native="true">
|
||||||
|
<property name="focusPolicy">
|
||||||
|
<enum>Qt::StrongFocus</enum>
|
||||||
|
</property>
|
||||||
|
<property name="accessibleName">
|
||||||
|
<string>Tags list</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
@ -288,12 +308,19 @@
|
|||||||
<header>gui/URLEdit.h</header>
|
<header>gui/URLEdit.h</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>TagsEdit</class>
|
||||||
|
<extends>QAbstractScrollArea</extends>
|
||||||
|
<header>gui/tag/TagsEdit.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>titleEdit</tabstop>
|
<tabstop>titleEdit</tabstop>
|
||||||
<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>expireCheck</tabstop>
|
<tabstop>expireCheck</tabstop>
|
||||||
<tabstop>expireDatePicker</tabstop>
|
<tabstop>expireDatePicker</tabstop>
|
||||||
|
@ -73,3 +73,7 @@ QPlainTextEdit, QTextEdit {
|
|||||||
QStatusBar {
|
QStatusBar {
|
||||||
background-color: palette(window);
|
background-color: palette(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*[title="true"] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@ -17,3 +17,7 @@ DatabaseWidget #SearchBanner, DatabaseWidget #KeeShareBanner {
|
|||||||
QLineEdit {
|
QLineEdit {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*[title="true"] {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
89
src/gui/tag/TagModel.cpp
Normal file
89
src/gui/tag/TagModel.cpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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 "TagModel.h"
|
||||||
|
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "gui/Icons.h"
|
||||||
|
|
||||||
|
TagModel::TagModel(QSharedPointer<Database> db, QObject* parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
setDatabase(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagModel::~TagModel()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagModel::setDatabase(QSharedPointer<Database> db)
|
||||||
|
{
|
||||||
|
m_db = db;
|
||||||
|
if (!m_db) {
|
||||||
|
m_tagList.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connect(m_db.data(), SIGNAL(tagListUpdated()), SLOT(updateTagList()));
|
||||||
|
updateTagList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagModel::updateTagList()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_tagList.clear();
|
||||||
|
m_tagList << tr("All") << tr("Expired") << tr("Weak Passwords") << m_db->tagList();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
int TagModel::rowCount(const QModelIndex& parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent);
|
||||||
|
return m_tagList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant TagModel::data(const QModelIndex& index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid() || index.row() >= m_tagList.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case Qt::DecorationRole:
|
||||||
|
if (index.row() <= 2) {
|
||||||
|
return icons()->icon("tag-search");
|
||||||
|
}
|
||||||
|
return icons()->icon("tag");
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
return m_tagList.at(index.row());
|
||||||
|
case Qt::UserRole:
|
||||||
|
if (index.row() == 0) {
|
||||||
|
return "";
|
||||||
|
} else if (index.row() == 1) {
|
||||||
|
return "is:expired";
|
||||||
|
} else if (index.row() == 2) {
|
||||||
|
return "is:weak";
|
||||||
|
}
|
||||||
|
return QString("tag:%1").arg(m_tagList.at(index.row()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList& TagModel::tags() const
|
||||||
|
{
|
||||||
|
return m_tagList;
|
||||||
|
}
|
48
src/gui/tag/TagModel.h
Normal file
48
src/gui/tag/TagModel.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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 KEEPASSX_TAGMODEL_H
|
||||||
|
#define KEEPASSX_TAGMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
|
||||||
|
class TagModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TagModel(QSharedPointer<Database> db, QObject* parent = nullptr);
|
||||||
|
~TagModel() override;
|
||||||
|
|
||||||
|
void setDatabase(QSharedPointer<Database> db);
|
||||||
|
const QStringList& tags() const;
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void updateTagList();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSharedPointer<Database> m_db;
|
||||||
|
QStringList m_tagList;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_TAGMODEL_H
|
963
src/gui/tag/TagsEdit.cpp
Normal file
963
src/gui/tag/TagsEdit.cpp
Normal file
@ -0,0 +1,963 @@
|
|||||||
|
/*
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Nicolai Trandafil
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TagsEdit.h"
|
||||||
|
#include "gui/MainWindow.h"
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QCompleter>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QStyleHints>
|
||||||
|
#include <QStyleOptionFrame>
|
||||||
|
#include <QTextLayout>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
|
||||||
|
#define FONT_METRICS_WIDTH(fmt, ...) fmt.width(__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define FONT_METRICS_WIDTH(fmt, ...) fmt.horizontalAdvance(__VA_ARGS__)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
constexpr int tag_v_spacing = 2;
|
||||||
|
constexpr int tag_h_spacing = 3;
|
||||||
|
|
||||||
|
constexpr QMargins tag_inner(5, 3, 4, 3);
|
||||||
|
|
||||||
|
constexpr int tag_cross_width = 5;
|
||||||
|
constexpr float tag_cross_radius = tag_cross_width / 2;
|
||||||
|
constexpr int tag_cross_padding = 5;
|
||||||
|
|
||||||
|
struct Tag
|
||||||
|
{
|
||||||
|
bool isEmpty() const noexcept
|
||||||
|
{
|
||||||
|
return text.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
QRect rect;
|
||||||
|
size_t row;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Non empty string filtering iterator
|
||||||
|
template <class It> struct EmptySkipIterator
|
||||||
|
{
|
||||||
|
EmptySkipIterator() = default;
|
||||||
|
|
||||||
|
// skip until `end`
|
||||||
|
explicit EmptySkipIterator(It it, It end)
|
||||||
|
: it(it)
|
||||||
|
, end(end)
|
||||||
|
{
|
||||||
|
while (this->it != end && this->it->isEmpty()) {
|
||||||
|
++this->it;
|
||||||
|
}
|
||||||
|
begin = it;
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit EmptySkipIterator(It it)
|
||||||
|
: it(it)
|
||||||
|
, end{}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
using difference_type = typename std::iterator_traits<It>::difference_type;
|
||||||
|
using value_type = typename std::iterator_traits<It>::value_type;
|
||||||
|
using pointer = typename std::iterator_traits<It>::pointer;
|
||||||
|
using reference = typename std::iterator_traits<It>::reference;
|
||||||
|
using iterator_category = std::output_iterator_tag;
|
||||||
|
|
||||||
|
EmptySkipIterator& operator++()
|
||||||
|
{
|
||||||
|
assert(it != end);
|
||||||
|
while (++it != end && it->isEmpty())
|
||||||
|
;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
decltype(auto) operator*()
|
||||||
|
{
|
||||||
|
return *it;
|
||||||
|
}
|
||||||
|
|
||||||
|
pointer operator->()
|
||||||
|
{
|
||||||
|
return &(*it);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(EmptySkipIterator const& rhs) const
|
||||||
|
{
|
||||||
|
return it != rhs.it;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(EmptySkipIterator const& rhs) const
|
||||||
|
{
|
||||||
|
return it == rhs.it;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
It begin;
|
||||||
|
It it;
|
||||||
|
It end;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class It> EmptySkipIterator(It, It) -> EmptySkipIterator<It>;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Invariant-1 ensures no empty tags apart from currently being edited.
|
||||||
|
// Default-state is one empty tag which is currently editing.
|
||||||
|
struct TagsEdit::Impl
|
||||||
|
{
|
||||||
|
explicit Impl(TagsEdit* ifce)
|
||||||
|
: ifce(ifce)
|
||||||
|
, tags{Tag()}
|
||||||
|
, editing_index(0)
|
||||||
|
, cursor(0)
|
||||||
|
, blink_timer(0)
|
||||||
|
, blink_status(true)
|
||||||
|
, select_start(0)
|
||||||
|
, select_size(0)
|
||||||
|
, cross_deleter(true)
|
||||||
|
, completer(std::make_unique<QCompleter>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
inline QRectF crossRect(QRectF const& r) const
|
||||||
|
{
|
||||||
|
QRectF cross(QPointF{0, 0}, QSizeF{tag_cross_width + tag_cross_padding * 2, r.top() - r.bottom()});
|
||||||
|
cross.moveCenter(QPointF(r.right() - tag_cross_radius - tag_cross_padding, r.center().y()));
|
||||||
|
return cross;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inCrossArea(int tag_index, QPoint point) const
|
||||||
|
{
|
||||||
|
return cross_deleter
|
||||||
|
? crossRect(tags[tag_index].rect)
|
||||||
|
.adjusted(-tag_cross_radius, 0, 0, 0)
|
||||||
|
.translated(-ifce->horizontalScrollBar()->value(), -ifce->verticalScrollBar()->value())
|
||||||
|
.contains(point)
|
||||||
|
&& (!cursorVisible() || tag_index != editing_index)
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class It> void drawTags(QPainter& p, std::pair<It, It> range) const
|
||||||
|
{
|
||||||
|
for (auto it = range.first; it != range.second; ++it) {
|
||||||
|
QRect const& i_r =
|
||||||
|
it->rect.translated(-ifce->horizontalScrollBar()->value(), -ifce->verticalScrollBar()->value());
|
||||||
|
auto const text_pos =
|
||||||
|
i_r.topLeft()
|
||||||
|
+ QPointF(tag_inner.left(),
|
||||||
|
ifce->fontMetrics().ascent() + ((i_r.height() - ifce->fontMetrics().height()) / 2));
|
||||||
|
|
||||||
|
// draw tag rect
|
||||||
|
auto palette = getMainWindow()->palette();
|
||||||
|
QPainterPath path;
|
||||||
|
auto cornerRadius = 4;
|
||||||
|
path.addRoundedRect(i_r, cornerRadius, cornerRadius);
|
||||||
|
p.fillPath(path, palette.brush(QPalette::ColorGroup::Inactive, QPalette::ColorRole::Highlight));
|
||||||
|
|
||||||
|
// draw text
|
||||||
|
p.drawText(text_pos, it->text);
|
||||||
|
|
||||||
|
if (cross_deleter) {
|
||||||
|
// calc cross rect
|
||||||
|
auto const i_cross_r = crossRect(i_r);
|
||||||
|
|
||||||
|
QPainterPath crossRectBg1, crossRectBg2;
|
||||||
|
crossRectBg1.addRoundedRect(i_cross_r, cornerRadius, cornerRadius);
|
||||||
|
// cover left rounded corners
|
||||||
|
crossRectBg2.addRect(
|
||||||
|
i_cross_r.left(), i_cross_r.bottom(), tag_cross_radius, i_cross_r.top() - i_cross_r.bottom());
|
||||||
|
p.fillPath(crossRectBg1, palette.highlight());
|
||||||
|
p.fillPath(crossRectBg2, palette.highlight());
|
||||||
|
|
||||||
|
QPen pen = p.pen();
|
||||||
|
pen.setWidth(2);
|
||||||
|
pen.setBrush(palette.highlightedText());
|
||||||
|
|
||||||
|
p.save();
|
||||||
|
p.setPen(pen);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing);
|
||||||
|
p.drawLine(QLineF(i_cross_r.center() - QPointF(tag_cross_radius, tag_cross_radius),
|
||||||
|
i_cross_r.center() + QPointF(tag_cross_radius, tag_cross_radius)));
|
||||||
|
p.drawLine(QLineF(i_cross_r.center() - QPointF(-tag_cross_radius, tag_cross_radius),
|
||||||
|
i_cross_r.center() + QPointF(-tag_cross_radius, tag_cross_radius)));
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect contentsRect() const
|
||||||
|
{
|
||||||
|
return ifce->viewport()->contentsRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect calcRects(QList<Tag>& tags) const
|
||||||
|
{
|
||||||
|
return calcRects(tags, contentsRect());
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect calcRects(QList<Tag>& tags, QRect r) const
|
||||||
|
{
|
||||||
|
size_t row = 0;
|
||||||
|
auto lt = r.topLeft();
|
||||||
|
QFontMetrics fm = ifce->fontMetrics();
|
||||||
|
|
||||||
|
auto const b = std::begin(tags);
|
||||||
|
auto const e = std::end(tags);
|
||||||
|
if (cursorVisible()) {
|
||||||
|
auto const m = b + static_cast<std::ptrdiff_t>(editing_index);
|
||||||
|
calcRects(lt, row, r, fm, std::make_pair(b, m));
|
||||||
|
calcEditorRect(lt, row, r, fm, m);
|
||||||
|
calcRects(lt, row, r, fm, std::make_pair(m + 1, e));
|
||||||
|
} else {
|
||||||
|
calcRects(lt, row, r, fm, std::make_pair(b, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
r.setBottom(lt.y() + fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom() - 1);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class It>
|
||||||
|
void calcRects(QPoint& lt, size_t& row, QRect r, QFontMetrics const& fm, std::pair<It, It> range) const
|
||||||
|
{
|
||||||
|
for (auto it = range.first; it != range.second; ++it) {
|
||||||
|
// calc text rect
|
||||||
|
const auto text_w = FONT_METRICS_WIDTH(fm, it->text);
|
||||||
|
auto const text_h = fm.height() + fm.leading();
|
||||||
|
auto const w = cross_deleter
|
||||||
|
? tag_inner.left() + tag_inner.right() + tag_cross_padding * 2 + tag_cross_width
|
||||||
|
: tag_inner.left() + tag_inner.right();
|
||||||
|
auto const h = tag_inner.top() + tag_inner.bottom();
|
||||||
|
QRect i_r(lt, QSize(text_w + w, text_h + h));
|
||||||
|
|
||||||
|
// line wrapping
|
||||||
|
if (r.right() < i_r.right() && // doesn't fit in current line
|
||||||
|
i_r.left() != r.left() // doesn't occupy entire line already
|
||||||
|
) {
|
||||||
|
i_r.moveTo(r.left(), i_r.bottom() + tag_v_spacing);
|
||||||
|
++row;
|
||||||
|
lt = i_r.topLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
it->rect = i_r;
|
||||||
|
it->row = row;
|
||||||
|
lt.setX(i_r.right() + tag_h_spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class It> void calcEditorRect(QPoint& lt, size_t& row, QRect r, QFontMetrics const& fm, It it) const
|
||||||
|
{
|
||||||
|
auto const text_w = FONT_METRICS_WIDTH(fm, text_layout.text());
|
||||||
|
auto const text_h = fm.height() + fm.leading();
|
||||||
|
auto const w = tag_inner.left() + tag_inner.right();
|
||||||
|
auto const h = tag_inner.top() + tag_inner.bottom();
|
||||||
|
QRect i_r(lt, QSize(text_w + w, text_h + h));
|
||||||
|
|
||||||
|
// line wrapping
|
||||||
|
if (r.right() < i_r.right() && // doesn't fit in current line
|
||||||
|
i_r.left() != r.left() // doesn't occupy entire line already
|
||||||
|
) {
|
||||||
|
i_r.moveTo(r.left(), i_r.bottom() + tag_v_spacing);
|
||||||
|
++row;
|
||||||
|
lt = i_r.topLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
it->rect = i_r;
|
||||||
|
it->row = row;
|
||||||
|
lt.setX(i_r.right() + tag_h_spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCursorVisible(bool visible)
|
||||||
|
{
|
||||||
|
if (blink_timer) {
|
||||||
|
ifce->killTimer(blink_timer);
|
||||||
|
blink_timer = 0;
|
||||||
|
blink_status = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
int flashTime = QGuiApplication::styleHints()->cursorFlashTime();
|
||||||
|
if (flashTime >= 2) {
|
||||||
|
blink_timer = ifce->startTimer(flashTime / 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
blink_status = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cursorVisible() const
|
||||||
|
{
|
||||||
|
return blink_timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorBlinking()
|
||||||
|
{
|
||||||
|
setCursorVisible(cursorVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDisplayText()
|
||||||
|
{
|
||||||
|
text_layout.clearLayout();
|
||||||
|
text_layout.setText(currentText());
|
||||||
|
text_layout.beginLayout();
|
||||||
|
text_layout.createLine();
|
||||||
|
text_layout.endLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes the tag at `i` currently editing, and ensures Invariant-1`.
|
||||||
|
void setEditingIndex(int i)
|
||||||
|
{
|
||||||
|
assert(i < tags.size());
|
||||||
|
auto occurrencesOfCurrentText =
|
||||||
|
std::count_if(tags.cbegin(), tags.cend(), [this](const auto& tag) { return tag.text == currentText(); });
|
||||||
|
if (currentText().isEmpty() || occurrencesOfCurrentText > 1) {
|
||||||
|
tags.erase(std::next(tags.begin(), std::ptrdiff_t(editing_index)));
|
||||||
|
if (editing_index <= i) { // Do we shift positions after `i`?
|
||||||
|
--i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editing_index = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
void calcRectsAndUpdateScrollRanges()
|
||||||
|
{
|
||||||
|
auto const row = tags.back().row;
|
||||||
|
auto const max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
|
||||||
|
return x.rect.width() < y.rect.width();
|
||||||
|
})->rect.width();
|
||||||
|
|
||||||
|
calcRects(tags);
|
||||||
|
|
||||||
|
if (row != tags.back().row) {
|
||||||
|
updateVScrollRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const new_max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
|
||||||
|
return x.rect.width() < y.rect.width();
|
||||||
|
})->rect.width();
|
||||||
|
|
||||||
|
if (max_width != new_max_width) {
|
||||||
|
updateHScrollRange(new_max_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void currentText(QString const& text)
|
||||||
|
{
|
||||||
|
currentText() = text;
|
||||||
|
moveCursor(currentText().length(), false);
|
||||||
|
updateDisplayText();
|
||||||
|
calcRectsAndUpdateScrollRanges();
|
||||||
|
ifce->viewport()->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString const& currentText() const
|
||||||
|
{
|
||||||
|
return tags[editing_index].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString& currentText()
|
||||||
|
{
|
||||||
|
return tags[editing_index].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect const& currentRect() const
|
||||||
|
{
|
||||||
|
return tags[editing_index].rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserts a new tag at `i`, makes the tag currently editing,
|
||||||
|
// and ensures Invariant-1.
|
||||||
|
void editNewTag(int i)
|
||||||
|
{
|
||||||
|
tags.insert(std::next(std::begin(tags), static_cast<std::ptrdiff_t>(i)), Tag());
|
||||||
|
if (editing_index >= i) {
|
||||||
|
++editing_index;
|
||||||
|
}
|
||||||
|
setEditingIndex(i);
|
||||||
|
moveCursor(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupCompleter()
|
||||||
|
{
|
||||||
|
completer->setWidget(ifce);
|
||||||
|
connect(completer.get(), qOverload<QString const&>(&QCompleter::activated), [this](QString const& text) {
|
||||||
|
currentText(text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QTextLayout::FormatRange> formatting() const
|
||||||
|
{
|
||||||
|
if (select_size == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextLayout::FormatRange selection;
|
||||||
|
selection.start = select_start;
|
||||||
|
selection.length = select_size;
|
||||||
|
selection.format.setBackground(ifce->palette().brush(QPalette::Highlight));
|
||||||
|
selection.format.setForeground(ifce->palette().brush(QPalette::HighlightedText));
|
||||||
|
return {selection};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasSelection() const noexcept
|
||||||
|
{
|
||||||
|
return select_size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeSelection()
|
||||||
|
{
|
||||||
|
cursor = select_start;
|
||||||
|
currentText().remove(cursor, select_size);
|
||||||
|
deselectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeBackwardOne()
|
||||||
|
{
|
||||||
|
if (hasSelection()) {
|
||||||
|
removeSelection();
|
||||||
|
} else {
|
||||||
|
currentText().remove(--cursor, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectAll()
|
||||||
|
{
|
||||||
|
select_start = 0;
|
||||||
|
select_size = currentText().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deselectAll()
|
||||||
|
{
|
||||||
|
select_start = 0;
|
||||||
|
select_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveCursor(int pos, bool mark)
|
||||||
|
{
|
||||||
|
if (mark) {
|
||||||
|
auto e = select_start + select_size;
|
||||||
|
int anchor = select_size > 0 && cursor == select_start ? e
|
||||||
|
: select_size > 0 && cursor == e ? select_start
|
||||||
|
: cursor;
|
||||||
|
select_start = qMin(anchor, pos);
|
||||||
|
select_size = qMax(anchor, pos) - select_start;
|
||||||
|
} else {
|
||||||
|
deselectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal cursorToX()
|
||||||
|
{
|
||||||
|
return text_layout.lineAt(0).cursorToX(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void editPreviousTag()
|
||||||
|
{
|
||||||
|
if (editing_index > 0) {
|
||||||
|
setEditingIndex(editing_index - 1);
|
||||||
|
moveCursor(currentText().size(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void editNextTag()
|
||||||
|
{
|
||||||
|
if (editing_index < tags.size() - 1) {
|
||||||
|
setEditingIndex(editing_index + 1);
|
||||||
|
moveCursor(0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void editTag(int i)
|
||||||
|
{
|
||||||
|
assert(i >= 0 && i < tags.size());
|
||||||
|
setEditingIndex(i);
|
||||||
|
moveCursor(currentText().size(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateVScrollRange()
|
||||||
|
{
|
||||||
|
auto fm = ifce->fontMetrics();
|
||||||
|
auto const row_h = fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom() + tag_v_spacing;
|
||||||
|
ifce->verticalScrollBar()->setPageStep(row_h);
|
||||||
|
auto const h = tags.back().rect.bottom() - tags.front().rect.top() + 1;
|
||||||
|
auto const contents_rect = contentsRect();
|
||||||
|
if (h > contents_rect.height()) {
|
||||||
|
ifce->verticalScrollBar()->setRange(0, h - contents_rect.height());
|
||||||
|
} else {
|
||||||
|
ifce->verticalScrollBar()->setRange(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateHScrollRange()
|
||||||
|
{
|
||||||
|
auto const max_width = std::max_element(std::begin(tags), std::end(tags), [](auto const& x, auto const& y) {
|
||||||
|
return x.rect.width() < y.rect.width();
|
||||||
|
})->rect.width();
|
||||||
|
updateHScrollRange(max_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateHScrollRange(int width)
|
||||||
|
{
|
||||||
|
auto const contents_rect_width = contentsRect().width();
|
||||||
|
if (width > contents_rect_width) {
|
||||||
|
ifce->horizontalScrollBar()->setRange(0, width - contents_rect_width);
|
||||||
|
} else {
|
||||||
|
ifce->horizontalScrollBar()->setRange(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureCursorIsVisibleV()
|
||||||
|
{
|
||||||
|
auto fm = ifce->fontMetrics();
|
||||||
|
auto const row_h = fm.height() + fm.leading() + tag_inner.top() + tag_inner.bottom();
|
||||||
|
auto const vscroll = ifce->verticalScrollBar()->value();
|
||||||
|
auto const cursor_top = currentRect().topLeft() + QPoint(qRound(cursorToX()), 0);
|
||||||
|
auto const cursor_bottom = cursor_top + QPoint(0, row_h - 1);
|
||||||
|
auto const contents_rect = contentsRect().translated(0, vscroll);
|
||||||
|
if (contents_rect.bottom() < cursor_bottom.y()) {
|
||||||
|
ifce->verticalScrollBar()->setValue(cursor_bottom.y() - row_h);
|
||||||
|
} else if (cursor_top.y() < contents_rect.top()) {
|
||||||
|
ifce->verticalScrollBar()->setValue(cursor_top.y() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureCursorIsVisibleH()
|
||||||
|
{
|
||||||
|
auto const hscroll = ifce->horizontalScrollBar()->value();
|
||||||
|
auto const contents_rect = contentsRect().translated(hscroll, 0);
|
||||||
|
auto const cursor_x = (currentRect() - tag_inner).left() + qRound(cursorToX());
|
||||||
|
if (contents_rect.right() < cursor_x) {
|
||||||
|
ifce->horizontalScrollBar()->setValue(cursor_x - contents_rect.width());
|
||||||
|
} else if (cursor_x < contents_rect.left()) {
|
||||||
|
ifce->horizontalScrollBar()->setValue(cursor_x - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TagsEdit* const ifce;
|
||||||
|
QList<Tag> tags;
|
||||||
|
int editing_index;
|
||||||
|
int cursor;
|
||||||
|
int blink_timer;
|
||||||
|
bool blink_status;
|
||||||
|
QTextLayout text_layout;
|
||||||
|
int select_start;
|
||||||
|
int select_size;
|
||||||
|
bool cross_deleter;
|
||||||
|
std::unique_ptr<QCompleter> completer;
|
||||||
|
int hscroll{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
TagsEdit::TagsEdit(QWidget* parent)
|
||||||
|
: QAbstractScrollArea(parent)
|
||||||
|
, impl(std::make_unique<Impl>(this))
|
||||||
|
, m_readOnly(false)
|
||||||
|
{
|
||||||
|
QSizePolicy size_policy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
|
size_policy.setHeightForWidth(true);
|
||||||
|
setSizePolicy(size_policy);
|
||||||
|
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
viewport()->setCursor(Qt::IBeamCursor);
|
||||||
|
setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||||
|
setMouseTracking(true);
|
||||||
|
|
||||||
|
impl->setupCompleter();
|
||||||
|
impl->setCursorVisible(hasFocus());
|
||||||
|
impl->updateDisplayText();
|
||||||
|
|
||||||
|
viewport()->setContentsMargins(1, 1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagsEdit::~TagsEdit() = default;
|
||||||
|
|
||||||
|
void TagsEdit::setReadOnly(bool readOnly)
|
||||||
|
{
|
||||||
|
m_readOnly = readOnly;
|
||||||
|
if (m_readOnly) {
|
||||||
|
setFocusPolicy(Qt::NoFocus);
|
||||||
|
setCursor(Qt::ArrowCursor);
|
||||||
|
setAttribute(Qt::WA_InputMethodEnabled, false);
|
||||||
|
setFrameShape(QFrame::NoFrame);
|
||||||
|
impl->cross_deleter = false;
|
||||||
|
} else {
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
setCursor(Qt::IBeamCursor);
|
||||||
|
setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||||
|
impl->cross_deleter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::resizeEvent(QResizeEvent*)
|
||||||
|
{
|
||||||
|
impl->calcRects(impl->tags);
|
||||||
|
impl->updateVScrollRange();
|
||||||
|
impl->updateHScrollRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::focusInEvent(QFocusEvent*)
|
||||||
|
{
|
||||||
|
impl->setCursorVisible(true);
|
||||||
|
impl->updateDisplayText();
|
||||||
|
impl->calcRects(impl->tags);
|
||||||
|
impl->completer->complete();
|
||||||
|
viewport()->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::focusOutEvent(QFocusEvent*)
|
||||||
|
{
|
||||||
|
impl->setCursorVisible(false);
|
||||||
|
impl->updateDisplayText();
|
||||||
|
impl->calcRects(impl->tags);
|
||||||
|
impl->completer->popup()->hide();
|
||||||
|
viewport()->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::paintEvent(QPaintEvent*)
|
||||||
|
{
|
||||||
|
QPainter p(viewport());
|
||||||
|
|
||||||
|
// clip
|
||||||
|
auto const rect = impl->contentsRect();
|
||||||
|
p.setClipRect(rect);
|
||||||
|
if (impl->cursorVisible()) {
|
||||||
|
// not terminated tag pos
|
||||||
|
auto const& r = impl->currentRect();
|
||||||
|
auto const& txt_p = r.topLeft() + QPointF(tag_inner.left(), ((r.height() - fontMetrics().height()) / 2));
|
||||||
|
|
||||||
|
// tags
|
||||||
|
impl->drawTags(
|
||||||
|
p,
|
||||||
|
std::make_pair(impl->tags.cbegin(), std::next(impl->tags.cbegin(), std::ptrdiff_t(impl->editing_index))));
|
||||||
|
|
||||||
|
// draw not terminated tag
|
||||||
|
auto const formatting = impl->formatting();
|
||||||
|
impl->text_layout.draw(
|
||||||
|
&p, txt_p - QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value()), formatting);
|
||||||
|
|
||||||
|
// draw cursor
|
||||||
|
if (impl->blink_status) {
|
||||||
|
impl->text_layout.drawCursor(
|
||||||
|
&p, txt_p - QPointF(horizontalScrollBar()->value(), verticalScrollBar()->value()), impl->cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tags
|
||||||
|
impl->drawTags(
|
||||||
|
p,
|
||||||
|
std::make_pair(std::next(impl->tags.cbegin(), std::ptrdiff_t(impl->editing_index + 1)), impl->tags.cend()));
|
||||||
|
} else {
|
||||||
|
impl->drawTags(p,
|
||||||
|
std::make_pair(EmptySkipIterator(impl->tags.begin(), impl->tags.end()),
|
||||||
|
EmptySkipIterator(impl->tags.end())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::timerEvent(QTimerEvent* event)
|
||||||
|
{
|
||||||
|
if (event->timerId() == impl->blink_timer) {
|
||||||
|
impl->blink_status = !impl->blink_status;
|
||||||
|
viewport()->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::mousePressEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
for (int i = 0; i < impl->tags.size(); ++i) {
|
||||||
|
if (impl->inCrossArea(i, event->pos())) {
|
||||||
|
impl->tags.erase(impl->tags.begin() + std::ptrdiff_t(i));
|
||||||
|
if (i <= impl->editing_index) {
|
||||||
|
--impl->editing_index;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!impl->tags[i]
|
||||||
|
.rect.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
|
||||||
|
.contains(event->pos())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl->editing_index == i) {
|
||||||
|
impl->moveCursor(impl->text_layout.lineAt(0).xToCursor(
|
||||||
|
(event->pos()
|
||||||
|
- impl->currentRect()
|
||||||
|
.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value())
|
||||||
|
.topLeft())
|
||||||
|
.x()),
|
||||||
|
false);
|
||||||
|
} else {
|
||||||
|
impl->editTag(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
for (auto it = std::begin(impl->tags); it != std::end(impl->tags); ++it) {
|
||||||
|
// Click of a row.
|
||||||
|
if (it->rect.translated(-horizontalScrollBar()->value(), -verticalScrollBar()->value()).bottom()
|
||||||
|
< event->pos().y()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last tag of the row.
|
||||||
|
auto const row = it->row;
|
||||||
|
while (it != std::end(impl->tags) && it->row == row) {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl->editNewTag(static_cast<size_t>(std::distance(std::begin(impl->tags), it)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->isAccepted()) {
|
||||||
|
impl->updateDisplayText();
|
||||||
|
impl->calcRectsAndUpdateScrollRanges();
|
||||||
|
impl->ensureCursorIsVisibleV();
|
||||||
|
impl->ensureCursorIsVisibleH();
|
||||||
|
impl->updateCursorBlinking();
|
||||||
|
viewport()->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize TagsEdit::sizeHint() const
|
||||||
|
{
|
||||||
|
return minimumSizeHint();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize TagsEdit::minimumSizeHint() const
|
||||||
|
{
|
||||||
|
ensurePolished();
|
||||||
|
QFontMetrics fm = fontMetrics();
|
||||||
|
QRect rect(0, 0, fm.maxWidth() + tag_cross_padding + tag_cross_width, fm.height() + fm.leading());
|
||||||
|
rect += tag_inner + contentsMargins() + viewport()->contentsMargins() + viewportMargins();
|
||||||
|
return rect.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int TagsEdit::heightForWidth(int w) const
|
||||||
|
{
|
||||||
|
auto const content_width = w;
|
||||||
|
QRect contents_rect(0, 0, content_width, 100);
|
||||||
|
contents_rect -= contentsMargins() + viewport()->contentsMargins() + viewportMargins();
|
||||||
|
auto tags = impl->tags;
|
||||||
|
contents_rect = impl->calcRects(tags, contents_rect);
|
||||||
|
contents_rect += contentsMargins() + viewport()->contentsMargins() + viewportMargins();
|
||||||
|
return contents_rect.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::keyPressEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
event->setAccepted(false);
|
||||||
|
bool unknown = false;
|
||||||
|
|
||||||
|
if (event == QKeySequence::SelectAll) {
|
||||||
|
impl->selectAll();
|
||||||
|
event->accept();
|
||||||
|
} else if (event == QKeySequence::SelectPreviousChar) {
|
||||||
|
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), true);
|
||||||
|
event->accept();
|
||||||
|
} else if (event == QKeySequence::SelectNextChar) {
|
||||||
|
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), true);
|
||||||
|
event->accept();
|
||||||
|
} else {
|
||||||
|
switch (event->key()) {
|
||||||
|
case Qt::Key_Left:
|
||||||
|
if (impl->cursor == 0) {
|
||||||
|
impl->editPreviousTag();
|
||||||
|
} else {
|
||||||
|
impl->moveCursor(impl->text_layout.previousCursorPosition(impl->cursor), false);
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Right:
|
||||||
|
if (impl->cursor == impl->currentText().size()) {
|
||||||
|
impl->editNextTag();
|
||||||
|
} else {
|
||||||
|
impl->moveCursor(impl->text_layout.nextCursorPosition(impl->cursor), false);
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Home:
|
||||||
|
if (impl->cursor == 0) {
|
||||||
|
impl->editTag(0);
|
||||||
|
} else {
|
||||||
|
impl->moveCursor(0, false);
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
case Qt::Key_End:
|
||||||
|
if (impl->cursor == impl->currentText().size()) {
|
||||||
|
impl->editTag(impl->tags.size() - 1);
|
||||||
|
} else {
|
||||||
|
impl->moveCursor(impl->currentText().length(), false);
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Backspace:
|
||||||
|
if (!impl->currentText().isEmpty()) {
|
||||||
|
impl->removeBackwardOne();
|
||||||
|
} else if (impl->editing_index > 0) {
|
||||||
|
impl->editPreviousTag();
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
case Qt::Key_Space:
|
||||||
|
if (!impl->currentText().isEmpty()) {
|
||||||
|
impl->editNewTag(impl->editing_index + 1);
|
||||||
|
}
|
||||||
|
event->accept();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
unknown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknown && isAcceptableInput(event)) {
|
||||||
|
if (impl->hasSelection()) {
|
||||||
|
impl->removeSelection();
|
||||||
|
}
|
||||||
|
impl->currentText().insert(impl->cursor, event->text());
|
||||||
|
impl->cursor = impl->cursor + event->text().length();
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->isAccepted()) {
|
||||||
|
// update content
|
||||||
|
impl->updateDisplayText();
|
||||||
|
impl->calcRectsAndUpdateScrollRanges();
|
||||||
|
impl->ensureCursorIsVisibleV();
|
||||||
|
impl->ensureCursorIsVisibleH();
|
||||||
|
impl->updateCursorBlinking();
|
||||||
|
|
||||||
|
// complete
|
||||||
|
impl->completer->setCompletionPrefix(impl->currentText());
|
||||||
|
impl->completer->complete();
|
||||||
|
|
||||||
|
viewport()->update();
|
||||||
|
|
||||||
|
emit tagsEdited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::completion(QStringList const& completions)
|
||||||
|
{
|
||||||
|
impl->completer = std::make_unique<QCompleter>([&] {
|
||||||
|
QStringList ret;
|
||||||
|
std::copy(completions.begin(), completions.end(), std::back_inserter(ret));
|
||||||
|
return ret;
|
||||||
|
}());
|
||||||
|
impl->setupCompleter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::tags(QStringList const& tags)
|
||||||
|
{
|
||||||
|
// Set to Default-state.
|
||||||
|
impl->editing_index = 0;
|
||||||
|
QList<Tag> t{Tag()};
|
||||||
|
|
||||||
|
std::transform(EmptySkipIterator(tags.begin(), tags.end()), // Ensure Invariant-1
|
||||||
|
EmptySkipIterator(tags.end()),
|
||||||
|
std::back_inserter(t),
|
||||||
|
[](QString const& text) {
|
||||||
|
return Tag{text, QRect(), 0};
|
||||||
|
});
|
||||||
|
|
||||||
|
impl->tags = std::move(t);
|
||||||
|
impl->editNewTag(impl->tags.size());
|
||||||
|
impl->updateDisplayText();
|
||||||
|
impl->calcRectsAndUpdateScrollRanges();
|
||||||
|
viewport()->update();
|
||||||
|
updateGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList TagsEdit::tags() const
|
||||||
|
{
|
||||||
|
QStringList ret;
|
||||||
|
std::transform(EmptySkipIterator(impl->tags.begin(), impl->tags.end()),
|
||||||
|
EmptySkipIterator(impl->tags.end()),
|
||||||
|
std::back_inserter(ret),
|
||||||
|
[](Tag const& tag) { return tag.text; });
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TagsEdit::mouseMoveEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (!m_readOnly) {
|
||||||
|
for (int i = 0; i < impl->tags.size(); ++i) {
|
||||||
|
if (impl->inCrossArea(i, event->pos())) {
|
||||||
|
viewport()->setCursor(Qt::ArrowCursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (impl->contentsRect().contains(event->pos())) {
|
||||||
|
viewport()->setCursor(Qt::IBeamCursor);
|
||||||
|
} else {
|
||||||
|
QAbstractScrollArea::mouseMoveEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TagsEdit::isAcceptableInput(const QKeyEvent* event) const
|
||||||
|
{
|
||||||
|
const QString text = event->text();
|
||||||
|
if (text.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QChar c = text.at(0);
|
||||||
|
|
||||||
|
// Formatting characters such as ZWNJ, ZWJ, RLM, etc. This needs to go before the
|
||||||
|
// next test, since CTRL+SHIFT is sometimes used to input it on Windows.
|
||||||
|
if (c.category() == QChar::Other_Format)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// QTBUG-35734: ignore Ctrl/Ctrl+Shift; accept only AltGr (Alt+Ctrl) on German keyboards
|
||||||
|
if (event->modifiers() == Qt::ControlModifier || event->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.isPrint())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (c.category() == QChar::Other_PrivateUse)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
78
src/gui/tag/TagsEdit.h
Normal file
78
src/gui/tag/TagsEdit.h
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Nicolai Trandafil
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractScrollArea>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
/// Tag multi-line editor widget
|
||||||
|
/// `Space` commits a tag and initiates a new tag edition
|
||||||
|
class TagsEdit : public QAbstractScrollArea
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit TagsEdit(QWidget* parent = nullptr);
|
||||||
|
~TagsEdit() override;
|
||||||
|
|
||||||
|
// QWidget
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
QSize minimumSizeHint() const override;
|
||||||
|
int heightForWidth(int w) const override;
|
||||||
|
|
||||||
|
/// Set completions
|
||||||
|
void completion(QStringList const& completions);
|
||||||
|
|
||||||
|
/// Set tags
|
||||||
|
void tags(QStringList const& tags);
|
||||||
|
|
||||||
|
/// Get tags
|
||||||
|
QStringList tags() const;
|
||||||
|
|
||||||
|
void setReadOnly(bool readOnly);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void tagsEdited();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// QWidget
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
void timerEvent(QTimerEvent* event) override;
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
void focusInEvent(QFocusEvent* event) override;
|
||||||
|
void focusOutEvent(QFocusEvent* event) override;
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool isAcceptableInput(QKeyEvent const* event) const;
|
||||||
|
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
|
bool m_readOnly;
|
||||||
|
};
|
@ -53,6 +53,7 @@
|
|||||||
#include "gui/group/EditGroupWidget.h"
|
#include "gui/group/EditGroupWidget.h"
|
||||||
#include "gui/group/GroupModel.h"
|
#include "gui/group/GroupModel.h"
|
||||||
#include "gui/group/GroupView.h"
|
#include "gui/group/GroupView.h"
|
||||||
|
#include "gui/tag/TagsEdit.h"
|
||||||
#include "gui/wizard/NewDatabaseWizard.h"
|
#include "gui/wizard/NewDatabaseWizard.h"
|
||||||
#include "keys/FileKey.h"
|
#include "keys/FileKey.h"
|
||||||
|
|
||||||
@ -447,6 +448,19 @@ void TestGui::testEditEntry()
|
|||||||
QCOMPARE(entry->historyItems().size(), ++editCount);
|
QCOMPARE(entry->historyItems().size(), ++editCount);
|
||||||
QVERIFY(entry->excludeFromReports());
|
QVERIFY(entry->excludeFromReports());
|
||||||
|
|
||||||
|
// Test tags
|
||||||
|
auto* tags = editEntryWidget->findChild<TagsEdit*>("tagsList");
|
||||||
|
QTest::keyClicks(tags, "_tag1");
|
||||||
|
QTest::keyClick(tags, Qt::Key_Space);
|
||||||
|
QCOMPARE(tags->tags().last(), QString("_tag1"));
|
||||||
|
QTest::keyClick(tags, Qt::Key_Space);
|
||||||
|
QTest::keyClicks(tags, "_tag2"); // adds another tag
|
||||||
|
QCOMPARE(tags->tags().last(), QString("_tag2"));
|
||||||
|
QTest::keyClick(tags, Qt::Key_Backspace); // Back into editing last tag
|
||||||
|
QTest::keyClicks(tags, "gers");
|
||||||
|
QTest::keyClick(tags, Qt::Key_Space);
|
||||||
|
QCOMPARE(tags->tags().last(), QString("_taggers"));
|
||||||
|
|
||||||
// Test entry colors (simulate choosing a color)
|
// Test entry colors (simulate choosing a color)
|
||||||
editEntryWidget->setCurrentPage(1);
|
editEntryWidget->setCurrentPage(1);
|
||||||
auto fgColor = QString("#FF0000");
|
auto fgColor = QString("#FF0000");
|
||||||
@ -872,6 +886,16 @@ void TestGui::testSearch()
|
|||||||
QTRY_VERIFY(m_dbWidget->isSearchActive());
|
QTRY_VERIFY(m_dbWidget->isSearchActive());
|
||||||
QTRY_COMPARE(entryView->model()->rowCount(), 0);
|
QTRY_COMPARE(entryView->model()->rowCount(), 0);
|
||||||
// Press the search clear button
|
// Press the search clear button
|
||||||
|
searchTextEdit->clear();
|
||||||
|
QTRY_VERIFY(searchTextEdit->text().isEmpty());
|
||||||
|
QTRY_VERIFY(searchTextEdit->hasFocus());
|
||||||
|
|
||||||
|
// Test tag search
|
||||||
|
searchTextEdit->clear();
|
||||||
|
QTest::keyClicks(searchTextEdit, "tag: testTag");
|
||||||
|
QTRY_VERIFY(m_dbWidget->isSearchActive());
|
||||||
|
QTRY_COMPARE(entryView->model()->rowCount(), 1);
|
||||||
|
|
||||||
searchTextEdit->clear();
|
searchTextEdit->clear();
|
||||||
QTRY_VERIFY(searchTextEdit->text().isEmpty());
|
QTRY_VERIFY(searchTextEdit->text().isEmpty());
|
||||||
QTRY_VERIFY(searchTextEdit->hasFocus());
|
QTRY_VERIFY(searchTextEdit->hasFocus());
|
||||||
@ -1736,6 +1760,8 @@ void TestGui::addCannedEntries()
|
|||||||
// Add entry "test" and confirm added
|
// Add entry "test" and confirm added
|
||||||
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
|
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
|
||||||
QTest::keyClicks(titleEdit, "test");
|
QTest::keyClicks(titleEdit, "test");
|
||||||
|
auto* editEntryWidgetTagsEdit = editEntryWidget->findChild<TagsEdit*>("tagsList");
|
||||||
|
editEntryWidgetTagsEdit->tags(QStringList() << "testTag");
|
||||||
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
|
auto* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
|
||||||
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
|
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user