Enhance Tags / Saved Searches
* Rename "Database Tags" to "Searches and Tags" * Separate searching for all entries and resetting the search * Support selecting multiple tags to search against * Fix using escaped quotes in search terms * Make tag searching more precise * Support `is:expired-#` to search for entries expiring within # days. Exclude recycled entries from expired search. * Don't list tags from entries that are recycled * Force hide tag auto-completion menu when tag editing widget is hidden. On rare occasions the focus out signal is not called when the tag view is hidden (entry edit is closed), this resolves that problem. * Remove spaces from before and after tags to prevent seemingly duplicate tags from being created. * Also fix some awkward signal/slot dances that were setup over time with the entry view and preview widget. Allow changing tags for multiple entries through context menu * Closes #8277 - show context menu with currently available tags in database and checks those that are set on one or more selected entries. When a tag is selected it is either set or unset on all entries depending on its checked state. * Add ability to save searches and recall them from the "Searches and Tags" view * Add ability to remove a tag from all entries from the "Searches and Tags" view * Cleanup tag handling and widgets
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include <QCheckBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QHostInfo>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QProcess>
|
||||
@@ -50,7 +51,7 @@
|
||||
#include "gui/group/EditGroupWidget.h"
|
||||
#include "gui/group/GroupView.h"
|
||||
#include "gui/reports/ReportsDialog.h"
|
||||
#include "gui/tag/TagModel.h"
|
||||
#include "gui/tag/TagView.h"
|
||||
#include "keeshare/KeeShare.h"
|
||||
|
||||
#ifdef WITH_XC_NETWORKING
|
||||
@@ -82,7 +83,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
||||
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
|
||||
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
|
||||
, m_groupView(new GroupView(m_db.data(), this))
|
||||
, m_tagView(new QListView(this))
|
||||
, m_tagView(new TagView(this))
|
||||
, m_saveAttempts(0)
|
||||
, m_entrySearcher(new EntrySearcher(false))
|
||||
{
|
||||
@@ -97,20 +98,15 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
||||
hbox->addWidget(m_mainSplitter);
|
||||
m_mainWidget->setLayout(mainLayout);
|
||||
|
||||
// Setup tags view and place under groups
|
||||
auto tagModel = new TagModel(m_db);
|
||||
// Setup searches and tags view and place under groups
|
||||
m_tagView->setObjectName("tagView");
|
||||
m_tagView->setModel(tagModel);
|
||||
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)));
|
||||
m_tagView->setDatabase(m_db);
|
||||
connect(m_tagView, SIGNAL(activated(QModelIndex)), this, SLOT(filterByTag()));
|
||||
connect(m_tagView, SIGNAL(clicked(QModelIndex)), this, SLOT(filterByTag()));
|
||||
|
||||
auto tagsWidget = new QWidget();
|
||||
auto tagsLayout = new QVBoxLayout();
|
||||
auto tagsTitle = new QLabel(tr("Database Tags"));
|
||||
auto tagsTitle = new QLabel(tr("Searches and Tags"));
|
||||
tagsTitle->setProperty("title", true);
|
||||
tagsWidget->setObjectName("tagWidget");
|
||||
tagsWidget->setLayout(tagsLayout);
|
||||
@@ -206,13 +202,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
|
||||
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
|
||||
connect(m_entryView, &EntryView::entrySelectionChanged, this, [this](Entry * currentEntry) {
|
||||
if (currentEntry) {
|
||||
m_previewView->setEntry(currentEntry);
|
||||
} else {
|
||||
m_previewView->setGroup(groupView()->currentGroup());
|
||||
}
|
||||
});
|
||||
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
|
||||
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
|
||||
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
|
||||
@@ -431,8 +420,7 @@ void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
|
||||
m_db = std::move(db);
|
||||
connectDatabaseSignals();
|
||||
m_groupView->changeDatabase(m_db);
|
||||
auto tagModel = new TagModel(m_db);
|
||||
m_tagView->setModel(tagModel);
|
||||
m_tagView->setDatabase(m_db);
|
||||
|
||||
// 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
|
||||
@@ -690,11 +678,23 @@ void DatabaseWidget::copyAttribute(QAction* action)
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::filterByTag(const QModelIndex& index)
|
||||
void DatabaseWidget::filterByTag()
|
||||
{
|
||||
m_tagView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select);
|
||||
const auto model = static_cast<TagModel*>(m_tagView->model());
|
||||
emit requestSearch(model->data(index, Qt::UserRole).toString());
|
||||
QStringList searchTerms;
|
||||
const auto selections = m_tagView->selectionModel()->selectedIndexes();
|
||||
for (const auto& index : selections) {
|
||||
searchTerms << index.data(Qt::UserRole).toString();
|
||||
}
|
||||
emit requestSearch(searchTerms.join(" "));
|
||||
}
|
||||
|
||||
void DatabaseWidget::setTag(QAction* action)
|
||||
{
|
||||
auto tag = action->text();
|
||||
auto state = action->isChecked();
|
||||
for (auto entry : m_entryView->selectedEntries()) {
|
||||
state ? entry->addTag(tag) : entry->removeTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::showTotpKeyQrCode()
|
||||
@@ -1128,22 +1128,13 @@ void DatabaseWidget::loadDatabase(bool accepted)
|
||||
// Only show expired entries if first unlock and option is enabled
|
||||
if (m_groupBeforeLock.isNull() && config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock).toBool()) {
|
||||
int expirationOffset = config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays).toInt();
|
||||
QList<Entry*> expiredEntries;
|
||||
for (auto entry : m_db->rootGroup()->entriesRecursive()) {
|
||||
if (entry->willExpireInDays(expirationOffset) && !entry->excludeFromReports() && !entry->isRecycled()) {
|
||||
expiredEntries << entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (!expiredEntries.isEmpty()) {
|
||||
m_entryView->displaySearch(expiredEntries);
|
||||
m_entryView->setFirstEntryActive();
|
||||
requestSearch(QString("is:expired-%1").arg(expirationOffset));
|
||||
QTimer::singleShot(150, this, [=] {
|
||||
m_searchingLabel->setText(
|
||||
expirationOffset == 0
|
||||
? tr("Expired entries")
|
||||
: tr("Entries expiring within %1 day(s)", "", expirationOffset).arg(expirationOffset));
|
||||
m_searchingLabel->setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
m_groupBeforeLock = QUuid();
|
||||
@@ -1449,6 +1440,40 @@ void DatabaseWidget::search(const QString& searchtext)
|
||||
emit searchModeActivated();
|
||||
}
|
||||
|
||||
void DatabaseWidget::saveSearch(const QString& searchtext)
|
||||
{
|
||||
if (!m_db->isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull the existing searches and prepend an empty string to allow
|
||||
// the user to input a new search name without seeing the first one
|
||||
QStringList searches(m_db->metadata()->savedSearches().keys());
|
||||
searches.prepend("");
|
||||
|
||||
QInputDialog dialog(this);
|
||||
connect(this, &DatabaseWidget::databaseLockRequested, &dialog, &QInputDialog::reject);
|
||||
|
||||
dialog.setComboBoxEditable(true);
|
||||
dialog.setComboBoxItems(searches);
|
||||
dialog.setOkButtonText(tr("Save"));
|
||||
dialog.setLabelText(tr("Enter a unique name or overwrite an existing search from the list:"));
|
||||
dialog.setWindowTitle(tr("Save Search"));
|
||||
dialog.exec();
|
||||
|
||||
auto name = dialog.textValue();
|
||||
if (!name.isEmpty()) {
|
||||
m_db->metadata()->addSavedSearch(name, searchtext);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::deleteSearch(const QString& name)
|
||||
{
|
||||
if (m_db->isInitialized()) {
|
||||
m_db->metadata()->deleteSavedSearch(name);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
||||
{
|
||||
m_entrySearcher->setCaseSensitive(state);
|
||||
@@ -1539,6 +1564,8 @@ void DatabaseWidget::onEntryChanged(Entry* entry)
|
||||
{
|
||||
if (entry) {
|
||||
m_previewView->setEntry(entry);
|
||||
} else {
|
||||
m_previewView->setGroup(groupView()->currentGroup());
|
||||
}
|
||||
|
||||
emit entrySelectionChanged();
|
||||
|
||||
@@ -49,6 +49,7 @@ class QSplitter;
|
||||
class QLabel;
|
||||
class MessageWidget;
|
||||
class EntryPreviewWidget;
|
||||
class TagView;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
@@ -175,7 +176,8 @@ public slots:
|
||||
void copyURL();
|
||||
void copyNotes();
|
||||
void copyAttribute(QAction* action);
|
||||
void filterByTag(const QModelIndex& index);
|
||||
void filterByTag();
|
||||
void setTag(QAction* action);
|
||||
void showTotp();
|
||||
void showTotpKeyQrCode();
|
||||
void copyTotp();
|
||||
@@ -218,6 +220,8 @@ public slots:
|
||||
|
||||
// Search related slots
|
||||
void search(const QString& searchtext);
|
||||
void saveSearch(const QString& searchtext);
|
||||
void deleteSearch(const QString& name);
|
||||
void setSearchCaseSensitive(bool state);
|
||||
void setSearchLimitGroup(bool state);
|
||||
void endSearch();
|
||||
@@ -283,7 +287,7 @@ private:
|
||||
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
|
||||
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
|
||||
QPointer<GroupView> m_groupView;
|
||||
QPointer<QListView> m_tagView;
|
||||
QPointer<TagView> m_tagView;
|
||||
QPointer<EntryView> m_entryView;
|
||||
|
||||
QScopedPointer<Group> m_newGroup;
|
||||
|
||||
@@ -115,48 +115,72 @@ void EntryPreviewWidget::clear()
|
||||
|
||||
void EntryPreviewWidget::setEntry(Entry* selectedEntry)
|
||||
{
|
||||
disconnect(m_currentEntry);
|
||||
disconnect(m_currentGroup);
|
||||
|
||||
m_currentEntry = selectedEntry;
|
||||
m_currentGroup = nullptr;
|
||||
|
||||
if (!selectedEntry) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentEntry = selectedEntry;
|
||||
|
||||
updateEntryHeaderLine();
|
||||
updateEntryTotp();
|
||||
updateEntryGeneralTab();
|
||||
updateEntryAdvancedTab();
|
||||
updateEntryAutotypeTab();
|
||||
|
||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||
|
||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
||||
const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
||||
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
||||
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
||||
connect(selectedEntry, &Entry::modified, this, &EntryPreviewWidget::refresh);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void EntryPreviewWidget::setGroup(Group* selectedGroup)
|
||||
{
|
||||
disconnect(m_currentEntry);
|
||||
disconnect(m_currentGroup);
|
||||
|
||||
m_currentEntry = nullptr;
|
||||
m_currentGroup = selectedGroup;
|
||||
|
||||
if (!selectedGroup) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentGroup = selectedGroup;
|
||||
updateGroupHeaderLine();
|
||||
updateGroupGeneralTab();
|
||||
connect(m_currentGroup, &Group::modified, this, &EntryPreviewWidget::refresh);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void EntryPreviewWidget::refresh()
|
||||
{
|
||||
if (m_currentEntry) {
|
||||
updateEntryHeaderLine();
|
||||
updateEntryTotp();
|
||||
updateEntryGeneralTab();
|
||||
updateEntryAdvancedTab();
|
||||
updateEntryAutotypeTab();
|
||||
|
||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||
|
||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
||||
const int tabIndex =
|
||||
m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
|
||||
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
||||
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
||||
} else if (m_currentGroup) {
|
||||
updateGroupHeaderLine();
|
||||
updateGroupGeneralTab();
|
||||
|
||||
#if defined(WITH_XC_KEESHARE)
|
||||
updateGroupSharingTab();
|
||||
updateGroupSharingTab();
|
||||
#endif
|
||||
|
||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||
setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
|
||||
|
||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
||||
const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
||||
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
||||
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
||||
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
||||
const int tabIndex =
|
||||
m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
|
||||
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
||||
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void EntryPreviewWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
||||
@@ -240,6 +264,8 @@ void EntryPreviewWidget::setNotesVisible(QTextEdit* notesWidget, const QString&
|
||||
} else {
|
||||
if (!notes.isEmpty()) {
|
||||
notesWidget->setPlainText(QString("\u25cf").repeated(6));
|
||||
} else {
|
||||
notesWidget->setPlainText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public slots:
|
||||
void setEntry(Entry* selectedEntry);
|
||||
void setGroup(Group* selectedGroup);
|
||||
void setDatabaseMode(DatabaseWidget::Mode mode);
|
||||
void refresh();
|
||||
void clear();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -129,6 +129,7 @@ MainWindow::MainWindow()
|
||||
m_entryContextMenu->addAction(m_ui->actionEntryCopyPassword);
|
||||
m_entryContextMenu->addAction(m_ui->menuEntryCopyAttribute->menuAction());
|
||||
m_entryContextMenu->addAction(m_ui->menuEntryTotp->menuAction());
|
||||
m_entryContextMenu->addAction(m_ui->menuTags->menuAction());
|
||||
m_entryContextMenu->addSeparator();
|
||||
m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
|
||||
m_entryContextMenu->addSeparator();
|
||||
@@ -240,6 +241,11 @@ MainWindow::MainWindow()
|
||||
m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*)));
|
||||
connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu()));
|
||||
|
||||
m_setTagsMenuActions = new QActionGroup(m_ui->menuTags);
|
||||
m_setTagsMenuActions->setExclusive(false);
|
||||
m_actionMultiplexer.connect(m_setTagsMenuActions, SIGNAL(triggered(QAction*)), SLOT(setTag(QAction*)));
|
||||
connect(m_ui->menuTags, &QMenu::aboutToShow, this, &MainWindow::updateSetTagsMenu);
|
||||
|
||||
Qt::Key globalAutoTypeKey = static_cast<Qt::Key>(config()->get(Config::GlobalAutoTypeKey).toInt());
|
||||
Qt::KeyboardModifiers globalAutoTypeModifiers =
|
||||
static_cast<Qt::KeyboardModifiers>(config()->get(Config::GlobalAutoTypeModifiers).toInt());
|
||||
@@ -791,6 +797,38 @@ void MainWindow::updateCopyAttributesMenu()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateSetTagsMenu()
|
||||
{
|
||||
// Remove all existing actions
|
||||
m_ui->menuTags->clear();
|
||||
|
||||
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
|
||||
if (dbWidget) {
|
||||
// Enumerate tags applied to the selected entries
|
||||
QSet<QString> selectedTags;
|
||||
for (auto entry : dbWidget->entryView()->selectedEntries()) {
|
||||
for (auto tag : entry->tagList()) {
|
||||
selectedTags.insert(tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Add known database tags as actions and set checked if
|
||||
// a selected entry has that tag
|
||||
for (auto tag : dbWidget->database()->tagList()) {
|
||||
auto action = m_ui->menuTags->addAction(icons()->icon("tag"), tag);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(selectedTags.contains(tag));
|
||||
m_setTagsMenuActions->addAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
// If no tags exist in the database then show a tip to the user
|
||||
if (m_ui->menuTags->isEmpty()) {
|
||||
auto action = m_ui->menuTags->addAction(tr("No Tags"));
|
||||
action->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::openRecentDatabase(QAction* action)
|
||||
{
|
||||
openDatabase(action->data().toString());
|
||||
@@ -870,6 +908,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
|
||||
m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes());
|
||||
m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected);
|
||||
m_ui->menuEntryTotp->setEnabled(singleEntrySelected);
|
||||
m_ui->menuTags->setEnabled(entriesSelected);
|
||||
m_ui->actionEntryAutoType->setEnabled(singleEntrySelected);
|
||||
m_ui->actionEntryAutoType->menu()->setEnabled(singleEntrySelected);
|
||||
m_ui->actionEntryAutoTypeSequence->setText(
|
||||
|
||||
@@ -130,6 +130,7 @@ private slots:
|
||||
void clearLastDatabases();
|
||||
void updateLastDatabasesMenu();
|
||||
void updateCopyAttributesMenu();
|
||||
void updateSetTagsMenu();
|
||||
void showEntryContextMenu(const QPoint& globalPos);
|
||||
void showGroupContextMenu(const QPoint& globalPos);
|
||||
void applySettingsChanges();
|
||||
@@ -172,6 +173,7 @@ private:
|
||||
QPointer<QMenu> m_entryNewContextMenu;
|
||||
QPointer<QActionGroup> m_lastDatabasesActions;
|
||||
QPointer<QActionGroup> m_copyAdditionalAttributeActions;
|
||||
QPointer<QActionGroup> m_setTagsMenuActions;
|
||||
QPointer<InactivityTimer> m_inactivityTimer;
|
||||
QPointer<InactivityTimer> m_touchIDinactivityTimer;
|
||||
int m_countDefaultAttributes;
|
||||
|
||||
@@ -316,6 +316,11 @@
|
||||
<addaction name="actionEntryTotpQRCode"/>
|
||||
<addaction name="actionEntrySetupTotp"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTags">
|
||||
<property name="title">
|
||||
<string>Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
<addaction name="actionEntryNew"/>
|
||||
<addaction name="actionEntryEdit"/>
|
||||
<addaction name="actionEntryClone"/>
|
||||
@@ -328,6 +333,7 @@
|
||||
<addaction name="actionEntryCopyPassword"/>
|
||||
<addaction name="menuEntryCopyAttribute"/>
|
||||
<addaction name="menuEntryTotp"/>
|
||||
<addaction name="menuTags"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionEntryAutoType"/>
|
||||
<addaction name="separator"/>
|
||||
|
||||
@@ -46,6 +46,7 @@ SearchWidget::SearchWidget(QWidget* parent)
|
||||
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
|
||||
connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp()));
|
||||
connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu()));
|
||||
connect(m_ui->saveIcon, &QAction::triggered, this, [this] { emit saveSearch(m_ui->searchEdit->text()); });
|
||||
connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
|
||||
connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
|
||||
connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
|
||||
@@ -70,6 +71,10 @@ SearchWidget::SearchWidget(QWidget* parent)
|
||||
m_ui->helpIcon->setIcon(icons()->icon("system-help"));
|
||||
m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition);
|
||||
|
||||
m_ui->saveIcon->setIcon(icons()->icon("document-save"));
|
||||
m_ui->searchEdit->addAction(m_ui->saveIcon, QLineEdit::TrailingPosition);
|
||||
m_ui->saveIcon->setVisible(false);
|
||||
|
||||
// Fix initial visibility of actions (bug in Qt)
|
||||
for (QToolButton* toolButton : m_ui->searchEdit->findChildren<QToolButton*>()) {
|
||||
toolButton->setVisible(toolButton->defaultAction()->isVisible());
|
||||
@@ -126,6 +131,7 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
||||
{
|
||||
// Connects basically only to the current DatabaseWidget, but allows to switch between instances!
|
||||
mx.connect(this, SIGNAL(search(QString)), SLOT(search(QString)));
|
||||
mx.connect(this, SIGNAL(saveSearch(QString)), SLOT(saveSearch(QString)));
|
||||
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
|
||||
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
|
||||
mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
|
||||
@@ -165,6 +171,7 @@ void SearchWidget::startSearch()
|
||||
m_searchTimer->stop();
|
||||
}
|
||||
|
||||
m_ui->saveIcon->setVisible(true);
|
||||
search(m_ui->searchEdit->text());
|
||||
}
|
||||
|
||||
@@ -208,6 +215,7 @@ void SearchWidget::focusSearch()
|
||||
void SearchWidget::clearSearch()
|
||||
{
|
||||
m_ui->searchEdit->clear();
|
||||
m_ui->saveIcon->setVisible(false);
|
||||
emit searchCanceled();
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ signals:
|
||||
void downPressed();
|
||||
void enterPressed();
|
||||
void lostFocus();
|
||||
void saveSearch(const QString& text);
|
||||
|
||||
public slots:
|
||||
void databaseChanged(DatabaseWidget* dbWidget = nullptr);
|
||||
|
||||
@@ -56,6 +56,11 @@
|
||||
<string>Search Help</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="saveIcon">
|
||||
<property name="text">
|
||||
<string>Save Search</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>searchEdit</tabstop>
|
||||
|
||||
@@ -320,8 +320,8 @@
|
||||
<tabstop>usernameComboBox</tabstop>
|
||||
<tabstop>passwordEdit</tabstop>
|
||||
<tabstop>urlEdit</tabstop>
|
||||
<tabstop>tagsList</tabstop>
|
||||
<tabstop>fetchFaviconButton</tabstop>
|
||||
<tabstop>tagsList</tabstop>
|
||||
<tabstop>expireCheck</tabstop>
|
||||
<tabstop>expireDatePicker</tabstop>
|
||||
<tabstop>expirePresets</tabstop>
|
||||
|
||||
@@ -263,6 +263,15 @@ Entry* EntryView::currentEntry()
|
||||
}
|
||||
}
|
||||
|
||||
QList<Entry*> EntryView::selectedEntries()
|
||||
{
|
||||
QList<Entry*> list;
|
||||
for (auto row : selectionModel()->selectedRows()) {
|
||||
list.append(m_model->entryFromIndex(m_sortModel->mapToSource(row)));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
int EntryView::numberOfSelectedEntries()
|
||||
{
|
||||
return selectionModel()->selectedRows().size();
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
void setModel(QAbstractItemModel* model) override;
|
||||
Entry* currentEntry();
|
||||
void setCurrentEntry(Entry* entry);
|
||||
QList<Entry*> selectedEntries();
|
||||
Entry* entryFromIndex(const QModelIndex& index);
|
||||
QModelIndex indexFromEntry(Entry* entry);
|
||||
int currentEntryIndex();
|
||||
|
||||
@@ -18,12 +18,19 @@
|
||||
#include "TagModel.h"
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/MessageBox.h"
|
||||
|
||||
TagModel::TagModel(QSharedPointer<Database> db, QObject* parent)
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
|
||||
TagModel::TagModel(QObject* parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
setDatabase(db);
|
||||
m_defaultSearches << qMakePair(tr("Clear Search"), QString("")) << qMakePair(tr("All Entries"), QString("*"))
|
||||
<< qMakePair(tr("Expired"), QString("is:expired"))
|
||||
<< qMakePair(tr("Weak Passwords"), QString("is:weak"));
|
||||
}
|
||||
|
||||
TagModel::~TagModel()
|
||||
@@ -32,12 +39,19 @@ TagModel::~TagModel()
|
||||
|
||||
void TagModel::setDatabase(QSharedPointer<Database> db)
|
||||
{
|
||||
if (m_db) {
|
||||
disconnect(m_db.data());
|
||||
}
|
||||
|
||||
m_db = db;
|
||||
if (!m_db) {
|
||||
m_tagList.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
connect(m_db.data(), SIGNAL(tagListUpdated()), SLOT(updateTagList()));
|
||||
connect(m_db->metadata()->customData(), SIGNAL(modified()), SLOT(updateTagList()));
|
||||
|
||||
updateTagList();
|
||||
}
|
||||
|
||||
@@ -45,10 +59,35 @@ void TagModel::updateTagList()
|
||||
{
|
||||
beginResetModel();
|
||||
m_tagList.clear();
|
||||
m_tagList << tr("All") << tr("Expired") << tr("Weak Passwords") << m_db->tagList();
|
||||
|
||||
m_tagList << m_defaultSearches;
|
||||
|
||||
auto savedSearches = m_db->metadata()->savedSearches();
|
||||
for (auto search : savedSearches.keys()) {
|
||||
m_tagList << qMakePair(search, savedSearches[search].toString());
|
||||
}
|
||||
|
||||
m_tagListStart = m_tagList.size();
|
||||
for (auto tag : m_db->tagList()) {
|
||||
auto escapedTag = tag;
|
||||
escapedTag.replace("\"", "\\\"");
|
||||
m_tagList << qMakePair(tag, QString("tag:\"%1\"").arg(escapedTag));
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
TagModel::TagType TagModel::itemType(const QModelIndex& index)
|
||||
{
|
||||
int row = index.row();
|
||||
if (row < m_defaultSearches.size()) {
|
||||
return TagType::DEFAULT_SEARCH;
|
||||
} else if (row < m_tagListStart) {
|
||||
return TagType::SAVED_SEARCH;
|
||||
}
|
||||
return TagType::TAG;
|
||||
}
|
||||
|
||||
int TagModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
@@ -61,29 +100,23 @@ QVariant TagModel::data(const QModelIndex& index, int role) const
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto row = index.row();
|
||||
switch (role) {
|
||||
case Qt::DecorationRole:
|
||||
if (index.row() <= 2) {
|
||||
return icons()->icon("tag-search");
|
||||
if (row < m_tagListStart) {
|
||||
return icons()->icon("database-search");
|
||||
}
|
||||
return icons()->icon("tag");
|
||||
case Qt::DisplayRole:
|
||||
return m_tagList.at(index.row());
|
||||
return m_tagList.at(row).first;
|
||||
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 m_tagList.at(row).second;
|
||||
case Qt::UserRole + 1:
|
||||
if (row == (m_defaultSearches.size() - 1)) {
|
||||
return true;
|
||||
}
|
||||
return QString("tag:%1").arg(m_tagList.at(index.row()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
const QStringList& TagModel::tags() const
|
||||
{
|
||||
return m_tagList;
|
||||
}
|
||||
|
||||
@@ -28,21 +28,30 @@ class TagModel : public QAbstractListModel
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagModel(QSharedPointer<Database> db, QObject* parent = nullptr);
|
||||
explicit TagModel(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;
|
||||
|
||||
enum TagType
|
||||
{
|
||||
DEFAULT_SEARCH,
|
||||
SAVED_SEARCH,
|
||||
TAG
|
||||
};
|
||||
TagType itemType(const QModelIndex& index);
|
||||
|
||||
private slots:
|
||||
void updateTagList();
|
||||
|
||||
private:
|
||||
QSharedPointer<Database> m_db;
|
||||
QStringList m_tagList;
|
||||
QList<QPair<QString, QString>> m_defaultSearches;
|
||||
QList<QPair<QString, QString>> m_tagList;
|
||||
int m_tagListStart = 0;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TAGMODEL_H
|
||||
|
||||
98
src/gui/tag/TagView.cpp
Normal file
98
src/gui/tag/TagView.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagView.h"
|
||||
|
||||
#include "TagModel.h"
|
||||
#include "core/Database.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/MessageBox.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
class TagItemDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
explicit TagItemDelegate(QObject* parent)
|
||||
: QStyledItemDelegate(parent){};
|
||||
|
||||
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
|
||||
{
|
||||
QStyledItemDelegate::paint(painter, option, index);
|
||||
if (index.data(Qt::UserRole + 1).toBool()) {
|
||||
QRect bounds = option.rect;
|
||||
bounds.setY(bounds.bottom());
|
||||
painter->fillRect(bounds, option.palette.mid());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TagView::TagView(QWidget* parent)
|
||||
: QListView(parent)
|
||||
, m_model(new TagModel(this))
|
||||
{
|
||||
setModel(m_model);
|
||||
setFrameStyle(QFrame::NoFrame);
|
||||
setSelectionMode(QListView::ExtendedSelection);
|
||||
setSelectionBehavior(QListView::SelectRows);
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
setItemDelegate(new TagItemDelegate(this));
|
||||
|
||||
connect(this, &QListView::customContextMenuRequested, this, &TagView::contextMenuRequested);
|
||||
}
|
||||
|
||||
void TagView::setDatabase(QSharedPointer<Database> db)
|
||||
{
|
||||
m_db = db;
|
||||
m_model->setDatabase(db);
|
||||
setCurrentIndex(m_model->index(0));
|
||||
}
|
||||
|
||||
void TagView::contextMenuRequested(const QPoint& pos)
|
||||
{
|
||||
auto index = indexAt(pos);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto type = m_model->itemType(index);
|
||||
if (type == TagModel::SAVED_SEARCH) {
|
||||
// Allow deleting saved searches
|
||||
QMenu menu;
|
||||
auto action = menu.exec({new QAction(icons()->icon("trash"), tr("Remove Search"))}, mapToGlobal(pos));
|
||||
if (action) {
|
||||
m_db->metadata()->deleteSavedSearch(index.data(Qt::DisplayRole).toString());
|
||||
}
|
||||
} else if (type == TagModel::TAG) {
|
||||
// Allow removing tags from all entries in a database
|
||||
QMenu menu;
|
||||
auto action = menu.exec({new QAction(icons()->icon("trash"), tr("Remove Tag"))}, mapToGlobal(pos));
|
||||
if (action) {
|
||||
auto tag = index.data(Qt::DisplayRole).toString();
|
||||
auto ans = MessageBox::question(this,
|
||||
tr("Confirm Remove Tag"),
|
||||
tr("Remove tag \"%1\" from all entries in this database?").arg(tag),
|
||||
MessageBox::Remove | MessageBox::Cancel);
|
||||
if (ans == MessageBox::Remove) {
|
||||
m_db->removeTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/gui/tag/TagView.h
Normal file
47
src/gui/tag/TagView.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 2 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_TAGVIEW_H
|
||||
#define KEEPASSXC_TAGVIEW_H
|
||||
|
||||
#include <QListView>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class Database;
|
||||
class QAbstractListModel;
|
||||
class TagModel;
|
||||
|
||||
class TagView : public QListView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TagView(QWidget* parent = nullptr);
|
||||
void setDatabase(QSharedPointer<Database> db);
|
||||
|
||||
signals:
|
||||
|
||||
private slots:
|
||||
void contextMenuRequested(const QPoint& pos);
|
||||
|
||||
private:
|
||||
QSharedPointer<Database> m_db;
|
||||
QPointer<TagModel> m_model;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_ENTRYVIEW_H
|
||||
@@ -401,6 +401,7 @@ struct TagsEdit::Impl
|
||||
// and ensures Invariant-1.
|
||||
void editNewTag(int i)
|
||||
{
|
||||
currentText() = currentText().trimmed();
|
||||
tags.insert(std::next(std::begin(tags), static_cast<std::ptrdiff_t>(i)), Tag());
|
||||
if (editing_index >= i) {
|
||||
++editing_index;
|
||||
@@ -646,6 +647,12 @@ void TagsEdit::focusOutEvent(QFocusEvent*)
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void TagsEdit::hideEvent(QHideEvent* event)
|
||||
{
|
||||
Q_UNUSED(event)
|
||||
impl->completer->popup()->hide();
|
||||
}
|
||||
|
||||
void TagsEdit::paintEvent(QPaintEvent*)
|
||||
{
|
||||
QPainter p(viewport());
|
||||
|
||||
@@ -68,6 +68,7 @@ protected:
|
||||
void focusOutEvent(QFocusEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void hideEvent(QHideEvent* event) override;
|
||||
|
||||
private:
|
||||
bool isAcceptableInput(QKeyEvent const* event) const;
|
||||
|
||||
Reference in New Issue
Block a user