diff options
Diffstat (limited to 'src/gui/MainWindow.cpp')
-rw-r--r-- | src/gui/MainWindow.cpp | 722 |
1 files changed, 496 insertions, 226 deletions
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 6452c051b..9751a3e77 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -31,23 +31,26 @@ #include "autotype/AutoType.h" #include "core/Config.h" -#include "core/FilePath.h" #include "core/InactivityTimer.h" #include "core/Metadata.h" +#include "core/Resources.h" #include "core/Tools.h" #include "gui/AboutDialog.h" #include "gui/DatabaseWidget.h" +#include "gui/MessageBox.h" #include "gui/SearchWidget.h" #include "keys/CompositeKey.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" #ifdef Q_OS_MACOS -#include "macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" +#ifdef WITH_XC_TOUCHID +#include "touchid/TouchID.h" +#endif #endif #ifdef WITH_XC_UPDATECHECK -#include "gui/MessageBox.h" #include "gui/UpdateCheckDialog.h" #include "updatecheck/UpdateChecker.h" #endif @@ -56,7 +59,7 @@ #include "sshagent/AgentSettingsPage.h" #include "sshagent/SSHAgent.h" #endif -#if defined(WITH_XC_KEESHARE) +#ifdef WITH_XC_KEESHARE #include "keeshare/KeeShare.h" #include "keeshare/SettingsPageKeeShare.h" #endif @@ -65,10 +68,13 @@ #include "fdosecrets/FdoSecretsPlugin.h" #endif +#ifdef WITH_XC_YUBIKEY +#include "keys/drivers/YubiKey.h" +#endif + #ifdef WITH_XC_BROWSER -#include "browser/BrowserOptionDialog.h" -#include "browser/BrowserSettings.h" -#include "browser/NativeMessagingHost.h" +#include "browser/BrowserService.h" +#include "browser/BrowserSettingsPage.h" #endif #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) @@ -77,61 +83,6 @@ #include <QtDBus/QtDBus> #endif -#include "gui/ApplicationSettingsWidget.h" -#include "gui/PasswordGeneratorWidget.h" - -#include "touchid/TouchID.h" - -#ifdef WITH_XC_BROWSER -class BrowserPlugin : public ISettingsPage -{ -public: - explicit 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(); - return dlg; - } - - void loadSettings(QWidget* widget) override - { - qobject_cast<BrowserOptionDialog*>(widget)->loadSettings(); - } - - 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; -}; -#endif - const QString MainWindow::BaseWindowTitle = "KeePassXC"; MainWindow* g_MainWindow = nullptr; @@ -156,6 +107,10 @@ MainWindow::MainWindow() setAcceptDrops(true); + if (config()->get(Config::GUI_CompactMode).toBool()) { + m_ui->toolBar->setIconSize({20, 20}); + } + // Setup the search widget in the toolbar m_searchWidget = new SearchWidget(); m_searchWidget->connectSignals(m_actionMultiplexer); @@ -175,6 +130,10 @@ MainWindow::MainWindow() m_entryContextMenu->addAction(m_ui->actionEntryEdit); m_entryContextMenu->addAction(m_ui->actionEntryClone); m_entryContextMenu->addAction(m_ui->actionEntryDelete); + m_entryContextMenu->addAction(m_ui->actionEntryNew); + m_entryContextMenu->addSeparator(); + m_entryContextMenu->addAction(m_ui->actionEntryMoveUp); + m_entryContextMenu->addAction(m_ui->actionEntryMoveDown); m_entryContextMenu->addSeparator(); m_entryContextMenu->addAction(m_ui->actionEntryOpenUrl); m_entryContextMenu->addAction(m_ui->actionEntryDownloadIcon); @@ -182,18 +141,38 @@ MainWindow::MainWindow() m_entryNewContextMenu = new QMenu(this); m_entryNewContextMenu->addAction(m_ui->actionEntryNew); - restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); - restoreState(config()->get("GUI/MainWindowState").toByteArray()); + restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray()); + restoreState(config()->get(Config::GUI_MainWindowState).toByteArray()); #ifdef WITH_XC_BROWSER - m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); + m_ui->settingsWidget->addSettingsPage(new BrowserSettingsPage()); + connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, browserService(), &BrowserService::databaseLocked); + connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, browserService(), &BrowserService::databaseUnlocked); + connect(m_ui->tabWidget, + &DatabaseTabWidget::activateDatabaseChanged, + browserService(), + &BrowserService::activeDatabaseChanged); + connect( + browserService(), &BrowserService::requestUnlock, m_ui->tabWidget, &DatabaseTabWidget::performBrowserUnlock); #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)); + connect(sshAgent(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); + connect(sshAgent(), SIGNAL(enabledChanged(bool)), this, SLOT(agentEnabled(bool))); + m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage()); + + m_entryContextMenu->addSeparator(); + m_entryContextMenu->addAction(m_ui->actionEntryAddToAgent); + m_entryContextMenu->addAction(m_ui->actionEntryRemoveFromAgent); + + m_ui->actionEntryAddToAgent->setIcon(resources()->icon("utilities-terminal")); + m_ui->actionEntryRemoveFromAgent->setIcon(resources()->icon("utilities-terminal")); #endif + m_ui->actionEntryAddToAgent->setVisible(false); + m_ui->actionEntryRemoveFromAgent->setVisible(false); + + initViewMenu(); + #if defined(WITH_XC_KEESHARE) KeeShare::init(this); m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); @@ -211,13 +190,14 @@ MainWindow::MainWindow() m_ui->settingsWidget->addSettingsPage(fdoSS); #endif - setWindowIcon(filePath()->applicationIcon()); - m_ui->globalMessageWidget->setHidden(true); - // 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 +#ifdef WITH_XC_YUBIKEY + connect(YubiKey::instance(), SIGNAL(userInteractionRequest()), SLOT(showYubiKeyPopup()), Qt::QueuedConnection); + connect(YubiKey::instance(), SIGNAL(challengeCompleted()), SLOT(hideYubiKeyPopup()), Qt::QueuedConnection); +#endif + + setWindowIcon(resources()->applicationIcon()); + m_ui->globalMessageWidget->hideMessage(); + connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); @@ -230,13 +210,16 @@ MainWindow::MainWindow() 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::Key globalAutoTypeKey = static_cast<Qt::Key>(config()->get(Config::GlobalAutoTypeKey).toInt()); Qt::KeyboardModifiers globalAutoTypeModifiers = - static_cast<Qt::KeyboardModifiers>(config()->get("GlobalAutoTypeModifiers").toInt()); + static_cast<Qt::KeyboardModifiers>(config()->get(Config::GlobalAutoTypeModifiers).toInt()); if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); } + m_ui->toolbarSeparator->setVisible(false); + m_showToolbarSeparator = config()->get(Config::GUI_ApplicationTheme).toString() != "classic"; + m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); m_inactivityTimer = new InactivityTimer(this); @@ -262,11 +245,15 @@ MainWindow::MainWindow() m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D); m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); + m_ui->actionEntryMoveUp->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Up); + m_ui->actionEntryMoveDown->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Down); m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); 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); + m_ui->actionEntryAddToAgent->setShortcut(Qt::CTRL + Qt::Key_H); + m_ui->actionEntryRemoveFromAgent->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_H); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus @@ -278,11 +265,15 @@ MainWindow::MainWindow() m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryMoveUp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryMoveDown->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); + m_ui->actionEntryAddToAgent->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryRemoveFromAgent->setShortcutVisibleInContextMenu(true); #endif connect(m_ui->menuEntries, SIGNAL(aboutToShow()), SLOT(obtainContextFocusLock())); @@ -295,48 +286,99 @@ MainWindow::MainWindow() connect(m_ui->menuGroups, SIGNAL(aboutToHide()), SLOT(releaseContextFocusLock())); // Control window state - new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); + new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(minimizeOrHide())); 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_PageDown, this, SLOT(selectNextDatabaseTab())); new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectPreviousDatabaseTab())); + + // Tab selection by number, Windows uses Ctrl, macOS uses Command, + // and Linux uses Alt to emulate a browser-like experience + auto dbTabModifier = Qt::CTRL; +#ifdef Q_OS_LINUX + dbTabModifier = Qt::ALT; +#endif + auto shortcut = new QShortcut(dbTabModifier + Qt::Key_1, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(0); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_2, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(1); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_3, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(2); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_4, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(3); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_5, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(4); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_6, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(5); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_7, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(6); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_8, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(7); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_9, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(m_ui->tabWidget->count() - 1); }); + + // Allow for direct focus of search, group view, and entry view + shortcut = new QShortcut(Qt::Key_F1, this); + connect(shortcut, SIGNAL(activated()), m_searchWidget, SLOT(searchFocus())); + shortcut = new QShortcut(Qt::Key_F2, this); + m_actionMultiplexer.connect(shortcut, SIGNAL(activated()), SLOT(focusOnGroups())); + shortcut = new QShortcut(Qt::Key_F3, this); + m_actionMultiplexer.connect(shortcut, SIGNAL(activated()), SLOT(focusOnEntries())); + // 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")); - m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); - 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")); - m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "database-lock")); - m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); - - 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->actionEntryDownloadIcon->setIcon(filePath()->icon("actions", "favicon-download")); - - 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->actionGroupDownloadFavicons->setIcon(filePath()->icon("actions", "favicon-download")); - - m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); - 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_ui->actionDatabaseNew->setIcon(resources()->icon("document-new")); + m_ui->actionDatabaseOpen->setIcon(resources()->icon("document-open")); + m_ui->menuRecentDatabases->setIcon(resources()->icon("document-open-recent")); + m_ui->actionDatabaseSave->setIcon(resources()->icon("document-save")); + m_ui->actionDatabaseSaveAs->setIcon(resources()->icon("document-save-as")); + m_ui->actionDatabaseSaveBackup->setIcon(resources()->icon("document-save-copy")); + m_ui->actionDatabaseClose->setIcon(resources()->icon("document-close")); + m_ui->actionReports->setIcon(resources()->icon("reports")); + m_ui->actionDatabaseSettings->setIcon(resources()->icon("document-edit")); + m_ui->actionDatabaseSecurity->setIcon(resources()->icon("database-change-key")); + m_ui->actionLockDatabases->setIcon(resources()->icon("database-lock")); + m_ui->actionQuit->setIcon(resources()->icon("application-exit")); + m_ui->actionDatabaseMerge->setIcon(resources()->icon("database-merge")); + m_ui->menuImport->setIcon(resources()->icon("document-import")); + m_ui->menuExport->setIcon(resources()->icon("document-export")); + + m_ui->actionEntryNew->setIcon(resources()->icon("entry-new")); + m_ui->actionEntryClone->setIcon(resources()->icon("entry-clone")); + m_ui->actionEntryEdit->setIcon(resources()->icon("entry-edit")); + m_ui->actionEntryDelete->setIcon(resources()->icon("entry-delete")); + m_ui->actionEntryAutoType->setIcon(resources()->icon("auto-type")); + m_ui->actionEntryMoveUp->setIcon(resources()->icon("move-up")); + m_ui->actionEntryMoveDown->setIcon(resources()->icon("move-down")); + m_ui->actionEntryCopyUsername->setIcon(resources()->icon("username-copy")); + m_ui->actionEntryCopyPassword->setIcon(resources()->icon("password-copy")); + m_ui->actionEntryCopyURL->setIcon(resources()->icon("url-copy")); + m_ui->actionEntryDownloadIcon->setIcon(resources()->icon("favicon-download")); + m_ui->actionGroupSortAsc->setIcon(resources()->icon("sort-alphabetical-ascending")); + m_ui->actionGroupSortDesc->setIcon(resources()->icon("sort-alphabetical-descending")); + + m_ui->actionGroupNew->setIcon(resources()->icon("group-new")); + m_ui->actionGroupEdit->setIcon(resources()->icon("group-edit")); + m_ui->actionGroupDelete->setIcon(resources()->icon("group-delete")); + m_ui->actionGroupEmptyRecycleBin->setIcon(resources()->icon("group-empty-trash")); + m_ui->actionEntryOpenUrl->setIcon(resources()->icon("web")); + m_ui->actionGroupDownloadFavicons->setIcon(resources()->icon("favicon-download")); + + m_ui->actionSettings->setIcon(resources()->icon("configure")); + m_ui->actionPasswordGenerator->setIcon(resources()->icon("password-generator")); + + m_ui->actionAbout->setIcon(resources()->icon("help-about")); + m_ui->actionDonate->setIcon(resources()->icon("donate")); + m_ui->actionBugReport->setIcon(resources()->icon("bugreport")); + m_ui->actionGettingStarted->setIcon(resources()->icon("getting-started")); + m_ui->actionUserGuide->setIcon(resources()->icon("user-guide")); + m_ui->actionOnlineHelp->setIcon(resources()->icon("system-help")); + m_ui->actionKeyboardShortcuts->setIcon(resources()->icon("keyboard-shortcuts")); + m_ui->actionCheckForUpdates->setIcon(resources()->icon("system-software-update")); m_actionMultiplexer.connect( SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); @@ -356,10 +398,13 @@ MainWindow::MainWindow() 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(currentChanged(int)), SLOT(updateTrayIcon())); 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(tabVisibilityChanged(bool)), SLOT(updateToolbarSeparatorVisibility())); connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateToolbarSeparatorVisibility())); connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); connect(m_ui->settingsWidget, SIGNAL(settingsReset()), SLOT(applySettingsChanges())); connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); @@ -369,10 +414,12 @@ MainWindow::MainWindow() 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->actionDatabaseSaveBackup, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseBackup())); 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->actionDatabaseSecurity, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseSecurity())); + connect(m_ui->actionReports, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseReports())); + connect(m_ui->actionDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseSettings())); 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->actionImportOpVault, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importOpVaultDatabase())); @@ -392,6 +439,8 @@ MainWindow::MainWindow() 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->actionEntryMoveUp, SIGNAL(triggered()), SLOT(moveEntryUp())); + m_actionMultiplexer.connect(m_ui->actionEntryMoveDown, SIGNAL(triggered()), SLOT(moveEntryDown())); 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())); @@ -399,6 +448,10 @@ MainWindow::MainWindow() 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->actionEntryDownloadIcon, SIGNAL(triggered()), SLOT(downloadSelectedFavicons())); +#ifdef WITH_XC_SSHAGENT + m_actionMultiplexer.connect(m_ui->actionEntryAddToAgent, SIGNAL(triggered()), SLOT(addToAgent())); + m_actionMultiplexer.connect(m_ui->actionEntryRemoveFromAgent, SIGNAL(triggered()), SLOT(removeFromAgent())); +#endif m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); @@ -409,8 +462,11 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionGroupDownloadFavicons, SIGNAL(triggered()), SLOT(downloadAllFavicons())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); - connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); - connect(m_ui->passwordGeneratorWidget, SIGNAL(dialogTerminated()), SLOT(closePasswordGen())); + connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(togglePasswordGenerator(bool))); + connect(m_ui->passwordGeneratorWidget, &PasswordGeneratorWidget::closed, this, [this] { + togglePasswordGenerator(false); + }); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); @@ -439,7 +495,11 @@ MainWindow::MainWindow() connect(UpdateChecker::instance(), SIGNAL(updateCheckFinished(bool, QString, bool)), SLOT(hasUpdateAvailable(bool, QString, bool))); - QTimer::singleShot(500, this, SLOT(showUpdateCheckStartup())); + // Setup an update check every hour (checked only occur every 7 days) + connect(&m_updateCheckTimer, &QTimer::timeout, this, &MainWindow::performUpdateCheck); + m_updateCheckTimer.start(3.6e6); + // Perform the startup update check after 500 ms + QTimer::singleShot(500, this, SLOT(performUpdateCheck())); #else m_ui->actionCheckForUpdates->setVisible(false); #endif @@ -450,10 +510,8 @@ MainWindow::MainWindow() #endif // clang-format off - connect(m_ui->tabWidget, - SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), - this, - SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); + connect(m_ui->tabWidget, SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), + SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); // clang-format on connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); @@ -469,34 +527,50 @@ MainWindow::MainWindow() m_trayIconTriggerTimer.setSingleShot(true); connect(&m_trayIconTriggerTimer, SIGNAL(timeout()), SLOT(processTrayIconTrigger())); - updateTrayIcon(); - if (config()->hasAccessError()) { m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), MessageWidget::Error); } +#if defined(KEEPASSXC_BUILD_TYPE_SNAPSHOT) || defined(KEEPASSXC_BUILD_TYPE_PRE_RELEASE) + auto* hidePreRelWarn = new QAction(tr("Don't show again for this version"), m_ui->globalMessageWidget); + m_ui->globalMessageWidget->addAction(hidePreRelWarn); + auto hidePreRelWarnConn = QSharedPointer<QMetaObject::Connection>::create(); + *hidePreRelWarnConn = connect(m_ui->globalMessageWidget, &KMessageWidget::hideAnimationFinished, [=] { + m_ui->globalMessageWidget->removeAction(hidePreRelWarn); + disconnect(*hidePreRelWarnConn); + hidePreRelWarn->deleteLater(); + }); + connect(hidePreRelWarn, &QAction::triggered, [=] { + m_ui->globalMessageWidget->animatedHide(); + config()->set(Config::Messages_HidePreReleaseWarning, KEEPASSXC_VERSION); + }); +#endif #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); + if (config()->get(Config::Messages_HidePreReleaseWarning) != KEEPASSXC_VERSION) { + 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); + if (config()->get(Config::Messages_HidePreReleaseWarning) != KEEPASSXC_VERSION) { + 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, + -1); + } #elif (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) && QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) - if (!config()->get("QtErrorMessageShown", false).toBool()) { + if (!config()->get(Config::Messages_Qt55CompatibilityWarning).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); - config()->set("QtErrorMessageShown", true); + config()->set(Config::Messages_Qt55CompatibilityWarning, true); } #endif } @@ -505,6 +579,15 @@ MainWindow::~MainWindow() { } +QList<DatabaseWidget*> MainWindow::getOpenDatabases() +{ + QList<DatabaseWidget*> dbWidgets; + for (int i = 0; i < m_ui->tabWidget->count(); ++i) { + dbWidgets << m_ui->tabWidget->databaseWidgetFromIndex(i); + } + return dbWidgets; +} + void MainWindow::showErrorMessage(const QString& message) { m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); @@ -520,7 +603,7 @@ void MainWindow::updateLastDatabasesMenu() { m_ui->menuRecentDatabases->clear(); - const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList(); + const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList(); for (const QString& database : lastDatabases) { QAction* action = m_ui->menuRecentDatabases->addAction(database); action->setData(database); @@ -561,7 +644,7 @@ void MainWindow::openRecentDatabase(QAction* action) void MainWindow::clearLastDatabases() { - config()->set("LastDatabases", QVariant()); + config()->remove(Config::LastDatabases); bool inWelcomeWidget = (m_ui->stackedWidget->currentIndex() == 2); if (inWelcomeWidget) { @@ -583,12 +666,10 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) 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) { @@ -601,19 +682,25 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) switch (mode) { case DatabaseWidget::Mode::ViewMode: { - bool hasFocus = m_contextMenuFocusLock || menuBar()->hasFocus() || m_searchWidget->hasFocus() - || dbWidget->currentEntryHasFocus(); - bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && hasFocus; - bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && hasFocus; + bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1; + bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0; bool groupSelected = dbWidget->isGroupSelected(); bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren(); bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty(); bool recycleBinSelected = dbWidget->isRecycleBinSelected(); + bool sorted = dbWidget->isSorted(); + int entryIndex = dbWidget->currentEntryIndex(); + int numEntries = dbWidget->currentGroup()->entries().size(); m_ui->actionEntryNew->setEnabled(true); m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryDelete->setEnabled(entriesSelected); + m_ui->actionEntryMoveUp->setVisible(!sorted); + m_ui->actionEntryMoveDown->setVisible(!sorted); + m_ui->actionEntryMoveUp->setEnabled(singleEntrySelected && !sorted && entryIndex > 0); + m_ui->actionEntryMoveDown->setEnabled(singleEntrySelected && !sorted && entryIndex >= 0 + && entryIndex < numEntries - 1); m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); // NOTE: Copy password is enabled even if the selected entry's password is blank to prevent Ctrl+C @@ -641,14 +728,24 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupDownloadFavicons->setVisible(!recycleBinSelected); m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries && !recycleBinSelected); - m_ui->actionChangeMasterKey->setEnabled(true); - m_ui->actionChangeDatabaseSettings->setEnabled(true); + m_ui->actionDatabaseSecurity->setEnabled(true); + m_ui->actionReports->setEnabled(true); + m_ui->actionDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); m_ui->actionDatabaseSaveAs->setEnabled(true); + m_ui->actionDatabaseSaveBackup->setEnabled(true); m_ui->menuExport->setEnabled(true); m_ui->actionExportCsv->setEnabled(true); m_ui->actionExportHtml->setEnabled(true); m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); +#ifdef WITH_XC_SSHAGENT + bool singleEntryHasSshKey = + singleEntrySelected && sshAgent()->isEnabled() && dbWidget->currentEntryHasSshKey(); + m_ui->actionEntryAddToAgent->setVisible(singleEntryHasSshKey); + m_ui->actionEntryAddToAgent->setEnabled(singleEntryHasSshKey); + m_ui->actionEntryRemoveFromAgent->setVisible(singleEntryHasSshKey); + m_ui->actionEntryRemoveFromAgent->setEnabled(singleEntryHasSshKey); +#endif m_searchWidgetAction->setEnabled(true); @@ -687,10 +784,12 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) action->setEnabled(false); } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSecurity->setEnabled(false); + m_ui->actionReports->setEnabled(false); + m_ui->actionDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionDatabaseSaveBackup->setEnabled(false); m_ui->menuExport->setEnabled(false); m_ui->actionExportCsv->setEnabled(false); m_ui->actionExportHtml->setEnabled(false); @@ -714,10 +813,12 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) action->setEnabled(false); } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSecurity->setEnabled(false); + m_ui->actionReports->setEnabled(false); + m_ui->actionDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionDatabaseSaveBackup->setEnabled(false); m_ui->actionDatabaseClose->setEnabled(false); m_ui->menuExport->setEnabled(false); m_ui->actionExportCsv->setEnabled(false); @@ -738,6 +839,26 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } } +void MainWindow::updateToolbarSeparatorVisibility() +{ + if (!m_showToolbarSeparator) { + m_ui->toolbarSeparator->setVisible(false); + return; + } + + switch (m_ui->stackedWidget->currentIndex()) { + case DatabaseTabScreen: + m_ui->toolbarSeparator->setVisible(!m_ui->tabWidget->tabBar()->isVisible() + && m_ui->tabWidget->tabBar()->count() == 1); + break; + case SettingsScreen: + m_ui->toolbarSeparator->setVisible(true); + break; + default: + m_ui->toolbarSeparator->setVisible(false); + } +} + void MainWindow::updateWindowTitle() { QString customWindowTitlePart; @@ -779,10 +900,10 @@ void MainWindow::showAboutDialog() aboutDialog->open(); } -void MainWindow::showUpdateCheckStartup() +void MainWindow::performUpdateCheck() { #ifdef WITH_XC_UPDATECHECK - if (!config()->get("UpdateCheckMessageShown", false).toBool()) { + if (!config()->get(Config::UpdateCheckMessageShown).toBool()) { auto result = MessageBox::question(this, tr("Check for updates on startup?"), @@ -791,11 +912,11 @@ void MainWindow::showUpdateCheckStartup() MessageBox::Yes | MessageBox::No, MessageBox::Yes); - config()->set("GUI/CheckForUpdates", (result == MessageBox::Yes)); - config()->set("UpdateCheckMessageShown", true); + config()->set(Config::GUI_CheckForUpdates, (result == MessageBox::Yes)); + config()->set(Config::UpdateCheckMessageShown, true); } - if (config()->get("GUI/CheckForUpdates", false).toBool()) { + if (config()->get(Config::GUI_CheckForUpdates).toBool()) { updateCheck()->checkForUpdates(false); } @@ -843,12 +964,12 @@ void MainWindow::openBugReportUrl() void MainWindow::openGettingStartedGuide() { - customOpenUrl(QString("file:///%1").arg(filePath()->dataPath("docs/KeePassXC_GettingStarted.pdf"))); + customOpenUrl(QString("file:///%1").arg(resources()->dataPath("docs/KeePassXC_GettingStarted.html"))); } void MainWindow::openUserGuide() { - customOpenUrl(QString("file:///%1").arg(filePath()->dataPath("docs/KeePassXC_UserGuide.pdf"))); + customOpenUrl(QString("file:///%1").arg(resources()->dataPath("docs/KeePassXC_UserGuide.html"))); } void MainWindow::openOnlineHelp() @@ -858,7 +979,7 @@ void MainWindow::openOnlineHelp() void MainWindow::openKeyboardShortcuts() { - customOpenUrl("https://github.com/keepassxreboot/keepassxc/blob/develop/docs/KEYBINDS.md"); + customOpenUrl(QString("file:///%1").arg(resources()->dataPath("docs/KeePassXC_KeyboardShortcuts.html"))); } void MainWindow::switchToDatabases() @@ -880,24 +1001,18 @@ void MainWindow::switchToSettings(bool enabled) } } -void MainWindow::switchToPasswordGen(bool enabled) +void MainWindow::togglePasswordGenerator(bool enabled) { if (enabled) { m_ui->passwordGeneratorWidget->loadSettings(); m_ui->passwordGeneratorWidget->regeneratePassword(); m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); - m_ui->passwordGeneratorWidget->setStandaloneMode(true); } else { m_ui->passwordGeneratorWidget->saveSettings(); switchToDatabases(); } } -void MainWindow::closePasswordGen() -{ - switchToPasswordGen(false); -} - void MainWindow::switchToNewDatabase() { m_ui->tabWidget->newDatabase(); @@ -940,28 +1055,38 @@ void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget) updateTrayIcon(); } -void MainWindow::selectNextDatabaseTab() +/** + * Select a database tab by its index. Stays bounded to first/last tab + * on overflow unless wrap is true. + * + * @param tabIndex 0-based tab index selector + * @param wrap if true wrap around to first/last tab + */ +void MainWindow::selectDatabaseTab(int tabIndex, bool wrap) { if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->currentIndex() + 1; - if (index >= m_ui->tabWidget->count()) { - m_ui->tabWidget->setCurrentIndex(0); + if (wrap) { + if (tabIndex < 0) { + tabIndex = m_ui->tabWidget->count() - 1; + } else if (tabIndex >= m_ui->tabWidget->count()) { + tabIndex = 0; + } } else { - m_ui->tabWidget->setCurrentIndex(index); + tabIndex = qBound(0, tabIndex, m_ui->tabWidget->count() - 1); } + + m_ui->tabWidget->setCurrentIndex(tabIndex); } } +void MainWindow::selectNextDatabaseTab() +{ + selectDatabaseTab(m_ui->tabWidget->currentIndex() + 1, true); +} + 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); - } - } + selectDatabaseTab(m_ui->tabWidget->currentIndex() - 1, true); } void MainWindow::databaseTabChanged(int tabIndex) @@ -1000,7 +1125,8 @@ void MainWindow::closeEvent(QCloseEvent* event) // Ignore event and hide to tray if this is not an actual close // request by the system's session manager. - if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled && !isHidden() && !qApp->isSavingSession()) { + if (config()->get(Config::GUI_MinimizeOnClose).toBool() && !m_appExitCalled && !isHidden() + && !qApp->isSavingSession()) { event->ignore(); hideWindow(); return; @@ -1010,11 +1136,12 @@ void MainWindow::closeEvent(QCloseEvent* event) if (m_appExiting) { saveWindowInformation(); event->accept(); - QApplication::quit(); + m_restartRequested ? kpxcApp->restart() : QApplication::quit(); return; } m_appExitCalled = false; + m_restartRequested = false; event->ignore(); } @@ -1022,12 +1149,12 @@ 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(Config::GUI_MinimizeToTray).toBool()) { event->ignore(); - QTimer::singleShot(0, this, SLOT(hide())); + hide(); } - if (config()->get("security/lockdatabaseminimize").toBool()) { + if (config()->get(Config::Security_LockDatabaseMinimize).toBool()) { m_ui->tabWidget->lockDatabases(); } } else { @@ -1035,22 +1162,52 @@ void MainWindow::changeEvent(QEvent* event) } } +bool MainWindow::focusNextPrevChild(bool next) +{ + // Only navigate around the main window if the database widget is showing the entry view + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget && dbWidget->isVisible() && dbWidget->isEntryViewActive()) { + // Search Widget <-> Tab Widget <-> DbWidget + if (next) { + if (m_searchWidget->hasFocus()) { + m_ui->tabWidget->setFocus(Qt::TabFocusReason); + } else if (m_ui->tabWidget->hasFocus()) { + dbWidget->setFocus(Qt::TabFocusReason); + } else { + m_searchWidget->setFocus(Qt::TabFocusReason); + } + } else { + if (m_searchWidget->hasFocus()) { + dbWidget->setFocus(Qt::BacktabFocusReason); + } else if (m_ui->tabWidget->hasFocus()) { + m_searchWidget->setFocus(Qt::BacktabFocusReason); + } else { + m_ui->tabWidget->setFocus(Qt::BacktabFocusReason); + } + } + return true; + } + + // Defer to Qt to make a decision, this maintains normal behavior + return QMainWindow::focusNextPrevChild(next); +} + void MainWindow::saveWindowInformation() { if (isVisible()) { - config()->set("GUI/MainWindowGeometry", saveGeometry()); - config()->set("GUI/MainWindowState", saveState()); + config()->set(Config::GUI_MainWindowGeometry, saveGeometry()); + config()->set(Config::GUI_MainWindowState, saveState()); } } bool MainWindow::saveLastDatabases() { - if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) { + if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) { auto currentDbWidget = m_ui->tabWidget->currentDatabaseWidget(); if (currentDbWidget) { - config()->set("LastActiveDatabase", currentDbWidget->database()->filePath()); + config()->set(Config::LastActiveDatabase, currentDbWidget->database()->filePath()); } else { - config()->set("LastActiveDatabase", {}); + config()->remove(Config::LastActiveDatabase); } QStringList openDatabases; @@ -1059,10 +1216,10 @@ bool MainWindow::saveLastDatabases() openDatabases.append(dbWidget->database()->filePath()); } - config()->set("LastOpenedDatabases", openDatabases); + config()->set(Config::LastOpenedDatabases, openDatabases); } else { - config()->set("LastActiveDatabase", {}); - config()->set("LastOpenedDatabases", {}); + config()->remove(Config::LastActiveDatabase); + config()->remove(Config::LastOpenedDatabases); } return m_ui->tabWidget->closeAllDatabaseTabs(); @@ -1073,11 +1230,11 @@ void MainWindow::updateTrayIcon() if (isTrayIconEnabled()) { if (!m_trayIcon) { m_trayIcon = new QSystemTrayIcon(this); - QMenu* menu = new QMenu(this); + auto* menu = new QMenu(this); - QAction* actionToggle = new QAction(tr("Toggle window"), menu); + auto* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); - actionToggle->setIcon(filePath()->icon("apps", "keepassxc")); + actionToggle->setIcon(resources()->icon("keepassxc-monochrome-dark")); menu->addAction(m_ui->actionLockDatabases); @@ -1097,19 +1254,21 @@ void MainWindow::updateTrayIcon() m_trayIcon->setContextMenu(menu); - m_trayIcon->setIcon(filePath()->trayIcon()); + m_trayIcon->setIcon(resources()->trayIcon()); m_trayIcon->show(); } - if (m_ui->tabWidget->hasLockableDatabases()) { - m_trayIcon->setIcon(filePath()->trayIconUnlocked()); + + if (m_ui->tabWidget->count() == 0) { + m_trayIcon->setIcon(resources()->trayIcon()); + } else if (m_ui->tabWidget->hasLockableDatabases()) { + m_trayIcon->setIcon(resources()->trayIconUnlocked()); } else { - m_trayIcon->setIcon(filePath()->trayIconLocked()); + m_trayIcon->setIcon(resources()->trayIconLocked()); } } else { if (m_trayIcon) { m_trayIcon->hide(); delete m_trayIcon; - m_trayIcon = nullptr; } } } @@ -1124,12 +1283,18 @@ void MainWindow::releaseContextFocusLock() m_contextMenuFocusLock = false; } +void MainWindow::agentEnabled(bool enabled) +{ + m_ui->actionEntryAddToAgent->setVisible(enabled); + m_ui->actionEntryRemoveFromAgent->setVisible(enabled); +} + void MainWindow::showEntryContextMenu(const QPoint& globalPos) { bool entrySelected = false; auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); if (dbWidget) { - entrySelected = dbWidget->currentEntryHasFocus(); + entrySelected = dbWidget->numberOfSelectedEntries() > 0; } if (entrySelected) { @@ -1155,38 +1320,39 @@ void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard void MainWindow::applySettingsChanges() { - int timeout = config()->get("security/lockdatabaseidlesec").toInt() * 1000; + int timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000; if (timeout <= 0) { timeout = 60; } m_inactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/lockdatabaseidle").toBool()) { + if (config()->get(Config::Security_LockDatabaseIdle).toBool()) { m_inactivityTimer->activate(); } 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; - } + if (config()->get(Config::Security_ResetTouchId).toBool()) { + // Calculate TouchID timeout in milliseconds + timeout = config()->get(Config::Security_ResetTouchIdTimeout).toInt() * 60 * 1000; + if (timeout <= 0) { + timeout = 30 * 60 * 1000; + } - m_touchIDinactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/resettouchid").toBool()) { + m_touchIDinactivityTimer->setInactivityTimeout(timeout); 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()); + m_ui->toolBar->setHidden(config()->get(Config::GUI_HideToolbar).toBool()); + m_ui->toolBar->setMovable(config()->get(Config::GUI_MovableToolbar).toBool()); bool isOk = false; - const auto toolButtonStyle = static_cast<Qt::ToolButtonStyle>(config()->get("GUI/ToolButtonStyle").toInt(&isOk)); + const auto toolButtonStyle = + static_cast<Qt::ToolButtonStyle>(config()->get(Config::GUI_ToolButtonStyle).toInt(&isOk)); if (isOk) { m_ui->toolBar->setToolButtonStyle(toolButtonStyle); } @@ -1197,7 +1363,7 @@ void MainWindow::applySettingsChanges() void MainWindow::focusWindowChanged(QWindow* focusWindow) { if (focusWindow != windowHandle()) { - m_lastFocusOutTime = Clock::currentSecondsSinceEpoch(); + m_lastFocusOutTime = Clock::currentMilliSecondsSinceEpoch(); } } @@ -1214,6 +1380,16 @@ void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) void MainWindow::processTrayIconTrigger() { +#ifdef Q_OS_MACOS + // Do not toggle the window on macOS and just show the context menu instead. + // Right click detection doesn't seem to be working anyway + // and anything else will only trigger the context menu AND + // toggle the window at the same time, which is confusing at best. + // Showing only a context menu for tray icons seems to be best + // practice on macOS anyway, so this is probably fine. + return; +#endif + if (m_trayIconTriggerReason == QSystemTrayIcon::DoubleClick) { // Always toggle window on double click toggleWindow(); @@ -1221,11 +1397,11 @@ void MainWindow::processTrayIconTrigger() || m_trayIconTriggerReason == QSystemTrayIcon::MiddleClick) { // Toggle window if is not in front. #ifdef Q_OS_WIN - // If on Windows, check if focus switched within the last second because + // If on Windows, check if focus switched within the 500 milliseconds since // clicking the tray icon removes focus from main window. - if (isHidden() || (Clock::currentSecondsSinceEpoch() - m_lastFocusOutTime) <= 1) { + if (isHidden() || (Clock::currentMilliSecondsSinceEpoch() - m_lastFocusOutTime) <= 500) { #else - // If on Linux or macOS, check if the window has focus. + // If on Linux, check if the window has focus. if (hasFocus() || isHidden() || windowHandle()->isActive()) { #endif toggleWindow(); @@ -1235,28 +1411,63 @@ void MainWindow::processTrayIconTrigger() } } +void MainWindow::show() +{ +#ifndef Q_OS_WIN + m_lastShowTime = Clock::currentMilliSecondsSinceEpoch(); +#endif +#ifdef Q_OS_MACOS + // Unset minimize state to avoid weird fly-in effects + setWindowState(windowState() & ~Qt::WindowMinimized); + macUtils()->toggleForegroundApp(true); +#endif + QMainWindow::show(); +} + +void MainWindow::hide() +{ +#ifndef Q_OS_WIN + qint64 current_time = Clock::currentMilliSecondsSinceEpoch(); + if (current_time - m_lastShowTime < 50) { + return; + } +#endif + QMainWindow::hide(); +#ifdef Q_OS_MACOS + macUtils()->toggleForegroundApp(false); +#endif +} + void MainWindow::hideWindow() { saveWindowInformation(); -#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 + // Only hide if tray icon is active, otherwise window will be gone forever if (isTrayIconEnabled()) { + // On X11, the window should NOT be minimized and hidden at the same time. See issue #1595. + // On macOS, we are skipping minimization as well to avoid playing the magic lamp animation. + if (QGuiApplication::platformName() != "xcb" && QGuiApplication::platformName() != "cocoa") { + setWindowState(windowState() | Qt::WindowMinimized); + } hide(); } else { showMinimized(); } - if (config()->get("security/lockdatabaseminimize").toBool()) { + if (config()->get(Config::Security_LockDatabaseMinimize).toBool()) { m_ui->tabWidget->lockDatabases(); } } +void MainWindow::minimizeOrHide() +{ + if (config()->get(Config::GUI_MinimizeToTray).toBool()) { + hideWindow(); + } else { + showMinimized(); + } +} + void MainWindow::toggleWindow() { if (isVisible() && !isMinimized()) { @@ -1269,7 +1480,7 @@ void MainWindow::toggleWindow() // 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 - const static auto isDesktopSessionUnity = qgetenv("XDG_CURRENT_DESKTOP") == "Unity"; + static const auto isDesktopSessionUnity = qgetenv("XDG_CURRENT_DESKTOP") == "Unity"; if (isDesktopSessionUnity && Tools::qtRuntimeVersion() < QT_VERSION_CHECK(5, 9, 0) && !m_ui->menubar->isVisible()) { @@ -1306,7 +1517,7 @@ void MainWindow::forgetTouchIDAfterInactivity() bool MainWindow::isTrayIconEnabled() const { - return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); + return config()->get(Config::GUI_ShowTrayIcon).toBool() && QSystemTrayIcon::isSystemTrayAvailable(); } void MainWindow::displayGlobalMessage(const QString& text, @@ -1357,12 +1568,12 @@ void MainWindow::bringToFront() void MainWindow::handleScreenLock() { - if (config()->get("security/lockdatabasescreenlock").toBool()) { + if (config()->get(Config::Security_LockDatabaseScreenLock).toBool()) { lockDatabasesAfterInactivity(); } #ifdef WITH_XC_TOUCHID - if (config()->get("security/resettouchidscreenlock").toBool()) { + if (config()->get(Config::Security_ResetTouchIdScreenlock).toBool()) { forgetTouchIDAfterInactivity(); } #endif @@ -1428,8 +1639,67 @@ void MainWindow::displayDesktopNotification(const QString& msg, QString title, i } #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) - m_trayIcon->showMessage(title, msg, filePath()->applicationIcon(), msTimeoutHint); + m_trayIcon->showMessage(title, msg, resources()->applicationIcon(), msTimeoutHint); #else m_trayIcon->showMessage(title, msg, QSystemTrayIcon::Information, msTimeoutHint); #endif } + +void MainWindow::restartApp(const QString& message) +{ + auto ans = MessageBox::question( + this, tr("Restart Application?"), message, MessageBox::Yes | MessageBox::No, MessageBox::Yes); + if (ans == MessageBox::Yes) { + m_appExitCalled = true; + m_restartRequested = true; + close(); + } else { + m_restartRequested = false; + } +} + +void MainWindow::initViewMenu() +{ + m_ui->actionThemeAuto->setData("auto"); + m_ui->actionThemeLight->setData("light"); + m_ui->actionThemeDark->setData("dark"); + m_ui->actionThemeClassic->setData("classic"); + + auto themeActions = new QActionGroup(this); + themeActions->addAction(m_ui->actionThemeAuto); + themeActions->addAction(m_ui->actionThemeLight); + themeActions->addAction(m_ui->actionThemeDark); + themeActions->addAction(m_ui->actionThemeClassic); + + auto theme = config()->get(Config::GUI_ApplicationTheme).toString(); + for (auto action : themeActions->actions()) { + if (action->data() == theme) { + action->setChecked(true); + break; + } + } + + connect(themeActions, &QActionGroup::triggered, this, [this](QAction* action) { + if (action->data() != config()->get(Config::GUI_ApplicationTheme)) { + config()->set(Config::GUI_ApplicationTheme, action->data()); + restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?")); + } + }); + + m_ui->actionCompactMode->setChecked(config()->get(Config::GUI_CompactMode).toBool()); + connect(m_ui->actionCompactMode, &QAction::toggled, this, [this](bool checked) { + config()->set(Config::GUI_CompactMode, checked); + restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?")); + }); + + m_ui->actionShowToolbar->setChecked(!config()->get(Config::GUI_HideToolbar).toBool()); + connect(m_ui->actionShowToolbar, &QAction::toggled, this, [this](bool checked) { + config()->set(Config::GUI_HideToolbar, !checked); + applySettingsChanges(); + }); + + m_ui->actionShowPreviewPanel->setChecked(!config()->get(Config::GUI_HidePreviewPanel).toBool()); + connect(m_ui->actionShowPreviewPanel, &QAction::toggled, this, [](bool checked) { + config()->set(Config::GUI_HidePreviewPanel, !checked); + }); +} |