diff --git a/share/icons/application/16x16/actions/system-help.png b/share/icons/application/16x16/actions/system-help.png new file mode 100644 index 000000000..75ebaf7f5 Binary files /dev/null and b/share/icons/application/16x16/actions/system-help.png differ diff --git a/share/icons/application/22x22/actions/system-help.png b/share/icons/application/22x22/actions/system-help.png new file mode 100644 index 000000000..86b64075f Binary files /dev/null and b/share/icons/application/22x22/actions/system-help.png differ diff --git a/share/icons/application/32x32/actions/system-help.png b/share/icons/application/32x32/actions/system-help.png new file mode 100644 index 000000000..8a9eb1a82 Binary files /dev/null and b/share/icons/application/32x32/actions/system-help.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd974a98..f68afc8f6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -126,7 +126,6 @@ set(keepassx_SOURCES gui/UnlockDatabaseWidget.cpp gui/UnlockDatabaseDialog.cpp gui/WelcomeWidget.cpp - gui/widgets/ElidedLabel.cpp gui/csvImport/CsvImportWidget.cpp gui/csvImport/CsvImportWizard.cpp gui/csvImport/CsvParserModel.cpp @@ -154,6 +153,8 @@ set(keepassx_SOURCES gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp gui/settings/SettingsWidget.cpp + gui/widgets/ElidedLabel.cpp + gui/widgets/PopupHelpWidget.cpp gui/wizard/NewDatabaseWizard.cpp gui/wizard/NewDatabaseWizardPage.cpp gui/wizard/NewDatabaseWizardPageMetaData.cpp diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index a1315ad49..95b9008e6 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -88,7 +88,7 @@ bool BrowserService::openDatabase(bool triggerUnlock) } if (triggerUnlock) { - KEEPASSXC_MAIN_WINDOW->bringToFront(); + getMainWindow()->bringToFront(); m_bringToFrontRequested = true; } @@ -901,7 +901,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) { if (dbWidget) { if (m_bringToFrontRequested) { - KEEPASSXC_MAIN_WINDOW->lower(); + getMainWindow()->lower(); m_bringToFrontRequested = false; } emit databaseUnlocked(); diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index 6614ab463..f582d62e7 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -96,6 +96,7 @@ bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) found = !attachments.filter(term->regex).empty(); break; default: + // Terms without a specific field try to match title, username, url, and notes found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() || term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() || term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() || @@ -139,7 +140,7 @@ QList > EntrySearcher::parseSearchTerm term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive); // Exclude modifier - term->exclude = mods.contains("-"); + term->exclude = mods.contains("-") || mods.contains("!"); // Determine the field to search QString field = result.captured(2); diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index b67f542c6..03cd0e551 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -49,7 +49,6 @@ namespace Application::Application(int& argc, char** argv) : QApplication(argc, argv) - , m_mainWindow(nullptr) #ifdef Q_OS_UNIX , m_unixSignalNotifier(nullptr) #endif @@ -143,16 +142,6 @@ Application::~Application() } } -QWidget* Application::mainWindow() const -{ - return m_mainWindow; -} - -void Application::setMainWindow(QWidget* mainWindow) -{ - m_mainWindow = mainWindow; -} - bool Application::event(QEvent* event) { // Handle Apple QFileOpenEvent from finder (double click on .kdbx file) diff --git a/src/gui/Application.h b/src/gui/Application.h index 7b1a77f60..9a3ef756b 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -37,9 +37,7 @@ class Application : public QApplication public: Application(int& argc, char** argv); - QWidget* mainWindow() const; ~Application() override; - void setMainWindow(QWidget* mainWindow); bool event(QEvent* event) override; bool isAlreadyRunning() const; @@ -60,8 +58,6 @@ private slots: void socketReadyRead(); private: - QWidget* m_mainWindow; - #if defined(Q_OS_UNIX) /** * Register Unix signals such as SIGINT and SIGTERM for clean shutdown. diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d9f9a0557..67e453392 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -109,12 +109,17 @@ public: const QString MainWindow::BaseWindowTitle = "KeePassXC"; +MainWindow* g_MainWindow = nullptr; +MainWindow* getMainWindow() { return g_MainWindow; } + MainWindow::MainWindow() : m_ui(new Ui::MainWindow()) , m_trayIcon(nullptr) , m_appExitCalled(false) , m_appExiting(false) { + g_MainWindow = this; + m_ui->setupUi(this); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index caf2a0c58..38b1c9308 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -148,8 +148,12 @@ private: bool m_appExiting; }; -#define KEEPASSXC_MAIN_WINDOW \ - (qobject_cast(qApp) ? qobject_cast(qobject_cast(qApp)->mainWindow()) \ - : nullptr) +/** + * Return instance of MainWindow created on app load + * non-gui instances will return nullptr + * + * @return MainWindow instance or nullptr + */ +MainWindow* getMainWindow(); #endif // KEEPASSX_MAINWINDOW_H diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui new file mode 100644 index 000000000..daa3a851e --- /dev/null +++ b/src/gui/SearchHelpWidget.ui @@ -0,0 +1,458 @@ + + + SearchHelpWidget + + + + 0 + 0 + 334 + 249 + + + + Search Help + + + false + + + #SearchHelpWidget { background-color: #ffffff } + + + QFrame::Box + + + QFrame::Plain + + + + 6 + + + QLayout::SetDefaultConstraint + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + 75 + true + + + + Search terms are as follows: [modifiers][field:]["]term["] + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Every search term must match (ie, logical AND) + + + + + + + + + + 50 + false + + + + Modifiers + + + + 8 + + + 8 + + + 9 + + + 10 + + + 9 + + + 9 + + + + + + 10 + 0 + + + + + 75 + true + + + + ! + + + false + + + Qt::AlignCenter + + + + + + + exclude term from results + + + + + + + match term exactly + + + + + + + + 10 + 0 + + + + + 75 + true + + + + * + + + Qt::AlignCenter + + + + + + + use regex in term + + + + + + + + 10 + 0 + + + + + 75 + true + + + + + + + + Qt::AlignCenter + + + + + + + + + + + 50 + false + + + + Fields + + + false + + + + 15 + + + 10 + + + 15 + + + 8 + + + 5 + + + + + username + + + + + + + password + + + + + + + title + + + + + + + url + + + + + + + notes + + + + + + + attribute + + + + + + + attachment + + + + + + + + + + + + + + Term Wildcards + + + + 8 + + + 8 + + + + + + 10 + 0 + + + + + 75 + true + + + + * + + + false + + + Qt::AlignCenter + + + + + + + + 10 + 0 + + + + + 75 + true + + + + ? + + + Qt::AlignCenter + + + + + + + + 10 + 0 + + + + + 75 + true + + + + | + + + Qt::AlignCenter + + + + + + + match anything + + + + + + + match one + + + + + + + logical OR + + + + + + + + + + Examples + + + + 8 + + + + + + 0 + 0 + + + + user:name1 url:google + + + + + + + + 0 + 0 + + + + user:"name1|name2" + + + + + + + + 0 + 0 + + + + +user:name1 *notes:"secret \d" + + + + + + + + + + + + + diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 6441502b0..cde899576 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -18,6 +18,7 @@ #include "SearchWidget.h" #include "ui_SearchWidget.h" +#include "ui_SearchHelpWidget.h" #include #include @@ -26,6 +27,7 @@ #include "core/Config.h" #include "core/FilePath.h" +#include "gui/widgets/PopupHelpWidget.h" SearchWidget::SearchWidget(QWidget* parent) : QWidget(parent) @@ -35,11 +37,17 @@ SearchWidget::SearchWidget(QWidget* parent) { m_ui->setupUi(this); + m_helpWidget = new PopupHelpWidget(m_ui->searchEdit); + m_helpWidget->setOffset(QPoint(0,1)); + Ui::SearchHelpWidget helpUi; + helpUi.setupUi(m_helpWidget); + m_searchTimer->setSingleShot(true); m_clearSearchTimer->setSingleShot(true); connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer())); connect(m_ui->clearIcon, SIGNAL(triggered(bool)), m_ui->searchEdit, SLOT(clear())); + connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp())); connect(m_searchTimer, SIGNAL(timeout()), this, SLOT(startSearch())); connect(m_clearSearchTimer, SIGNAL(timeout()), m_ui->searchEdit, SLOT(clear())); connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); @@ -65,6 +73,9 @@ SearchWidget::SearchWidget(QWidget* parent) m_ui->searchIcon->setMenu(searchMenu); m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition); + m_ui->helpIcon->setIcon(filePath()->icon("actions", "system-help")); + m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition); + m_ui->clearIcon->setIcon(filePath()->icon("actions", "edit-clear-locationbar-rtl")); m_ui->clearIcon->setVisible(false); m_ui->searchEdit->addAction(m_ui->clearIcon, QLineEdit::TrailingPosition); @@ -86,13 +97,6 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) if (keyEvent->key() == Qt::Key_Escape) { emit escapePressed(); return true; - } else if (keyEvent->matches(QKeySequence::Copy)) { - // If Control+C is pressed in the search edit when no text - // is selected, copy the password of the current entry - if (!m_ui->searchEdit->hasSelectedText()) { - emit copyPressed(); - return true; - } } else if (keyEvent->matches(QKeySequence::MoveToNextLine)) { if (m_ui->searchEdit->cursorPosition() == m_ui->searchEdit->text().length()) { // If down is pressed at EOL, move the focus to the entry view @@ -111,7 +115,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) m_clearSearchTimer->stop(); } - return QObject::eventFilter(obj, event); + return QWidget::eventFilter(obj, event); } void SearchWidget::connectSignals(SignalMultiplexer& mx) @@ -188,3 +192,12 @@ void SearchWidget::searchFocus() m_ui->searchEdit->setFocus(); m_ui->searchEdit->selectAll(); } + +void SearchWidget::toggleHelp() +{ + if (m_helpWidget->isVisible()) { + m_helpWidget->hide(); + } else { + m_helpWidget->show(); + } +} diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index 6f4387004..43dd76430 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -30,6 +30,8 @@ namespace Ui class SearchWidget; } +class PopupHelpWidget; + class SearchWidget : public QWidget { Q_OBJECT @@ -45,6 +47,7 @@ public: void setLimitGroup(bool state); protected: + // Filter key presses in the search field bool eventFilter(QObject* obj, QEvent* event) override; signals: @@ -65,9 +68,11 @@ private slots: void updateCaseSensitive(); void updateLimitGroup(); void searchFocus(); + void toggleHelp(); private: const QScopedPointer m_ui; + PopupHelpWidget* m_helpWidget; QTimer* m_searchTimer; QTimer* m_clearSearchTimer; QAction* m_actionCaseSensitive; diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 19b274a88..93fbbdee5 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -81,6 +81,11 @@ Clear + + + Search Help + + searchEdit diff --git a/src/gui/masterkey/KeyFileEditWidget.cpp b/src/gui/masterkey/KeyFileEditWidget.cpp index 14ac4879c..c694e2c5a 100644 --- a/src/gui/masterkey/KeyFileEditWidget.cpp +++ b/src/gui/masterkey/KeyFileEditWidget.cpp @@ -45,7 +45,7 @@ bool KeyFileEditWidget::addToCompositeKey(QSharedPointer key) } if (fileKey->type() != FileKey::Hashed) { - QMessageBox::warning(KEEPASSXC_MAIN_WINDOW, + QMessageBox::warning(getMainWindow(), tr("Legacy key file format"), tr("You are using a legacy key file format which may become\n" "unsupported in the future.\n\n" @@ -100,7 +100,7 @@ void KeyFileEditWidget::createKeyFile() QString errorMsg; bool created = FileKey::create(fileName, &errorMsg); if (!created) { - MessageBox::critical(KEEPASSXC_MAIN_WINDOW, tr("Error creating key file"), + MessageBox::critical(getMainWindow(), tr("Error creating key file"), tr("Unable to create key file: %1").arg(errorMsg), QMessageBox::Button::Ok); } else { m_compUi->keyFileCombo->setEditText(fileName); diff --git a/src/gui/widgets/PopupHelpWidget.cpp b/src/gui/widgets/PopupHelpWidget.cpp new file mode 100644 index 000000000..45e19e81e --- /dev/null +++ b/src/gui/widgets/PopupHelpWidget.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#include "PopupHelpWidget.h" + +#include + +#include "gui/MainWindow.h" + +PopupHelpWidget::PopupHelpWidget(QWidget* parent) + : QFrame(parent) + , m_parentWindow(parent->window()) + , m_appWindow(getMainWindow()) + , m_offset({0, 0}) + , m_corner(Qt::BottomLeftCorner) +{ + Q_ASSERT(parent); + + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + hide(); + + m_appWindow->installEventFilter(this); + parent->installEventFilter(this); +} + +PopupHelpWidget::~PopupHelpWidget() +{ + m_parentWindow->removeEventFilter(this); + parentWidget()->removeEventFilter(this); +} + +void PopupHelpWidget::setOffset(const QPoint& offset) +{ + m_offset = offset; + if (isVisible()) { + alignWithParent(); + } +} + +void PopupHelpWidget::setPosition(Qt::Corner corner) +{ + m_corner = corner; + if (isVisible()) { + alignWithParent(); + } +} + +bool PopupHelpWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == parentWidget() && event->type() == QEvent::FocusOut) { + hide(); + } else if (obj == m_appWindow && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) { + if (isVisible()) { + alignWithParent(); + } + } + return QFrame::eventFilter(obj, event); +} + +void PopupHelpWidget::showEvent(QShowEvent* event) +{ + alignWithParent(); + QFrame::showEvent(event); +} + +void PopupHelpWidget::alignWithParent() +{ + QPoint pos; + switch (m_corner) { + case Qt::TopLeftCorner: + pos = parentWidget()->geometry().topLeft() + m_offset - QPoint(0, height()); + break; + case Qt::TopRightCorner: + pos = parentWidget()->geometry().topRight() + m_offset - QPoint(width(), height()); + break; + case Qt::BottomRightCorner: + pos = parentWidget()->geometry().bottomRight() + m_offset - QPoint(width(), 0); + break; + default: + pos = parentWidget()->geometry().bottomLeft() + m_offset; + break; + } + + move(m_parentWindow->mapToGlobal(pos)); +} \ No newline at end of file diff --git a/src/gui/widgets/PopupHelpWidget.h b/src/gui/widgets/PopupHelpWidget.h new file mode 100644 index 000000000..66dac2b30 --- /dev/null +++ b/src/gui/widgets/PopupHelpWidget.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_POPUPHELPWIDGET_H +#define KEEPASSXC_POPUPHELPWIDGET_H + +#include +#include + +class PopupHelpWidget : public QFrame +{ + Q_OBJECT +public: + explicit PopupHelpWidget(QWidget* parent); + ~PopupHelpWidget() override; + + void setOffset(const QPoint& offset); + void setPosition(Qt::Corner corner); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void showEvent(QShowEvent* event) override; + +private: + void alignWithParent(); + QPointer m_parentWindow; + QPointer m_appWindow; + + QPoint m_offset; + Qt::Corner m_corner; +}; + + +#endif //KEEPASSXC_POPUPHELPWIDGET_H diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp index b2a40bd23..ade1b6324 100644 --- a/src/keys/YkChallengeResponseKey.cpp +++ b/src/keys/YkChallengeResponseKey.cpp @@ -37,9 +37,9 @@ YkChallengeResponseKey::YkChallengeResponseKey(int slot, bool blocking) , m_slot(slot) , m_blocking(blocking) { - if (KEEPASSXC_MAIN_WINDOW) { - connect(this, SIGNAL(userInteractionRequired()), KEEPASSXC_MAIN_WINDOW, SLOT(showYubiKeyPopup())); - connect(this, SIGNAL(userConfirmed()), KEEPASSXC_MAIN_WINDOW, SLOT(hideYubiKeyPopup())); + if (getMainWindow()) { + connect(this, SIGNAL(userInteractionRequired()), getMainWindow(), SLOT(showYubiKeyPopup())); + connect(this, SIGNAL(userConfirmed()), getMainWindow(), SLOT(hideYubiKeyPopup())); } } diff --git a/src/main.cpp b/src/main.cpp index 9764c52d1..c811fe62c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -111,7 +111,6 @@ int main(int argc, char** argv) } MainWindow mainWindow; - app.setMainWindow(&mainWindow); QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString))); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index f48d0777c..0adeabd95 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -802,10 +802,22 @@ void TestGui::testSearch() auto* clearButton = searchWidget->findChild("clearIcon"); QVERIFY(!clearButton->isVisible()); + auto* helpButton = searchWidget->findChild("helpIcon"); + auto* helpPanel = searchWidget->findChild("SearchHelpWidget"); + QVERIFY(helpButton->isVisible()); + QVERIFY(!helpPanel->isVisible()); + // Enter search QTest::mouseClick(searchTextEdit, Qt::LeftButton); QTRY_VERIFY(searchTextEdit->hasFocus()); QTRY_VERIFY(!clearButton->isVisible()); + // Show/Hide search help + helpButton->trigger(); + QTRY_VERIFY(helpPanel->isVisible()); + QTest::mouseClick(searchTextEdit, Qt::LeftButton); + QTRY_VERIFY(helpPanel->isVisible()); + helpButton->trigger(); + QTRY_VERIFY(!helpPanel->isVisible()); // Search for "ZZZ" QTest::keyClicks(searchTextEdit, "ZZZ"); QTRY_COMPARE(searchTextEdit->text(), QString("ZZZ"));