Skip to content

Commit

Permalink
Allow selecting any open database in unlock dialog
Browse files Browse the repository at this point in the history
When showing the database unlock dialog from browser or auto-type and
there's more than one open database, add tabs to the unlock dialog so
that the user can choose which database to unlock.

This implements part of [1] and improves the UX when working with
multiple databases after version 2.6 switched to using the dialog for
browser unlock.

Make the DatabaseOpenDialog window Application-Modal so that it blocks
input to the main UI when the dialog is open. This reduces corner cases
by avoiding the possibility of databases getting closed or unlocked
behind the open dialog.

[1] #2322
  • Loading branch information
aswild committed Aug 19, 2021
1 parent 5e68cd2 commit 1fbfc75
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 25 deletions.
125 changes: 108 additions & 17 deletions src/gui/DatabaseOpenDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,124 @@
#include "DatabaseOpenDialog.h"

#include "DatabaseOpenWidget.h"
#include "DatabaseTabWidget.h"
#include "DatabaseWidget.h"

#include <QLayout>

#include <QFileInfo>
#include <QShortcut>

#ifdef Q_OS_WIN
#include <QtPlatformHeaders/QWindowsWindowFunctions>
#endif

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)
Expand All @@ -77,13 +153,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<Database> DatabaseOpenDialog::database()
QSharedPointer<Database> DatabaseOpenDialog::database() const
{
return m_db;
}
Expand All @@ -98,6 +182,13 @@ void DatabaseOpenDialog::complete(bool accepted)
} else {
reject();
}
emit dialogFinished(accepted, m_dbWidget);

if (m_intent != Intent::Merge) {
// Update the current database in the main UI to match what we just unlocked
auto* tabWidget = qobject_cast<DatabaseTabWidget*>(parentWidget());
tabWidget->setCurrentIndex(tabWidget->indexOf(m_currentDbWidget));
}

emit dialogFinished(accepted, m_currentDbWidget);
clearForms();
}
16 changes: 12 additions & 4 deletions src/gui/DatabaseOpenDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
#include "core/Global.h"

#include <QDialog>
#include <QList>
#include <QPointer>
#include <QTabBar>

class Database;
class DatabaseWidget;
Expand All @@ -41,23 +43,29 @@ class DatabaseOpenDialog : public QDialog
};

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> database();
QSharedPointer<Database> database() const;
void clearForms();

signals:
void dialogFinished(bool accepted, DatabaseWidget* dbWidget);

public slots:
void complete(bool accepted);
void tabChanged(int index);

private:
void selectTabOffset(int offset);

QPointer<DatabaseOpenWidget> m_view;
QPointer<QTabBar> m_tabBar;
QSharedPointer<Database> m_db;
QPointer<DatabaseWidget> m_dbWidget;
QList<QPointer<DatabaseWidget>> m_tabDbWidgets;
QPointer<DatabaseWidget> m_currentDbWidget;
Intent m_intent = Intent::None;
};

Expand Down
40 changes: 36 additions & 4 deletions src/gui/DatabaseTabWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,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->setTarget(dbWidget, filePath);
displayUnlockDialog();
}

/**
* Unlock a database with an unlock popup dialog.
* The dialog allows the user to select any open & unlocked database.
*
* @param intent intent for unlocking
*/
void DatabaseTabWidget::unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent)
{
m_databaseOpenDialog->clearForms();
m_databaseOpenDialog->setIntent(intent);
m_databaseOpenDialog->setFilePath(filePath);

// 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);
Expand Down Expand Up @@ -740,14 +772,14 @@ void DatabaseTabWidget::performGlobalAutoType()
if (config()->get(Config::Security_RelockAutoType).toBool()) {
m_dbWidgetPendingLock = currentDatabaseWidget();
}
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::AutoType);
unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::AutoType);
}
}

void DatabaseTabWidget::performBrowserUnlock()
{
auto dbWidget = currentDatabaseWidget();
if (dbWidget && dbWidget->isLocked()) {
unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::Browser);
unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::Browser);
}
}
2 changes: 2 additions & 0 deletions src/gui/DatabaseTabWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public slots:
void closeDatabaseFromSender();
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent);
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath);
void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent);
void relockPendingDatabase();

void showDatabaseSecurity();
Expand Down Expand Up @@ -104,6 +105,7 @@ private slots:
QSharedPointer<Database> execNewDatabaseWizard();
void updateLastDatabases(const QString& filename);
bool warnOnExport();
void displayUnlockDialog();

QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock;
Expand Down

0 comments on commit 1fbfc75

Please sign in to comment.