diff options
author | Jonathan White <support@dmapps.us> | 2019-03-20 01:59:14 +0300 |
---|---|---|
committer | Jonathan White <support@dmapps.us> | 2019-03-20 01:59:14 +0300 |
commit | c51752df3913329d6fc78563c54ddd6ebd994b0f (patch) | |
tree | 54df6109d1459d50d6d7ac2c0378c54a11ef9c07 /src/gui/MainWindow.cpp | |
parent | 6fe821c30ec747e7fda0a8abe75d774d61a21054 (diff) | |
parent | 348a7e511118f48b9eb0527bf7b30837b01f03e0 (diff) |
Release 2.4.02.4.0
- New Database Wizard [#1952]
- Advanced Search [#1797]
- Automatic update checker [#2648]
- KeeShare database synchronization [#2109, #1992, #2738, #2742, #2746, #2739]
- Improve favicon fetching; transition to Duck-Duck-Go [#2795, #2011, #2439]
- Remove KeePassHttp support [#1752]
- CLI: output info to stderr for easier scripting [#2558]
- CLI: Add --quiet option [#2507]
- CLI: Add create command [#2540]
- CLI: Add recursive listing of entries [#2345]
- CLI: Fix stdin/stdout encoding on Windows [#2425]
- SSH Agent: Support OpenSSH for Windows [#1994]
- macOS: TouchID Quick Unlock [#1851]
- macOS: Multiple improvements; include CLI in DMG [#2165, #2331, #2583]
- Linux: Prevent Klipper from storing secrets in clipboard [#1969]
- Linux: Use polling based file watching for NFS [#2171]
- Linux: Enable use of browser plugin in Snap build [#2802]
- TOTP QR Code Generator [#1167]
- High-DPI Scaling for 4k screens [#2404]
- Make keyboard shortcuts more consistent [#2431]
- Warn user if deleting referenced entries [#1744]
- Allow toolbar to be hidden and repositioned [#1819, #2357]
- Increase max allowed database timeout to 12 hours [#2173]
- Password generator uses existing password length by default [#2318]
- Improve alert message box button labels [#2376]
- Show message when a database merge makes no changes [#2551]
- Browser Integration Enhancements [#1497, #2253, #1904, #2232, #1850, #2218, #2391, #2396, #2542, #2622, #2637, #2790]
- Overall Code Improvements [#2316, #2284, #2351, #2402, #2410, #2419, #2422, #2443, #2491, #2506, #2610, #2667, #2709, #2731]
Diffstat (limited to 'src/gui/MainWindow.cpp')
-rw-r--r-- | src/gui/MainWindow.cpp | 843 |
1 files changed, 445 insertions, 398 deletions
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f6cc2e4bb..6e3c96af0 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -20,10 +20,11 @@ #include "ui_MainWindow.h" #include <QCloseEvent> +#include <QDesktopServices> +#include <QFileInfo> #include <QMimeData> #include <QShortcut> #include <QTimer> -#include <QDesktopServices> #include "config-keepassx.h" @@ -32,54 +33,62 @@ #include "core/FilePath.h" #include "core/InactivityTimer.h" #include "core/Metadata.h" -#include "format/KeePass2Writer.h" +#include "core/Tools.h" #include "gui/AboutDialog.h" #include "gui/DatabaseWidget.h" -#include "gui/DatabaseRepairWidget.h" -#include "gui/FileDialog.h" -#include "gui/MessageBox.h" #include "gui/SearchWidget.h" +#include "keys/CompositeKey.h" +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" -#ifdef WITH_XC_HTTP -#include "http/Service.h" -#include "http/HttpSettings.h" -#include "http/OptionDialog.h" +#ifdef WITH_XC_NETWORKING +#include "gui/MessageBox.h" +#include "gui/UpdateCheckDialog.h" +#include "updatecheck/UpdateChecker.h" #endif #ifdef WITH_XC_SSHAGENT #include "sshagent/AgentSettingsPage.h" #include "sshagent/SSHAgent.h" #endif - +#if defined(WITH_XC_KEESHARE) +#include "keeshare/KeeShare.h" +#include "keeshare/SettingsPageKeeShare.h" +#endif #ifdef WITH_XC_BROWSER -#include "browser/NativeMessagingHost.h" -#include "browser/BrowserSettings.h" #include "browser/BrowserOptionDialog.h" +#include "browser/BrowserSettings.h" +#include "browser/NativeMessagingHost.h" #endif -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(QT_NO_DBUS) +#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) +#include "gui/MainWindowAdaptor.h" #include <QList> #include <QtDBus/QtDBus> -#include "gui/MainWindowAdaptor.h" #endif -#include "gui/SettingsWidget.h" +#include "gui/ApplicationSettingsWidget.h" #include "gui/PasswordGeneratorWidget.h" -#ifdef WITH_XC_HTTP -class HttpPlugin: public ISettingsPage +#include "touchid/TouchID.h" + +#ifdef WITH_XC_BROWSER +class BrowserPlugin : public ISettingsPage { public: - HttpPlugin(DatabaseTabWidget* tabWidget) + explicit BrowserPlugin(DatabaseTabWidget* tabWidget) { - m_service = new Service(tabWidget); + m_nativeMessagingHost = + QSharedPointer<NativeMessagingHost>(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); } - ~HttpPlugin() = default; + ~BrowserPlugin() + { + } QString name() override { - return QObject::tr("Legacy Browser Integration"); + return QObject::tr("Browser Integration"); } QIcon icon() override @@ -87,91 +96,51 @@ public: return FilePath::instance()->icon("apps", "internet-web-browser"); } - QWidget * createWidget() override + QWidget* createWidget() override { - OptionDialog* dlg = new OptionDialog(); - QObject::connect(dlg, SIGNAL(removeSharedEncryptionKeys()), m_service, SLOT(removeSharedEncryptionKeys())); - QObject::connect(dlg, SIGNAL(removeStoredPermissions()), m_service, SLOT(removeStoredPermissions())); + BrowserOptionDialog* dlg = new BrowserOptionDialog(); return dlg; } void loadSettings(QWidget* widget) override { - qobject_cast<OptionDialog*>(widget)->loadSettings(); + qobject_cast<BrowserOptionDialog*>(widget)->loadSettings(); } void saveSettings(QWidget* widget) override { - qobject_cast<OptionDialog*>(widget)->saveSettings(); - if (HttpSettings::isEnabled()) - m_service->start(); - else - m_service->stop(); - } -private: - Service* m_service; -}; -#endif - -#ifdef WITH_XC_BROWSER -class BrowserPlugin: public ISettingsPage -{ - public: - BrowserPlugin(DatabaseTabWidget* tabWidget) { - m_nativeMessagingHost = QSharedPointer<NativeMessagingHost>(new NativeMessagingHost(tabWidget, BrowserSettings::isEnabled())); - } - - ~BrowserPlugin() { - - } - - QString name() override - { - return QObject::tr("Browser Integration"); - } - - QIcon icon() override - { - return FilePath::instance()->icon("apps", "internet-web-browser"); - } - - QWidget* createWidget() override { - BrowserOptionDialog* dlg = new BrowserOptionDialog(); - QObject::connect(dlg, SIGNAL(removeSharedEncryptionKeys()), m_nativeMessagingHost.data(), SLOT(removeSharedEncryptionKeys())); - QObject::connect(dlg, SIGNAL(removeStoredPermissions()), m_nativeMessagingHost.data(), SLOT(removeStoredPermissions())); - return dlg; - } - - void loadSettings(QWidget* widget) override - { - qobject_cast<BrowserOptionDialog*>(widget)->loadSettings(); + qobject_cast<BrowserOptionDialog*>(widget)->saveSettings(); + if (browserSettings()->isEnabled()) { + m_nativeMessagingHost->run(); + } else { + m_nativeMessagingHost->stop(); } + } - void saveSettings(QWidget* widget) override - { - qobject_cast<BrowserOptionDialog*>(widget)->saveSettings(); - if (BrowserSettings::isEnabled()) { - m_nativeMessagingHost->run(); - } else { - m_nativeMessagingHost->stop(); - } - } - private: - QSharedPointer<NativeMessagingHost> m_nativeMessagingHost; +private: + QSharedPointer<NativeMessagingHost> m_nativeMessagingHost; }; #endif 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_MAC) && !defined(QT_NO_DBUS) + +#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) new MainWindowAdaptor(this); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject("/keepassxc", this); @@ -180,10 +149,8 @@ MainWindow::MainWindow() setAcceptDrops(true); - m_ui->toolBar->setContextMenuPolicy(Qt::PreventContextMenu); - // Setup the search widget in the toolbar - SearchWidget *search = new SearchWidget(); + auto* search = new SearchWidget(); search->connectSignals(m_actionMultiplexer); m_searchWidgetAction = m_ui->toolBar->addWidget(search); m_searchWidgetAction->setEnabled(false); @@ -191,23 +158,31 @@ MainWindow::MainWindow() m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); + restoreState(config()->get("GUI/MainWindowState").toByteArray()); #ifdef WITH_XC_BROWSER m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); #endif -#ifdef WITH_XC_HTTP - m_ui->settingsWidget->addSettingsPage(new HttpPlugin(m_ui->tabWidget)); -#endif + #ifdef WITH_XC_SSHAGENT SSHAgent::init(this); connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif +#if defined(WITH_XC_KEESHARE) + KeeShare::init(this); + m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); + connect(KeeShare::instance(), + SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), + SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); +#endif setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); - connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); - connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); - connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); + // clang-format off + connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); + connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); + connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); + // clang-format on m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); @@ -216,14 +191,13 @@ MainWindow::MainWindow() connect(m_ui->menuRecentDatabases, SIGNAL(aboutToShow()), this, SLOT(updateLastDatabasesMenu())); m_copyAdditionalAttributeActions = new QActionGroup(m_ui->menuEntryCopyAttribute); - m_actionMultiplexer.connect(m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), - SLOT(copyAttribute(QAction*))); - connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), - this, SLOT(updateCopyAttributesMenu())); + m_actionMultiplexer.connect( + m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*))); + connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu())); Qt::Key globalAutoTypeKey = static_cast<Qt::Key>(config()->get("GlobalAutoTypeKey").toInt()); - Qt::KeyboardModifiers globalAutoTypeModifiers = static_cast<Qt::KeyboardModifiers>( - config()->get("GlobalAutoTypeModifiers").toInt()); + Qt::KeyboardModifiers globalAutoTypeModifiers = + static_cast<Qt::KeyboardModifiers>(config()->get("GlobalAutoTypeModifiers").toInt()); if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); } @@ -231,14 +205,17 @@ MainWindow::MainWindow() m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); m_inactivityTimer = new InactivityTimer(this); - connect(m_inactivityTimer, SIGNAL(inactivityDetected()), - this, SLOT(lockDatabasesAfterInactivity())); + connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); +#ifdef WITH_XC_TOUCHID + m_touchIDinactivityTimer = new InactivityTimer(this); + connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity())); +#endif applySettingsChanges(); m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); - setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs); + setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L); setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); @@ -251,11 +228,37 @@ MainWindow::MainWindow() m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); - setShortcut(m_ui->actionEntryAutoType, QKeySequence::Paste, Qt::CTRL + Qt::Key_V); - m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::Key_U); - m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_U); + m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); + m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); + m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus + // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them + m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryOpenUrl->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyURL->setShortcutVisibleInContextMenu(true); +#endif + // Control window state new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_M, this, SLOT(hideWindow())); + // Control database tabs + new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(selectPreviousDatabaseTab())); + // Toggle password and username visibility in entry view + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden())); m_ui->actionDatabaseNew->setIcon(filePath()->icon("actions", "document-new")); m_ui->actionDatabaseOpen->setIcon(filePath()->icon("actions", "document-open")); @@ -263,58 +266,50 @@ MainWindow::MainWindow() m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); - m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key", false)); - m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt", false)); + m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); + m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt")); m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); - m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new", false)); - m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone", false)); - m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit", false)); - m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete", false)); - m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type", false)); - m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy", false)); - m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy", false)); - m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy", false)); + m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); + m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); + m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit")); + m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete")); + m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type")); + m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); + m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); + m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); - m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new", false)); - m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit", false)); - m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete", false)); - m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash", false)); + m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); + m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); + m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); + m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); - m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator", false)); + m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about")); + m_ui->actionCheckForUpdates->setIcon(filePath()->icon("actions", "system-software-update")); - m_actionMultiplexer.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), - this, SLOT(setMenuActionState(DatabaseWidget::Mode))); - m_actionMultiplexer.connect(SIGNAL(groupChanged()), - this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), - this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), - this, SLOT(showGroupContextMenu(QPoint))); - m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), - this, SLOT(showEntryContextMenu(QPoint))); + m_actionMultiplexer.connect( + SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); + m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), this, SLOT(showGroupContextMenu(QPoint))); + m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), this, SLOT(showEntryContextMenu(QPoint))); // Notify search when the active database changes or gets locked - connect(m_ui->tabWidget, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), - search, SLOT(databaseChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), - search, SLOT(databaseChanged())); - - connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), - SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), - SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), - SLOT(databaseTabChanged(int))); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), - SLOT(setMenuActionState())); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), - SLOT(databaseStatusChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), - SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, + SIGNAL(activateDatabaseChanged(DatabaseWidget*)), + search, + SLOT(databaseChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), search, SLOT(databaseChanged())); + + connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); @@ -322,73 +317,42 @@ MainWindow::MainWindow() connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); connect(m_ui->settingsWidget, SIGNAL(rejected()), SLOT(switchToDatabases())); - connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(newDatabase())); - connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(openDatabase())); - connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(saveDatabase())); - connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(saveDatabaseAs())); - connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(closeDatabase())); - connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(mergeDatabase())); - connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(changeMasterKey())); - connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(changeDatabaseSettings())); - connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(importCsv())); - connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(importKeePass1Database())); - connect(m_ui->actionRepairDatabase, SIGNAL(triggered()), this, - SLOT(repairDatabase())); - connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(exportToCsv())); - connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, - SLOT(lockDatabases())); + connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, SLOT(newDatabase())); + connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, SLOT(openDatabase())); + connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabase())); + connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseAs())); + connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); + connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); + connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); + connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); + connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); + connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); + connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); - m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), - SLOT(createEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), - SLOT(cloneEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), - SLOT(switchToEntryEdit())); - m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), - SLOT(deleteEntries())); - - m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), - SLOT(showTotp())); - m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), - SLOT(setupTotp())); - - m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), - SLOT(copyTotp())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), - SLOT(copyTitle())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), - SLOT(copyUsername())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), - SLOT(copyPassword())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), - SLOT(copyURL())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), - SLOT(copyNotes())); - m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), - SLOT(performAutoType())); - m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), - SLOT(openUrl())); - - m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), - SLOT(createGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), - SLOT(switchToGroupEdit())); - m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), - SLOT(deleteGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), - SLOT(emptyRecycleBin())); + m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); + m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); + + m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); + m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); + + m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); + m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); + m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); + + m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); + m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); @@ -398,20 +362,34 @@ MainWindow::MainWindow() connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); - connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToImportCsv())); + connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); connect(m_ui->actionBugReport, SIGNAL(triggered()), SLOT(openBugReportUrl())); -#ifdef Q_OS_MAC +#ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); #endif - connect(m_ui->tabWidget, SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), this, SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); +#ifdef WITH_XC_NETWORKING + connect(m_ui->actionCheckForUpdates, SIGNAL(triggered()), SLOT(showUpdateCheckDialog())); + connect(UpdateChecker::instance(), + SIGNAL(updateCheckFinished(bool, QString, bool)), + SLOT(hasUpdateAvailable(bool, QString, bool))); + QTimer::singleShot(3000, this, SLOT(showUpdateCheckStartup())); +#else + m_ui->actionCheckForUpdates->setVisible(false); +#endif + + // clang-format off + connect(m_ui->tabWidget, + SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), + this, + SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); + // clang-format on + connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); - connect(m_ui->tabWidget, SIGNAL(messageTab(QString,MessageWidget::MessageType)), this, SLOT(displayTabMessage(QString, MessageWidget::MessageType))); - connect(m_ui->tabWidget, SIGNAL(messageDismissTab()), this, SLOT(hideTabMessage())); m_screenLockListener = new ScreenLockListener(this); connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); @@ -419,31 +397,32 @@ MainWindow::MainWindow() updateTrayIcon(); if (config()->hasAccessError()) { - m_ui->globalMessageWidget->showMessage( - tr("Access error for config file %1").arg(config()->getFileName()), MessageWidget::Error); - } -#ifdef WITH_XC_HTTP - if (config()->get("Http/Enabled", false).toBool() && config()->get("Http/DeprecationNoticeShown", 0).toInt() < 3) { - // show message after global widget dismissed all messages - connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), this, SLOT(showKeePassHTTPDeprecationNotice())); + m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), + MessageWidget::Error); } -#endif -#if !defined(KEEPASSXC_BUILD_TYPE_RELEASE) - m_ui->globalMessageWidget->showMessage(tr("WARNING: You are using an unstable build of KeePassXC!\n" - "There is a high risk of corruption, maintain a backup of your databases.\n" - "This version is not meant for production use."), - MessageWidget::Warning, -1); +#if defined(KEEPASSXC_BUILD_TYPE_SNAPSHOT) + m_ui->globalMessageWidget->showMessage( + tr("WARNING: You are using an unstable build of KeePassXC!\n" + "There is a high risk of corruption, maintain a backup of your databases.\n" + "This version is not meant for production use."), + MessageWidget::Warning, + -1); +#elif defined(KEEPASSXC_BUILD_TYPE_PRE_RELEASE) + m_ui->globalMessageWidget->showMessage( + tr("NOTE: You are using a pre-release version of KeePassXC!\n" + "Expect some bugs and minor issues, this version is not meant for production use."), + MessageWidget::Information, + 15000); #elif (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) && QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) if (!config()->get("QtErrorMessageShown", false).toBool()) { - m_ui->globalMessageWidget->showMessage(tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" - "We recommend you use the AppImage available on our downloads page."), - MessageWidget::Warning, -1); + m_ui->globalMessageWidget->showMessage( + tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" + "We recommend you use the AppImage available on our downloads page."), + MessageWidget::Warning, + -1); config()->set("QtErrorMessageShown", true); } -#else - // Show the HTTP deprecation message if enabled above - emit m_ui->globalMessageWidget->hideAnimationFinished(); #endif } @@ -451,20 +430,6 @@ MainWindow::~MainWindow() { } -void MainWindow::showKeePassHTTPDeprecationNotice() -{ - int warningNum = config()->get("Http/DeprecationNoticeShown", 0).toInt(); - displayGlobalMessage(tr("<p>It looks like you are using KeePassHTTP for browser integration. " - "This feature has been deprecated and will be removed in the future.<br>" - "Please switch to KeePassXC-Browser instead! For help with migration, " - "visit our <a class=\"link\" href=\"https://keepassxc.org/docs/keepassxc-browser-migration\">" - "migration guide</a> (warning %1 of 3).</p>").arg(warningNum + 1), - MessageWidget::Warning, true, -1); - - config()->set("Http/DeprecationNoticeShown", warningNum + 1); - disconnect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), this, SLOT(showKeePassHTTPDeprecationNotice())); -} - void MainWindow::showErrorMessage(const QString& message) { m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); @@ -529,30 +494,60 @@ void MainWindow::clearLastDatabases() } } -void MainWindow::openDatabase(const QString& fileName, const QString& pw, const QString& keyFile) +void MainWindow::openDatabase(const QString& filePath, const QString& pw, const QString& keyFile) { - m_ui->tabWidget->openDatabase(fileName, pw, keyFile); + if (pw.isEmpty() && keyFile.isEmpty()) { + m_ui->tabWidget->addDatabaseTab(filePath); + return; + } + + auto db = QSharedPointer<Database>::create(); + auto key = QSharedPointer<CompositeKey>::create(); + if (!pw.isEmpty()) { + key->addKey(QSharedPointer<PasswordKey>::create(pw)); + } + if (!keyFile.isEmpty()) { + auto fileKey = QSharedPointer<FileKey>::create(); + fileKey->load(keyFile); + key->addKey(fileKey); + } + if (db->open(filePath, key, nullptr, false)) { + auto* dbWidget = new DatabaseWidget(db, this); + m_ui->tabWidget->addDatabaseTab(dbWidget); + dbWidget->switchToMainView(true); + } } void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) { int currentIndex = m_ui->stackedWidget->currentIndex(); + bool inDatabaseTabWidget = (currentIndex == DatabaseTabScreen); bool inWelcomeWidget = (currentIndex == WelcomeScreen); + bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; + + m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); + + m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + + m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); Q_ASSERT(dbWidget); - if (mode == DatabaseWidget::None) { + if (mode == DatabaseWidget::Mode::None) { mode = dbWidget->currentMode(); } switch (mode) { - case DatabaseWidget::ViewMode: { - //bool inSearch = dbWidget->isInSearchMode(); - bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1; - bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0; + case DatabaseWidget::Mode::ViewMode: { + // bool inSearch = dbWidget->isInSearchMode(); + bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && dbWidget->currentEntryHasFocus(); + bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && dbWidget->currentEntryHasFocus(); bool groupSelected = dbWidget->isGroupSelected(); bool recycleBinSelected = dbWidget->isRecycleBinSelected(); @@ -566,12 +561,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes()); m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected); - m_ui->menuEntryTotp->setEnabled(true); + m_ui->menuEntryTotp->setEnabled(singleEntrySelected); m_ui->actionEntryAutoType->setEnabled(singleEntrySelected); m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); @@ -588,9 +584,9 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) break; } - case DatabaseWidget::EditMode: - case DatabaseWidget::ImportMode: - case DatabaseWidget::LockedMode: { + case DatabaseWidget::Mode::EditMode: + case DatabaseWidget::Mode::ImportMode: + case DatabaseWidget::Mode::LockedMode: { const QList<QAction*> entryActions = m_ui->menuEntries->actions(); for (QAction* action : entryActions) { action->setEnabled(false); @@ -600,13 +596,6 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) for (QAction* action : groupActions) { action->setEnabled(false); } - m_ui->actionEntryCopyTitle->setEnabled(false); - m_ui->actionEntryCopyUsername->setEnabled(false); - m_ui->actionEntryCopyPassword->setEnabled(false); - m_ui->actionEntryCopyURL->setEnabled(false); - m_ui->actionEntryCopyNotes->setEnabled(false); - m_ui->menuEntryCopyAttribute->setEnabled(false); - m_ui->menuEntryTotp->setEnabled(false); m_ui->actionChangeMasterKey->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); @@ -622,8 +611,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) Q_ASSERT(false); } m_ui->actionDatabaseClose->setEnabled(true); - } - else { + } else { const QList<QAction*> entryActions = m_ui->menuEntries->actions(); for (QAction* action : entryActions) { action->setEnabled(false); @@ -633,13 +621,6 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) for (QAction* action : groupActions) { action->setEnabled(false); } - m_ui->actionEntryCopyTitle->setEnabled(false); - m_ui->actionEntryCopyUsername->setEnabled(false); - m_ui->actionEntryCopyPassword->setEnabled(false); - m_ui->actionEntryCopyURL->setEnabled(false); - m_ui->actionEntryCopyNotes->setEnabled(false); - m_ui->menuEntryCopyAttribute->setEnabled(false); - m_ui->menuEntryTotp->setEnabled(false); m_ui->actionChangeMasterKey->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); @@ -652,15 +633,6 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_searchWidgetAction->setEnabled(false); } - bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; - m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - - m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); - if ((currentIndex == PasswordGeneratorScreen) != m_ui->actionPasswordGenerator->isChecked()) { bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); m_ui->actionPasswordGenerator->toggle(); @@ -680,14 +652,11 @@ void MainWindow::updateWindowTitle() bool isModified = m_ui->tabWidget->isModified(tabWidgetIndex); if (stackedWidgetIndex == DatabaseTabScreen && tabWidgetIndex != -1) { - customWindowTitlePart = m_ui->tabWidget->tabText(tabWidgetIndex); + customWindowTitlePart = m_ui->tabWidget->tabName(tabWidgetIndex); if (isModified) { // remove asterisk '*' from title customWindowTitlePart.remove(customWindowTitlePart.size() - 1, 1); } - if (m_ui->tabWidget->readOnly(tabWidgetIndex)) { - customWindowTitlePart.append(QString(" [%1]").arg(tr("read-only"))); - } m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); } else if (stackedWidgetIndex == 1) { customWindowTitlePart = tr("Settings"); @@ -703,20 +672,66 @@ void MainWindow::updateWindowTitle() if (customWindowTitlePart.isEmpty() || stackedWidgetIndex == 1) { setWindowFilePath(""); } else { - setWindowFilePath(m_ui->tabWidget->databasePath(tabWidgetIndex)); + setWindowFilePath(m_ui->tabWidget->databaseWidgetFromIndex(tabWidgetIndex)->database()->filePath()); } - setWindowModified(isModified); - setWindowTitle(windowTitle); + setWindowModified(isModified); } void MainWindow::showAboutDialog() { - AboutDialog* aboutDialog = new AboutDialog(this); + auto* aboutDialog = new AboutDialog(this); aboutDialog->open(); } +void MainWindow::showUpdateCheckStartup() +{ +#ifdef WITH_XC_NETWORKING + if (!config()->get("UpdateCheckMessageShown", false).toBool()) { + auto result = + MessageBox::question(this, + tr("Check for updates on startup?"), + tr("Would you like KeePassXC to check for updates on startup?") + "\n\n" + + tr("You can always check for updates manually from the application menu."), + MessageBox::Yes | MessageBox::No, + MessageBox::Yes); + + config()->set("GUI/CheckForUpdates", (result == MessageBox::Yes)); + config()->set("UpdateCheckMessageShown", true); + } + + if (config()->get("GUI/CheckForUpdates", false).toBool()) { + updateCheck()->checkForUpdates(false); + } + +#endif +} + +void MainWindow::hasUpdateAvailable(bool hasUpdate, const QString& version, bool isManuallyRequested) +{ +#ifdef WITH_XC_NETWORKING + if (hasUpdate && !isManuallyRequested) { + auto* updateCheckDialog = new UpdateCheckDialog(this); + updateCheckDialog->showUpdateCheckResponse(hasUpdate, version); + updateCheckDialog->show(); + } +#else + Q_UNUSED(hasUpdate) + Q_UNUSED(version) + Q_UNUSED(isManuallyRequested) +#endif +} + +void MainWindow::showUpdateCheckDialog() +{ +#ifdef WITH_XC_NETWORKING + updateCheck()->checkForUpdates(true); + auto* updateCheckDialog = new UpdateCheckDialog(this); + updateCheckDialog->show(); +#endif +} + void MainWindow::openDonateUrl() { QDesktopServices::openUrl(QUrl("https://keepassxc.org/donate")); @@ -731,8 +746,7 @@ void MainWindow::switchToDatabases() { if (m_ui->tabWidget->currentIndex() == -1) { m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); - } - else { + } else { m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); } } @@ -750,13 +764,13 @@ void MainWindow::switchToSettings(bool enabled) void MainWindow::switchToPasswordGen(bool enabled) { if (enabled) { - m_ui->passwordGeneratorWidget->loadSettings(); - m_ui->passwordGeneratorWidget->regeneratePassword(); - m_ui->passwordGeneratorWidget->setStandaloneMode(true); - m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); + m_ui->passwordGeneratorWidget->loadSettings(); + m_ui->passwordGeneratorWidget->regeneratePassword(); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); + m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); } else { - m_ui->passwordGeneratorWidget->saveSettings(); - switchToDatabases(); + m_ui->passwordGeneratorWidget->saveSettings(); + switchToDatabases(); } } @@ -777,9 +791,9 @@ void MainWindow::switchToOpenDatabase() switchToDatabases(); } -void MainWindow::switchToDatabaseFile(QString file) +void MainWindow::switchToDatabaseFile(const QString& file) { - m_ui->tabWidget->openDatabase(file); + m_ui->tabWidget->addDatabaseTab(file); switchToDatabases(); } @@ -789,29 +803,69 @@ void MainWindow::switchToKeePass1Database() switchToDatabases(); } -void MainWindow::switchToImportCsv() +void MainWindow::switchToCsvImport() { m_ui->tabWidget->importCsv(); switchToDatabases(); } -void MainWindow::databaseStatusChanged(DatabaseWidget *) +void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget) { + Q_UNUSED(dbWidget); updateTrayIcon(); } +void MainWindow::selectNextDatabaseTab() +{ + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() + 1; + if (index >= m_ui->tabWidget->count()) { + m_ui->tabWidget->setCurrentIndex(0); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } +} + +void MainWindow::selectPreviousDatabaseTab() +{ + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() - 1; + if (index < 0) { + m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->count() - 1); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } +} + void MainWindow::databaseTabChanged(int tabIndex) { if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); - } - else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + } else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); } m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget()); } +void MainWindow::togglePasswordsHidden() +{ + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setPasswordsHidden(!dbWidget->isPasswordsHidden()); + } +} + +void MainWindow::toggleUsernamesHidden() +{ + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setUsernamesHidden(!dbWidget->isUsernamesHidden()); + } +} + void MainWindow::closeEvent(QCloseEvent* event) { // ignore double close events (happens on macOS when closing from the dock) @@ -820,17 +874,9 @@ void MainWindow::closeEvent(QCloseEvent* event) return; } - bool minimizeOnClose = isTrayIconEnabled() && - config()->get("GUI/MinimizeOnClose").toBool(); - if (minimizeOnClose && !m_appExitCalled) - { - event->accept(); + if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled) { + event->ignore(); hideWindow(); - - if (config()->get("security/lockdatabaseminimize").toBool()) { - m_ui->tabWidget->lockDatabases(); - } - return; } @@ -842,8 +888,7 @@ void MainWindow::closeEvent(QCloseEvent* event) event->accept(); QApplication::quit(); - } - else { + } else { event->ignore(); } } @@ -852,8 +897,7 @@ void MainWindow::changeEvent(QEvent* event) { if ((event->type() == QEvent::WindowStateChange) && isMinimized()) { if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() - && config()->get("GUI/MinimizeToTray").toBool()) - { + && config()->get("GUI/MinimizeToTray").toBool()) { event->ignore(); QTimer::singleShot(0, this, SLOT(hide())); } @@ -861,8 +905,7 @@ void MainWindow::changeEvent(QEvent* event) if (config()->get("security/lockdatabaseminimize").toBool()) { m_ui->tabWidget->lockDatabases(); } - } - else { + } else { QMainWindow::changeEvent(event); } } @@ -871,6 +914,7 @@ void MainWindow::saveWindowInformation() { if (isVisible()) { config()->set("GUI/MainWindowGeometry", saveGeometry()); + config()->set("GUI/MainWindowState", saveState()); } } @@ -881,20 +925,15 @@ bool MainWindow::saveLastDatabases() bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool(); if (openPreviousDatabasesOnStartup) { - connect(m_ui->tabWidget, SIGNAL(databaseWithFileClosed(QString)), - this, SLOT(rememberOpenDatabases(QString))); + connect( + m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); } - if (!m_ui->tabWidget->closeAllDatabases()) { - accept = false; - } - else { - accept = true; - } + accept = m_ui->tabWidget->closeAllDatabaseTabs(); if (openPreviousDatabasesOnStartup) { - disconnect(m_ui->tabWidget, SIGNAL(databaseWithFileClosed(QString)), - this, SLOT(rememberOpenDatabases(QString))); + disconnect( + m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); config()->set("LastOpenedDatabases", m_openDatabases); } @@ -911,7 +950,7 @@ void MainWindow::updateTrayIcon() QAction* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); -#ifdef Q_OS_MAC +#ifdef Q_OS_MACOS QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu); menu->addAction(actionQuit); @@ -919,24 +958,23 @@ void MainWindow::updateTrayIcon() #else menu->addAction(m_ui->actionQuit); - connect(m_trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + connect(m_trayIcon, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason))); #endif connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow())); m_trayIcon->setContextMenu(menu); - + m_trayIcon->setIcon(filePath()->trayIcon()); m_trayIcon->show(); } if (m_ui->tabWidget->hasLockableDatabases()) { m_trayIcon->setIcon(filePath()->trayIconUnlocked()); - } - else { + } else { m_trayIcon->setIcon(filePath()->trayIconLocked()); } - } - else { + } else { if (m_trayIcon) { m_trayIcon->hide(); delete m_trayIcon; @@ -959,8 +997,7 @@ void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard { if (!QKeySequence::keyBindings(standard).isEmpty()) { action->setShortcuts(standard); - } - else if (fallback != 0) { + } else if (fallback != 0) { action->setShortcut(QKeySequence(fallback)); } } @@ -980,11 +1017,34 @@ void MainWindow::applySettingsChanges() m_inactivityTimer->setInactivityTimeout(timeout); if (config()->get("security/lockdatabaseidle").toBool()) { m_inactivityTimer->activate(); - } - else { + } else { m_inactivityTimer->deactivate(); } +#ifdef WITH_XC_TOUCHID + // forget TouchID (in minutes) + timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000; + if (timeout <= 0) { + timeout = 30 * 60 * 1000; + } + + m_touchIDinactivityTimer->setInactivityTimeout(timeout); + if (config()->get("security/resettouchid").toBool()) { + m_touchIDinactivityTimer->activate(); + } else { + m_touchIDinactivityTimer->deactivate(); + } +#endif + + m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool()); + m_ui->toolBar->setMovable(config()->get("GUI/MovableToolbar").toBool()); + + bool isOk = false; + const auto toolButtonStyle = static_cast<Qt::ToolButtonStyle>(config()->get("GUI/ToolButtonStyle").toInt(&isOk)); + if (isOk) { + m_ui->toolBar->setToolButtonStyle(toolButtonStyle); + } + updateTrayIcon(); } @@ -998,14 +1058,19 @@ void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) void MainWindow::hideWindow() { saveWindowInformation(); -#if !defined(Q_OS_LINUX) && !defined(Q_OS_MAC) +#if !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS) // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at // the same time (which would happen if both minimize on startup and minimize to tray are set) // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. // TODO: Add an explanation for why this is also not done on Mac (or remove the check) setWindowState(windowState() | Qt::WindowMinimized); #endif - QTimer::singleShot(0, this, SLOT(hide())); + // Only hide if tray icon is active, otherwise window will be gone forever + if (isTrayIconEnabled()) { + hide(); + } else { + showMinimized(); + } if (config()->get("security/lockdatabaseminimize").toBool()) { m_ui->tabWidget->lockDatabases(); @@ -1019,17 +1084,19 @@ void MainWindow::toggleWindow() } else { bringToFront(); -#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) +#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) // re-register global D-Bus menu (needed on Ubuntu with Unity) // see https://github.com/keepassxreboot/keepassxc/issues/271 // and https://bugreports.qt.io/browse/QTBUG-58723 // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5 - if (!m_ui->menubar->isVisible()) { - QDBusMessage msg = QDBusMessage::createMethodCall( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar", - "com.canonical.AppMenu.Registrar", - "RegisterWindow"); + const static auto isDesktopSessionUnity = qgetenv("XDG_CURRENT_DESKTOP") == "Unity"; + + if (isDesktopSessionUnity && Tools::qtRuntimeVersion() < QT_VERSION_CHECK(5, 9, 0) + && !m_ui->menubar->isVisible()) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("com.canonical.AppMenu.Registrar"), + QStringLiteral("/com/canonical/AppMenu/Registrar"), + QStringLiteral("com.canonical.AppMenu.Registrar"), + QStringLiteral("RegisterWindow")); QList<QVariant> args; args << QVariant::fromValue(static_cast<uint32_t>(winId())) << QVariant::fromValue(QDBusObjectPath("/MenuBar/1")); @@ -1050,51 +1117,30 @@ void MainWindow::lockDatabasesAfterInactivity() m_ui->tabWidget->lockDatabases(); } -void MainWindow::repairDatabase() +void MainWindow::forgetTouchIDAfterInactivity() { - QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files")); - QString fileName = fileDialog()->getOpenFileName(this, tr("Open database"), QString(), - filter); - if (fileName.isEmpty()) { - return; - } - - QScopedPointer<QDialog> dialog(new QDialog(this)); - DatabaseRepairWidget* dbRepairWidget = new DatabaseRepairWidget(dialog.data()); - connect(dbRepairWidget, SIGNAL(success()), dialog.data(), SLOT(accept())); - connect(dbRepairWidget, SIGNAL(error()), dialog.data(), SLOT(reject())); - dbRepairWidget->load(fileName); - if (dialog->exec() == QDialog::Accepted && dbRepairWidget->database()) { - QString saveFileName = fileDialog()->getSaveFileName(this, tr("Save repaired database"), QString(), - tr("KeePass 2 Database").append(" (*.kdbx)"), - nullptr, 0, "kdbx"); - - if (!saveFileName.isEmpty()) { - KeePass2Writer writer; - writer.writeDatabase(saveFileName, dbRepairWidget->database()); - if (writer.hasError()) { - displayGlobalMessage( - tr("Writing the database failed.").append("\n").append(writer.errorString()), - MessageWidget::Error); - } - } - } +#ifdef WITH_XC_TOUCHID + TouchID::getInstance().reset(); +#endif } bool MainWindow::isTrayIconEnabled() const { - return config()->get("GUI/ShowTrayIcon").toBool() - && QSystemTrayIcon::isSystemTrayAvailable(); + return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); } -void MainWindow::displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton, +void MainWindow::displayGlobalMessage(const QString& text, + MessageWidget::MessageType type, + bool showClosebutton, int autoHideTimeout) { m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); } -void MainWindow::displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton, +void MainWindow::displayTabMessage(const QString& text, + MessageWidget::MessageType type, + bool showClosebutton, int autoHideTimeout) { m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); @@ -1105,17 +1151,12 @@ void MainWindow::hideGlobalMessage() m_ui->globalMessageWidget->hideMessage(); } -void MainWindow::hideTabMessage() -{ - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - m_ui->tabWidget->currentDatabaseWidget()->hideMessage(); - } -} - void MainWindow::showYubiKeyPopup() { - displayGlobalMessage(tr("Please touch the button on your YubiKey!"), MessageWidget::Information, - false, MessageWidget::DisableAutoHide); + displayGlobalMessage(tr("Please touch the button on your YubiKey!"), + MessageWidget::Information, + false, + MessageWidget::DisableAutoHide); setEnabled(false); } @@ -1128,7 +1169,7 @@ void MainWindow::hideYubiKeyPopup() void MainWindow::bringToFront() { ensurePolished(); - setWindowState(windowState() & ~Qt::WindowMinimized); + setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); show(); raise(); activateWindow(); @@ -1136,15 +1177,21 @@ void MainWindow::bringToFront() void MainWindow::handleScreenLock() { - if (config()->get("security/lockdatabasescreenlock").toBool()){ + if (config()->get("security/lockdatabasescreenlock").toBool()) { lockDatabasesAfterInactivity(); } + +#ifdef WITH_XC_TOUCHID + if (config()->get("security/resettouchidscreenlock").toBool()) { + forgetTouchIDAfterInactivity(); + } +#endif } QStringList MainWindow::kdbxFilesFromUrls(const QList<QUrl>& urls) { QStringList kdbxFiles; - for (const QUrl& url: urls) { + for (const QUrl& url : urls) { const QFileInfo fInfo(url.toLocalFile()); const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; if (isKdbxFile) { @@ -1174,7 +1221,7 @@ void MainWindow::dropEvent(QDropEvent* event) if (!kdbxFiles.isEmpty()) { event->acceptProposedAction(); } - for (const QString& kdbxFile: kdbxFiles) { + for (const QString& kdbxFile : kdbxFiles) { openDatabase(kdbxFile); } } @@ -1182,7 +1229,7 @@ void MainWindow::dropEvent(QDropEvent* event) void MainWindow::closeAllDatabases() { - m_ui->tabWidget->closeAllDatabases(); + m_ui->tabWidget->closeAllDatabaseTabs(); } void MainWindow::lockAllDatabases() |