diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp index b322a058..cfa0fcad 100644 --- a/src/gui/DatabaseOpenDialog.cpp +++ b/src/gui/DatabaseOpenDialog.cpp @@ -18,9 +18,12 @@ #include "DatabaseOpenDialog.h" #include "DatabaseOpenWidget.h" +#include "DatabaseTabWidget.h" #include "DatabaseWidget.h" +#include #include +#include #ifdef Q_OS_WIN #include @@ -29,37 +32,109 @@ DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent) : QDialog(parent) , m_view(new DatabaseOpenWidget(this)) + , m_tabBar(new QTabBar(this)) { setWindowTitle(tr("Unlock Database - KeePassXC")); setWindowFlags(Qt::Dialog | Qt::WindowStaysOnTopHint); + // block input to the main window/application while the dialog is open + setWindowModality(Qt::ApplicationModal); #ifdef Q_OS_WIN QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow); #endif - connect(m_view, SIGNAL(dialogFinished(bool)), this, SLOT(complete(bool))); + connect(m_view, &DatabaseOpenWidget::dialogFinished, this, &DatabaseOpenDialog::complete); + + m_tabBar->setAutoHide(true); + m_tabBar->setExpanding(false); + connect(m_tabBar, &QTabBar::currentChanged, this, &DatabaseOpenDialog::tabChanged); + auto* layout = new QVBoxLayout(); - layout->setMargin(0); - setLayout(layout); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tabBar); layout->addWidget(m_view); + setLayout(layout); setMinimumWidth(700); + + // set up Ctrl+PageUp and Ctrl+PageDown shortcuts to cycle tabs + auto* shortcut = new QShortcut(Qt::CTRL + Qt::Key_PageUp, this); + shortcut->setContext(Qt::WidgetWithChildrenShortcut); + connect(shortcut, &QShortcut::activated, this, [this]() { selectTabOffset(-1); }); + shortcut = new QShortcut(Qt::CTRL + Qt::Key_PageDown, this); + shortcut->setContext(Qt::WidgetWithChildrenShortcut); + connect(shortcut, &QShortcut::activated, this, [this]() { selectTabOffset(1); }); } -void DatabaseOpenDialog::setFilePath(const QString& filePath) +void DatabaseOpenDialog::selectTabOffset(int offset) { - m_view->load(filePath); + if (offset == 0 || m_tabBar->count() <= 1) { + return; + } + int tab = m_tabBar->currentIndex() + offset; + int last = m_tabBar->count() - 1; + if (tab < 0) { + tab = last; + } else if (tab > last) { + tab = 0; + } + m_tabBar->setCurrentIndex(tab); +} + +void DatabaseOpenDialog::addDatabaseTab(DatabaseWidget* dbWidget) +{ + Q_ASSERT(dbWidget); + if (!dbWidget) { + return; + } + + // important - we must add the DB widget first, because addTab will fire + // tabChanged immediately which will look for a dbWidget in the list + m_tabDbWidgets.append(dbWidget); + QFileInfo fileInfo(dbWidget->database()->filePath()); + m_tabBar->addTab(fileInfo.fileName()); + Q_ASSERT(m_tabDbWidgets.count() == m_tabBar->count()); +} + +void DatabaseOpenDialog::setActiveDatabaseTab(DatabaseWidget* dbWidget) +{ + if (!dbWidget) { + return; + } + int index = m_tabDbWidgets.indexOf(dbWidget); + if (index != -1) { + m_tabBar->setCurrentIndex(index); + } +} + +void DatabaseOpenDialog::tabChanged(int index) +{ + if (index < 0 || index >= m_tabDbWidgets.count()) { + return; + } + + if (m_tabDbWidgets.count() == m_tabBar->count()) { + DatabaseWidget* dbWidget = m_tabDbWidgets[index]; + setTarget(dbWidget, dbWidget->database()->filePath()); + } else { + // if these list sizes don't match, there's a bug somewhere nearby + qWarning("DatabaseOpenDialog: mismatch between tab count %d and DB count %d", + m_tabBar->count(), + m_tabDbWidgets.count()); + } } /** - * Set target DatabaseWidget to which signals are connected. - * - * @param dbWidget database widget + * Sets the target DB and reloads the UI. */ -void DatabaseOpenDialog::setTargetDatabaseWidget(DatabaseWidget* dbWidget) +void DatabaseOpenDialog::setTarget(DatabaseWidget* dbWidget, const QString& filePath) { - if (m_dbWidget) { - disconnect(this, nullptr, m_dbWidget, nullptr); + // reconnect finished signal to new dbWidget, then reload the UI + if (m_currentDbWidget) { + disconnect(this, &DatabaseOpenDialog::dialogFinished, m_currentDbWidget, nullptr); } - m_dbWidget = dbWidget; connect(this, &DatabaseOpenDialog::dialogFinished, dbWidget, &DatabaseWidget::unlockDatabase); + + m_currentDbWidget = dbWidget; + m_view->load(filePath); } void DatabaseOpenDialog::setIntent(DatabaseOpenDialog::Intent intent) @@ -77,13 +152,21 @@ void DatabaseOpenDialog::clearForms() m_view->clearForms(); m_db.reset(); m_intent = Intent::None; - if (m_dbWidget) { - disconnect(this, nullptr, m_dbWidget, nullptr); - m_dbWidget = nullptr; + if (m_currentDbWidget) { + disconnect(this, &DatabaseOpenDialog::dialogFinished, m_currentDbWidget, nullptr); } + m_currentDbWidget.clear(); + m_tabDbWidgets.clear(); + + // block signals while removing tabs so that tabChanged doesn't get called + m_tabBar->blockSignals(true); + while (m_tabBar->count() > 0) { + m_tabBar->removeTab(0); + } + m_tabBar->blockSignals(false); } -QSharedPointer DatabaseOpenDialog::database() +QSharedPointer DatabaseOpenDialog::database() const { return m_db; } @@ -98,6 +181,7 @@ void DatabaseOpenDialog::complete(bool accepted) } else { reject(); } - emit dialogFinished(accepted, m_dbWidget); + + emit dialogFinished(accepted, m_currentDbWidget); clearForms(); } diff --git a/src/gui/DatabaseOpenDialog.h b/src/gui/DatabaseOpenDialog.h index 44476b87..5fcee76a 100644 --- a/src/gui/DatabaseOpenDialog.h +++ b/src/gui/DatabaseOpenDialog.h @@ -21,7 +21,9 @@ #include "core/Global.h" #include +#include #include +#include class Database; class DatabaseWidget; @@ -41,11 +43,12 @@ public: }; explicit DatabaseOpenDialog(QWidget* parent = nullptr); - void setFilePath(const QString& filePath); - void setTargetDatabaseWidget(DatabaseWidget* dbWidget); + void setTarget(DatabaseWidget* dbWidget, const QString& filePath); + void addDatabaseTab(DatabaseWidget* dbWidget); + void setActiveDatabaseTab(DatabaseWidget* dbWidget); void setIntent(Intent intent); Intent intent() const; - QSharedPointer database(); + QSharedPointer database() const; void clearForms(); signals: @@ -53,11 +56,16 @@ signals: public slots: void complete(bool accepted); + void tabChanged(int index); private: + void selectTabOffset(int offset); + QPointer m_view; + QPointer m_tabBar; QSharedPointer m_db; - QPointer m_dbWidget; + QList> m_tabDbWidgets; + QPointer m_currentDbWidget; Intent m_intent = Intent::None; }; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index ce71588c..5b706729 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -51,7 +51,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase())); connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase())); connect(m_databaseOpenDialog.data(), &DatabaseOpenDialog::dialogFinished, - this, &DatabaseTabWidget::databaseUnlockDialogFinished); + this, &DatabaseTabWidget::handleDatabaseUnlockDialogFinished); // clang-format on #ifdef Q_OS_MACOS @@ -664,11 +664,43 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath) { - m_databaseOpenDialog->setTargetDatabaseWidget(dbWidget); + m_databaseOpenDialog->clearForms(); m_databaseOpenDialog->setIntent(intent); - m_databaseOpenDialog->setFilePath(filePath); + m_databaseOpenDialog->setTarget(dbWidget, filePath); + displayUnlockDialog(); +} +/** + * Unlock a database with an unlock popup dialog. + * The dialog allows the user to select any open & locked database. + * + * @param intent intent for unlocking + */ +void DatabaseTabWidget::unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent) +{ + m_databaseOpenDialog->clearForms(); + m_databaseOpenDialog->setIntent(intent); + + // add a tab to the dialog for each open unlocked database + for (int i = 0, c = count(); i < c; ++i) { + auto* dbWidget = databaseWidgetFromIndex(i); + if (dbWidget && dbWidget->isLocked()) { + m_databaseOpenDialog->addDatabaseTab(dbWidget); + } + } + // default to the current tab + m_databaseOpenDialog->setActiveDatabaseTab(currentDatabaseWidget()); + displayUnlockDialog(); +} + +/** + * Display the unlock dialog after it's been initialized. + * This is an internal method, it should only be called by unlockDatabaseInDialog or unlockAnyDatabaseInDialog. + */ +void DatabaseTabWidget::displayUnlockDialog() +{ #ifdef Q_OS_MACOS + auto intent = m_databaseOpenDialog->intent(); if (intent == DatabaseOpenDialog::Intent::AutoType || intent == DatabaseOpenDialog::Intent::Browser) { macUtils()->raiseOwnWindow(); Tools::wait(200); @@ -680,6 +712,29 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget, m_databaseOpenDialog->activateWindow(); } +/** + * Actions to take when the unlock dialog has completed. + */ +void DatabaseTabWidget::handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget) +{ + // change the active tab to the database that was just unlocked in the dialog + auto intent = m_databaseOpenDialog->intent(); + if (accepted && intent != DatabaseOpenDialog::Intent::Merge) { + int index = indexOf(dbWidget); + if (index != -1) { + setCurrentIndex(index); + } + } + + // if unlocked for AutoType, set pending lock flag if needed + if (intent == DatabaseOpenDialog::Intent::AutoType && config()->get(Config::Security_RelockAutoType).toBool()) { + m_dbWidgetPendingLock = dbWidget; + } + + // signal other objects that the dialog finished + emit databaseUnlockDialogFinished(accepted, dbWidget); +} + /** * This function relock the pending database when autotype has been performed successfully * A database is marked as pending when it's unlocked after a global Auto-Type invocation @@ -737,23 +792,26 @@ void DatabaseTabWidget::emitDatabaseLockChanged() void DatabaseTabWidget::performGlobalAutoType() { - QList> unlockedDatabases; - - for (int i = 0, c = count(); i < c; ++i) { - auto* dbWidget = databaseWidgetFromIndex(i); - if (!dbWidget->isLocked()) { - unlockedDatabases.append(dbWidget->database()); + auto currentDbWidget = currentDatabaseWidget(); + if (!currentDbWidget) { + // no open databases, nothing to do + return; + } else if (currentDbWidget->isLocked()) { + // Current database tab is locked, match behavior of browser unlock - prompt with + // the unlock dialog even if there are additional unlocked open database tabs. + unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::AutoType); + } else { + // current database is unlocked, use it for AutoType along with any other unlocked databases + QList> unlockedDatabases; + for (int i = 0, c = count(); i < c; ++i) { + auto* dbWidget = databaseWidgetFromIndex(i); + if (!dbWidget->isLocked()) { + unlockedDatabases.append(dbWidget->database()); + } } - } - // TODO: allow for database selection during Auto-Type instead of using the current tab - if (!unlockedDatabases.isEmpty()) { + Q_ASSERT(!unlockedDatabases.isEmpty()); autoType()->performGlobalAutoType(unlockedDatabases); - } else if (count() > 0) { - if (config()->get(Config::Security_RelockAutoType).toBool()) { - m_dbWidgetPendingLock = currentDatabaseWidget(); - } - unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::AutoType); } } @@ -761,6 +819,6 @@ void DatabaseTabWidget::performBrowserUnlock() { auto dbWidget = currentDatabaseWidget(); if (dbWidget && dbWidget->isLocked()) { - unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::Browser); + unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::Browser); } } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 583b9c07..5afed77b 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -99,11 +99,14 @@ private slots: void toggleTabbar(); void emitActiveDatabaseChanged(); void emitDatabaseLockChanged(); + void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget); private: QSharedPointer execNewDatabaseWizard(); void updateLastDatabases(const QString& filename); bool warnOnExport(); + void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent); + void displayUnlockDialog(); QPointer m_dbWidgetStateSync; QPointer m_dbWidgetPendingLock; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 9daae9f8..7ace475b 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -186,6 +186,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged())); + connect(this, SIGNAL(requestGlobalAutoType()), parent, SLOT(performGlobalAutoType())); // clang-format on connectDatabaseSignals(); @@ -1109,9 +1110,10 @@ void DatabaseWidget::unlockDatabase(bool accepted) } if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::AutoType) { - QList> dbList; - dbList.append(m_db); - autoType()->performGlobalAutoType(dbList); + // Rather than starting AutoType directly for this database, signal the parent DatabaseTabWidget to + // restart AutoType now that this database is unlocked, so that other open+unlocked databases + // can be included in the search. + emit requestGlobalAutoType(); } } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index ae3fad67..957b8290 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -151,6 +151,7 @@ signals: void previewSplitterSizesChanged(); void entryViewStateChanged(); void clearSearch(); + void requestGlobalAutoType(); public slots: bool lock();