Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorJonathan White <support@dmapps.us>2020-07-07 04:13:28 +0300
committerJonathan White <support@dmapps.us>2020-07-07 04:13:28 +0300
commit07659547ce6967e4b26e5ec981dcd1324ac8ff4c (patch)
treea3dd9f3bc9dd9a931ff2c10c85a4a482af3f7c90 /src/gui
parentdcca5aa0f0ee2f2b8685787c01f934cb2b506ef7 (diff)
parentcf95f5e72eafe6f4d9bb8136158b8e7285326395 (diff)
Release 2.6.02.6.0
Added - Custom Light and Dark themes [#4110, #4769, #4791, #4796, #4892, #4915] - Compact mode to use classic Group and Entry line height [#4910] - View menu to quickly switch themes, compact mode, and toggle UI elements [#4910] - Search for groups and scope search to matched groups [#4705] - Save Database Backup feature [#4550] - Sort entries by "natural order" and move lines up/down [#4357] - Option to launch KeePassXC on system startup/login [#4675] - Caps Lock warning on password input fields [#3646] - Add "Size" column to entry view [#4588] - Browser-like tab experience using Ctrl+[Num] (Alt+[Num] on Linux) [#4063, #4305] - Password Generator: Define additional characters to choose from [#3876] - Reports: Database password health check (offline) [#3993] - Reports: HIBP online service to check for breached passwords [#4438] - Auto-Type: DateTime placeholders [#4409] - Browser: Show group name in results sent to browser extension [#4111] - Browser: Ability to define a custom browser location (macOS and Linux only) [#4148] - Browser: Ability to change root group UUID and inline edit connection ID [#4315, #4591] - CLI: `db-info` command [#4231] - CLI: Use wl-clipboard if xclip is not available (Linux) [#4323] - CLI: Incorporate xclip into snap builds [#4697] - SSH Agent: Key file path env substitution, SSH_AUTH_SOCK override, and connection test [#3769, #3801, #4545] - SSH Agent: Context menu actions to add/remove keys [#4290] Changed - Complete replacement of default database icons [#4699] - Complete replacement of application icons [#4066, #4161, #4203, #4411] - Complete rewrite of documentation and manpages using Asciidoctor [#4937] - Complete refactor of config files; separate between local and roaming [#4665] - Complete refactor of browser integration and proxy code [#4680] - Complete refactor of hardware key integration (YubiKey and OnlyKey) [#4584, #4843] - Significantly improve performance when saving and opening databases [#4309, #4833] - Remove read-only detection for database files [#4508] - Overhaul of password fields and password generator [#4367] - Replace instances of "Master Key" with "Database Credentials" [#4929] - Change settings checkboxes to positive phrasing for consistency [#4715] - Improve UX of using entry actions (focus fix) [#3893] - Set expiration time to Now when enabling entry expiration [#4406] - Always show "New Entry" in context menu [#4617] - Issue warning before adding large attachments [#4651] - Improve importing OPVault [#4630] - Improve AutoOpen capability [#3901, #4752] - Check for updates every 7 days even while still running [#4752] - Improve Windows installer UI/UX [#4675] - Improve config file handling of portable distribution [#4131, #4752] - macOS: Hide dock icon when application is hidden to tray [#4782] - Browser: Use unlock dialog to improve UX of opening a locked database [#3698] - Browser: Improve database and entry settings experience [#4392, #4591] - Browser: Improve confirm access dialog [#2143, #4660] - KeeShare: Improve monitoring file changes of shares [#4720] - CLI: Rename `create` command to `db-create` [#4231] - CLI: Cleanup `db-create` options (`--set-key-file` and `--set-password`) [#4313] - CLI: Use stderr for help text and password prompts [#4086, #4623] - FdoSecrets: Display existing secret service process [#4128] Fixed - Fix changing focus around the main window using tab key [#4641] - Fix search field clearing while still using the application [#4368] - Improve search help widget displaying on macOS and Linux [#4236] - Return keyboard focus after editing an entry [#4287] - Reset database path after failed "Save As" [#4526] - Use SHA256 Digest for Windows code signing [#4129] - Improve handling of ccache when building [#4104, #4335] - macOS: Properly re-hide application window after browser integration and Auto-Type usage [#4909] - Auto-Type: Fix crash when performing on new entry [#4132] - Browser: Send legacy HTTP settings to recycle bin [#4589] - Browser: Fix merging browser keys [#4685] - CLI: Fix encoding when exporting database [#3921] - SSH Agent: Improve reliability and underlying code [#3833, #4256, #4549, #4595] - FdoSecrets: Fix crash when editing settings before service is enabled [#4332]
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/AboutDialog.cpp63
-rw-r--r--src/gui/AboutDialog.ui8
-rw-r--r--src/gui/Application.cpp59
-rw-r--r--src/gui/Application.h10
-rw-r--r--src/gui/ApplicationSettingsWidget.cpp312
-rw-r--r--src/gui/ApplicationSettingsWidget.h1
-rw-r--r--src/gui/ApplicationSettingsWidgetGeneral.ui1630
-rw-r--r--src/gui/ApplicationSettingsWidgetSecurity.ui21
-rw-r--r--src/gui/CategoryListWidget.cpp72
-rw-r--r--src/gui/CategoryListWidget.ui4
-rw-r--r--src/gui/Clipboard.cpp4
-rw-r--r--src/gui/CloneDialog.cpp2
-rw-r--r--src/gui/DatabaseOpenDialog.cpp5
-rw-r--r--src/gui/DatabaseOpenWidget.cpp334
-rw-r--r--src/gui/DatabaseOpenWidget.h33
-rw-r--r--src/gui/DatabaseOpenWidget.ui331
-rw-r--r--src/gui/DatabaseTabWidget.cpp95
-rw-r--r--src/gui/DatabaseTabWidget.h12
-rw-r--r--src/gui/DatabaseWidget.cpp423
-rw-r--r--src/gui/DatabaseWidget.h29
-rw-r--r--src/gui/DatabaseWidgetStateSync.cpp29
-rw-r--r--src/gui/DialogyWidget.cpp1
-rw-r--r--src/gui/EditWidget.cpp13
-rw-r--r--src/gui/EditWidget.ui31
-rw-r--r--src/gui/EditWidgetIcons.cpp11
-rw-r--r--src/gui/EditWidgetIcons.ui33
-rw-r--r--src/gui/EditWidgetProperties.ui56
-rw-r--r--src/gui/EntryPreviewWidget.cpp57
-rw-r--r--src/gui/EntryPreviewWidget.h1
-rw-r--r--src/gui/EntryPreviewWidget.ui176
-rw-r--r--src/gui/FileDialog.cpp61
-rw-r--r--src/gui/FileDialog.h7
-rw-r--r--src/gui/IconDownloaderDialog.cpp4
-rw-r--r--src/gui/IconDownloaderDialog.ui5
-rw-r--r--src/gui/IconModels.cpp9
-rw-r--r--src/gui/KMessageWidget.cpp24
-rw-r--r--src/gui/KeePass1OpenWidget.cpp6
-rw-r--r--src/gui/LineEdit.cpp12
-rw-r--r--src/gui/MainWindow.cpp722
-rw-r--r--src/gui/MainWindow.h27
-rw-r--r--src/gui/MainWindow.ui351
-rw-r--r--src/gui/MessageBox.cpp4
-rw-r--r--src/gui/MessageWidget.cpp23
-rw-r--r--src/gui/MessageWidget.h4
-rw-r--r--src/gui/PasswordEdit.cpp162
-rw-r--r--src/gui/PasswordEdit.h27
-rw-r--r--src/gui/PasswordGeneratorWidget.cpp438
-rw-r--r--src/gui/PasswordGeneratorWidget.h23
-rw-r--r--src/gui/PasswordGeneratorWidget.ui2067
-rw-r--r--src/gui/SearchHelpWidget.ui86
-rw-r--r--src/gui/SearchWidget.cpp35
-rw-r--r--src/gui/SearchWidget.h1
-rw-r--r--src/gui/SearchWidget.ui33
-rw-r--r--src/gui/TotpDialog.cpp8
-rw-r--r--src/gui/TotpExportSettingsDialog.cpp8
-rw-r--r--src/gui/TotpSetupDialog.ui2
-rw-r--r--src/gui/URLEdit.cpp4
-rw-r--r--src/gui/UpdateCheckDialog.cpp4
-rw-r--r--src/gui/WelcomeWidget.cpp46
-rw-r--r--src/gui/WelcomeWidget.h2
-rw-r--r--src/gui/csvImport/CsvImportWidget.cpp105
-rw-r--r--src/gui/csvImport/CsvImportWidget.h2
-rw-r--r--src/gui/csvImport/CsvImportWidget.ui1122
-rw-r--r--src/gui/csvImport/CsvParserModel.cpp26
-rw-r--r--src/gui/databasekey/KeyComponentWidget.cpp (renamed from src/gui/masterkey/KeyComponentWidget.cpp)0
-rw-r--r--src/gui/databasekey/KeyComponentWidget.h (renamed from src/gui/masterkey/KeyComponentWidget.h)0
-rw-r--r--src/gui/databasekey/KeyComponentWidget.ui (renamed from src/gui/masterkey/KeyComponentWidget.ui)0
-rw-r--r--src/gui/databasekey/KeyFileEditWidget.cpp (renamed from src/gui/masterkey/KeyFileEditWidget.cpp)2
-rw-r--r--src/gui/databasekey/KeyFileEditWidget.h (renamed from src/gui/masterkey/KeyFileEditWidget.h)0
-rw-r--r--src/gui/databasekey/KeyFileEditWidget.ui (renamed from src/gui/masterkey/KeyFileEditWidget.ui)0
-rw-r--r--src/gui/databasekey/PasswordEditWidget.cpp (renamed from src/gui/masterkey/PasswordEditWidget.cpp)38
-rw-r--r--src/gui/databasekey/PasswordEditWidget.h (renamed from src/gui/masterkey/PasswordEditWidget.h)1
-rw-r--r--src/gui/databasekey/PasswordEditWidget.ui (renamed from src/gui/masterkey/PasswordEditWidget.ui)87
-rw-r--r--src/gui/databasekey/YubiKeyEditWidget.cpp (renamed from src/gui/masterkey/YubiKeyEditWidget.cpp)93
-rw-r--r--src/gui/databasekey/YubiKeyEditWidget.h (renamed from src/gui/masterkey/YubiKeyEditWidget.h)6
-rw-r--r--src/gui/databasekey/YubiKeyEditWidget.ui (renamed from src/gui/masterkey/YubiKeyEditWidget.ui)0
-rw-r--r--src/gui/dbsettings/DatabaseSettingsDialog.cpp36
-rw-r--r--src/gui/dbsettings/DatabaseSettingsDialog.h6
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp86
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h5
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui113
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp (renamed from src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp)44
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.h (renamed from src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.h)14
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp25
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h6
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui28
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui2
-rw-r--r--src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui79
-rw-r--r--src/gui/entry/AutoTypeMatchModel.cpp8
-rw-r--r--src/gui/entry/EditEntryWidget.cpp318
-rw-r--r--src/gui/entry/EditEntryWidget.h8
-rw-r--r--src/gui/entry/EditEntryWidgetAdvanced.ui37
-rw-r--r--src/gui/entry/EditEntryWidgetAutoType.ui24
-rw-r--r--src/gui/entry/EditEntryWidgetMain.ui275
-rw-r--r--src/gui/entry/EditEntryWidgetSSHAgent.ui244
-rw-r--r--src/gui/entry/EntryAttachmentsWidget.cpp45
-rw-r--r--src/gui/entry/EntryAttachmentsWidget.h2
-rw-r--r--src/gui/entry/EntryModel.cpp169
-rw-r--r--src/gui/entry/EntryModel.h11
-rw-r--r--src/gui/entry/EntryURLModel.cpp4
-rw-r--r--src/gui/entry/EntryView.cpp194
-rw-r--r--src/gui/entry/EntryView.h11
-rw-r--r--src/gui/group/EditGroupWidget.cpp12
-rw-r--r--src/gui/group/EditGroupWidgetMain.ui186
-rw-r--r--src/gui/group/GroupModel.cpp16
-rw-r--r--src/gui/group/GroupView.cpp15
-rw-r--r--src/gui/group/GroupView.h4
-rw-r--r--src/gui/osutils/OSUtils.h41
-rw-r--r--src/gui/osutils/OSUtilsBase.cpp27
-rw-r--r--src/gui/osutils/OSUtilsBase.h58
-rw-r--r--src/gui/osutils/macutils/AppKit.h (renamed from src/gui/macutils/AppKit.h)1
-rw-r--r--src/gui/osutils/macutils/AppKitImpl.h (renamed from src/gui/macutils/AppKitImpl.h)1
-rw-r--r--src/gui/osutils/macutils/AppKitImpl.mm (renamed from src/gui/macutils/AppKitImpl.mm)15
-rw-r--r--src/gui/osutils/macutils/MacUtils.cpp (renamed from src/gui/macutils/MacUtils.cpp)63
-rw-r--r--src/gui/osutils/macutils/MacUtils.h (renamed from src/gui/macutils/MacUtils.h)25
-rw-r--r--src/gui/osutils/nixutils/NixUtils.cpp136
-rw-r--r--src/gui/osutils/nixutils/NixUtils.h53
-rw-r--r--src/gui/osutils/winutils/WinUtils.cpp107
-rw-r--r--src/gui/osutils/winutils/WinUtils.h62
-rw-r--r--src/gui/reports/ReportsDialog.cpp148
-rw-r--r--src/gui/reports/ReportsDialog.h88
-rw-r--r--src/gui/reports/ReportsDialog.ui43
-rw-r--r--src/gui/reports/ReportsPageHealthcheck.cpp55
-rw-r--r--src/gui/reports/ReportsPageHealthcheck.h41
-rw-r--r--src/gui/reports/ReportsPageHibp.cpp55
-rw-r--r--src/gui/reports/ReportsPageHibp.h41
-rw-r--r--src/gui/reports/ReportsPageStatistics.cpp (renamed from src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp)26
-rw-r--r--src/gui/reports/ReportsPageStatistics.h (renamed from src/gui/dbsettings/DatabaseSettingsPageStatistics.h)10
-rw-r--r--src/gui/reports/ReportsWidget.cpp44
-rw-r--r--src/gui/reports/ReportsWidget.h53
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.cpp342
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.h76
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.ui81
-rw-r--r--src/gui/reports/ReportsWidgetHibp.cpp404
-rw-r--r--src/gui/reports/ReportsWidgetHibp.h89
-rw-r--r--src/gui/reports/ReportsWidgetHibp.ui234
-rw-r--r--src/gui/reports/ReportsWidgetStatistics.cpp (renamed from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp)54
-rw-r--r--src/gui/reports/ReportsWidgetStatistics.h (renamed from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h)16
-rw-r--r--src/gui/reports/ReportsWidgetStatistics.ui73
-rw-r--r--src/gui/styles/StateColorPalette.cpp57
-rw-r--r--src/gui/styles/StateColorPalette.h66
-rw-r--r--src/gui/styles/base/BaseStyle.cpp4808
-rw-r--r--src/gui/styles/base/BaseStyle.h101
-rw-r--r--src/gui/styles/base/basestyle.qss48
-rw-r--r--src/gui/styles/base/phantomcolor.cpp423
-rw-r--r--src/gui/styles/base/phantomcolor.h165
-rw-r--r--src/gui/styles/dark/DarkStyle.cpp128
-rw-r--r--src/gui/styles/dark/DarkStyle.h38
-rw-r--r--src/gui/styles/dark/darkstyle.qss18
-rw-r--r--src/gui/styles/light/LightStyle.cpp127
-rw-r--r--src/gui/styles/light/LightStyle.h38
-rw-r--r--src/gui/styles/light/lightstyle.qss18
-rw-r--r--src/gui/styles/styles.qrc8
-rw-r--r--src/gui/widgets/ElidedLabel.cpp9
-rw-r--r--src/gui/widgets/PopupHelpWidget.cpp32
-rw-r--r--src/gui/widgets/PopupHelpWidget.h1
-rw-r--r--src/gui/wizard/NewDatabaseWizard.cpp30
-rw-r--r--src/gui/wizard/NewDatabaseWizardPage.ui2
-rw-r--r--src/gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp (renamed from src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp)18
-rw-r--r--src/gui/wizard/NewDatabaseWizardPageDatabaseKey.h (renamed from src/gui/wizard/NewDatabaseWizardPageMasterKey.h)14
160 files changed, 15139 insertions, 5142 deletions
diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp
index 4b9fe5f85..b97d62590 100644
--- a/src/gui/AboutDialog.cpp
+++ b/src/gui/AboutDialog.cpp
@@ -20,7 +20,7 @@
#include "ui_AboutDialog.h"
#include "config-keepassx.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "core/Tools.h"
#include "crypto/Crypto.h"
@@ -30,11 +30,10 @@ static const QString aboutMaintainers = R"(
<p><ul>
<li>Jonathan White (<a href="https://github.com/droidmonkey">droidmonkey</a>)</li>
<li>Janek Bevendorff (<a href="https://github.com/phoerious">phoerious</a>)</li>
- <li><a href="https://github.com/TheZ3ro">TheZ3ro</a></li>
- <li>Louis-Bertrand (<a href="https://github.com/louib">louib</a>)</li>
- <li>Weslly Honorato (<a href="https://github.com/weslly">weslly</a>)</li>
- <li>Toni Spets (<a href="https://github.com/hifi">hifi</a>)</li>
<li>Sami V&auml;nttinen (<a href="https://github.com/varjolintu">varjolintu</a>)</li>
+ <li>Toni Spets (<a href="https://github.com/hifi">hifi</a>)</li>
+ <li>Louis-Bertrand (<a href="https://github.com/louib">louib</a>)</li>
+ <li><a href="https://github.com/TheZ3ro">TheZ3ro</a> (retired)</li>
</ul></p>
)";
@@ -57,64 +56,68 @@ static const QString aboutContributors = R"(
<li>Riley Moses</li>
<li>Korbinian Schildmann</li>
<li>Andreas (nitrohorse)</li>
+ <li>Kernellinux</li>
+ <li>Micha Ober</li>
+ <li>PublicByte</li>
</ul>
<h3>Notable Code Contributions:</h3>
<ul>
<li>droidmonkey</li>
<li>phoerious</li>
- <li>TheZ3ro</li>
- <li>louib</li>
- <li>weslly</li>
- <li>varjolintu (KeePassXC-Browser)</li>
+ <li>louib (CLI)</li>
+ <li>varjolintu (Browser Integration)</li>
<li>hifi (SSH Agent)</li>
<li>ckieschnick (KeeShare)</li>
<li>seatedscribe (CSV Import)</li>
- <li>Aetf (Secret Storage Server)</li>
+ <li>Aetf (FdoSecrets Storage Server)</li>
+ <li>weslly (macOS improvements)</li>
<li>brainplot (many improvements)</li>
<li>kneitinger (many improvements)</li>
<li>frostasm (many improvements)</li>
<li>fonic (Entry Table View)</li>
<li>kylemanna (YubiKey)</li>
<li>c4rlo (Offline HIBP Checker)</li>
- <li>wolframroesler (HTML Exporter)</li>
+ <li>wolframroesler (HTML Export, Statistics, Password Health, HIBP integration)</li>
<li>mdaniel (OpVault Importer)</li>
- <li>keithbennett (KeePassHTTP)</li>
- <li>Typz (KeePassHTTP)</li>
- <li>denk-mal (KeePassHTTP)</li>
<li>angelsl (KDBX 4)</li>
+ <li>TheZ3ro (retired lead)</li>
<li>debfx (KeePassX)</li>
<li>BlueIce (KeePassX)</li>
</ul>
<h3>Patreon Supporters:</h3>
<ul>
+ <li>Igor Zinovik</li>
<li>Alexanderjb</li>
- <li>Andreas Kollmann</li>
<li>Richard Ames</li>
- <li>Christian Rasmussen</li>
- <li>Gregory Werbin</li>
- <li>Nuutti Toivola</li>
<li>SLmanDR</li>
- <li>Ashura</li>
+ <li>Christian Rasmussen</li>
<li>Tyler Gass</li>
- <li>Lionel Laské</li>
- <li>Dmitrii Galinskii</li>
- <li>Sergei Maximov</li>
- <li>John-Ivar</li>
- <li>Clayton Casciato</li>
- <li>John</li>
+ <li>Nuutti Toivola</li>
+ <li>Gregory Werbin</li>
+ <li>Lionel Laské</li>
+ <li>Ivar</li>
<li>Darren</li>
<li>Brad</li>
<li>Mathieu Peltier</li>
+ <li>gonczor</li>
<li>Oleksii Aleksieiev</li>
- <li>Daniel Epp</li>
<li>Gernot Premper</li>
<li>Julian Stier</li>
- <li>gonczor</li>
+ <li>Daniel Epp</li>
<li>Ruben Schade</li>
<li>Esteban Martinez</li>
+ <li>Niels Ganser</li>
<li>turin231</li>
<li>judd</li>
- <li>Niels Ganser</li>
+ <li>Tarek Sherif</li>
+ <li>Bernhard</li>
+ <li>William Komanetsky</li>
+ <li>Clark Henry</li>
+ <li>Justin Carroll</li>
+ <li>Shintaro Matsushima</li>
+ <li>Larry Siden</li>
+ <li>Thammachart Chinvarapon</li>
+ <li>Patrick Evans</li>
</ul>
<h3>Translations:</h3>
<ul>
@@ -207,7 +210,7 @@ AboutDialog::AboutDialog(QWidget* parent)
nameLabelFont.setPointSize(nameLabelFont.pointSize() + 4);
m_ui->nameLabel->setFont(nameLabelFont);
- m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48));
+ m_ui->iconLabel->setPixmap(resources()->applicationIcon().pixmap(48));
QString debugInfo = Tools::debugInfo().append("\n").append(Crypto::debugInfo());
m_ui->debugInfo->setPlainText(debugInfo);
@@ -218,6 +221,8 @@ AboutDialog::AboutDialog(QWidget* parent)
setAttribute(Qt::WA_DeleteOnClose);
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
connect(m_ui->copyToClipboard, SIGNAL(clicked()), SLOT(copyToClipboard()));
+
+ m_ui->buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
}
AboutDialog::~AboutDialog()
diff --git a/src/gui/AboutDialog.ui b/src/gui/AboutDialog.ui
index 8bd8ea01f..348c79cf7 100644
--- a/src/gui/AboutDialog.ui
+++ b/src/gui/AboutDialog.ui
@@ -231,7 +231,7 @@
<x>0</x>
<y>0</y>
<width>466</width>
- <height>246</height>
+ <height>242</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
@@ -336,6 +336,12 @@
</item>
</layout>
</widget>
+ <tabstops>
+ <tabstop>tabWidget</tabstop>
+ <tabstop>scrollArea</tabstop>
+ <tabstop>debugInfo</tabstop>
+ <tabstop>copyToClipboard</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp
index b79f2c30a..625540c1d 100644
--- a/src/gui/Application.cpp
+++ b/src/gui/Application.cpp
@@ -18,8 +18,14 @@
*/
#include "Application.h"
-#include "MainWindow.h"
+
+#include "autotype/AutoType.h"
#include "core/Config.h"
+#include "core/Global.h"
+#include "gui/MainWindow.h"
+#include "gui/osutils/OSUtils.h"
+#include "gui/styles/dark/DarkStyle.h"
+#include "gui/styles/light/LightStyle.h"
#include <QFileInfo>
#include <QFileOpenEvent>
@@ -28,9 +34,6 @@
#include <QStandardPaths>
#include <QtNetwork/QLocalSocket>
-#include "autotype/AutoType.h"
-#include "core/Global.h"
-
#if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
#include "core/OSEventFilter.h"
#endif
@@ -96,7 +99,7 @@ Application::Application(int& argc, char** argv)
m_lockServer.listen(m_socketName);
break;
case QLockFile::LockFailedError: {
- if (config()->get("SingleInstance").toBool()) {
+ if (config()->get(Config::SingleInstance).toBool()) {
// Attempt to connect to the existing instance
QLocalSocket client;
for (int i = 0; i < 3; ++i) {
@@ -139,6 +142,32 @@ Application::~Application()
}
}
+void Application::applyTheme()
+{
+ QString appTheme = config()->get(Config::GUI_ApplicationTheme).toString();
+ if (appTheme == "auto") {
+ if (osUtils->isDarkMode()) {
+ setStyle(new DarkStyle);
+ m_darkTheme = true;
+ } else {
+ setStyle(new LightStyle);
+ }
+ } else if (appTheme == "light") {
+ setStyle(new LightStyle);
+ } else if (appTheme == "dark") {
+ setStyle(new DarkStyle);
+ m_darkTheme = true;
+ } else {
+ // Classic mode, don't check for dark theme on Windows
+ // because Qt 5.x does not support it
+#ifndef Q_OS_WIN
+ m_darkTheme = osUtils->isDarkMode();
+#endif
+ }
+
+ setPalette(style()->standardPalette());
+}
+
bool Application::event(QEvent* event)
{
// Handle Apple QFileOpenEvent from finder (double click on .kdbx file)
@@ -257,7 +286,7 @@ bool Application::isAlreadyRunning() const
// In DEBUG mode we can run unlimited instances
return false;
#endif
- return config()->get("SingleInstance").toBool() && m_alreadyRunning;
+ return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning;
}
bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
@@ -281,3 +310,21 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
const bool disconnected = client.waitForDisconnected(WaitTimeoutMSec);
return writeOk && disconnected;
}
+
+bool Application::isDarkTheme() const
+{
+ return m_darkTheme;
+}
+
+void Application::restart()
+{
+ // Disable single instance
+ m_lockServer.close();
+ if (m_lockFile) {
+ m_lockFile->unlock();
+ delete m_lockFile;
+ m_lockFile = nullptr;
+ }
+
+ exit(RESTART_EXITCODE);
+}
diff --git a/src/gui/Application.h b/src/gui/Application.h
index 9a3ef756b..9f694f8c3 100644
--- a/src/gui/Application.h
+++ b/src/gui/Application.h
@@ -31,6 +31,8 @@ class OSEventFilter;
class QLockFile;
class QSocketNotifier;
+constexpr int RESTART_EXITCODE = -1;
+
class Application : public QApplication
{
Q_OBJECT
@@ -39,11 +41,16 @@ public:
Application(int& argc, char** argv);
~Application() override;
+ void applyTheme();
+
bool event(QEvent* event) override;
bool isAlreadyRunning() const;
+ bool isDarkTheme() const;
bool sendFileNamesToRunningInstance(const QStringList& fileNames);
+ void restart();
+
signals:
void openFile(const QString& filename);
void anotherInstanceStarted();
@@ -68,6 +75,7 @@ private:
static int unixSignalSocket[2];
#endif
bool m_alreadyRunning;
+ bool m_darkTheme = false;
QLockFile* m_lockFile;
QLocalServer m_lockServer;
QString m_socketName;
@@ -76,4 +84,6 @@ private:
#endif
};
+#define kpxcApp qobject_cast<Application*>(Application::instance())
+
#endif // KEEPASSX_APPLICATION_H
diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp
index d3cc994f8..691115368 100644
--- a/src/gui/ApplicationSettingsWidget.cpp
+++ b/src/gui/ApplicationSettingsWidget.cpp
@@ -24,9 +24,10 @@
#include "autotype/AutoType.h"
#include "core/Config.h"
-#include "core/FilePath.h"
#include "core/Global.h"
+#include "core/Resources.h"
#include "core/Translator.h"
+#include "gui/osutils/OSUtils.h"
#include "MessageBox.h"
#include "touchid/TouchID.h"
@@ -91,8 +92,8 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_secUi->setupUi(m_secWidget);
m_generalUi->setupUi(m_generalWidget);
- addPage(tr("General"), FilePath::instance()->icon("categories", "preferences-other"), m_generalWidget);
- addPage(tr("Security"), FilePath::instance()->icon("status", "security-high"), m_secWidget);
+ addPage(tr("General"), Resources::instance()->icon("preferences-other"), m_generalWidget);
+ addPage(tr("Security"), Resources::instance()->icon("security-high"), m_secWidget);
if (!autoType()->isAvailable()) {
m_generalUi->generalSettingsTabWidget->removeTab(1);
@@ -105,7 +106,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), SLOT(autoSaveToggled(bool)));
connect(m_generalUi->hideWindowOnCopyCheckBox, SIGNAL(toggled(bool)), SLOT(hideWindowOnCopyCheckBoxToggled(bool)));
connect(m_generalUi->systrayShowCheckBox, SIGNAL(toggled(bool)), SLOT(systrayToggled(bool)));
- connect(m_generalUi->toolbarHideCheckBox, SIGNAL(toggled(bool)), SLOT(toolbarSettingsToggled(bool)));
connect(m_generalUi->rememberLastDatabasesCheckBox, SIGNAL(toggled(bool)), SLOT(rememberDatabasesToggled(bool)));
connect(m_generalUi->resetSettingsButton, SIGNAL(clicked()), SLOT(resetSettings()));
@@ -172,46 +172,44 @@ void ApplicationSettingsWidget::loadSettings()
#ifdef QT_DEBUG
m_generalUi->singleInstanceCheckBox->setEnabled(false);
+ m_generalUi->launchAtStartup->setEnabled(false);
#endif
- m_generalUi->singleInstanceCheckBox->setChecked(config()->get("SingleInstance").toBool());
- m_generalUi->rememberLastDatabasesCheckBox->setChecked(config()->get("RememberLastDatabases").toBool());
- m_generalUi->rememberLastKeyFilesCheckBox->setChecked(config()->get("RememberLastKeyFiles").toBool());
+ m_generalUi->singleInstanceCheckBox->setChecked(config()->get(Config::SingleInstance).toBool());
+ m_generalUi->launchAtStartup->setChecked(osUtils->isLaunchAtStartupEnabled());
+ m_generalUi->rememberLastDatabasesCheckBox->setChecked(config()->get(Config::RememberLastDatabases).toBool());
+ m_generalUi->rememberLastKeyFilesCheckBox->setChecked(config()->get(Config::RememberLastKeyFiles).toBool());
m_generalUi->openPreviousDatabasesOnStartupCheckBox->setChecked(
- config()->get("OpenPreviousDatabasesOnStartup").toBool());
- m_generalUi->autoSaveAfterEveryChangeCheckBox->setChecked(config()->get("AutoSaveAfterEveryChange").toBool());
- m_generalUi->autoSaveOnExitCheckBox->setChecked(config()->get("AutoSaveOnExit").toBool());
- m_generalUi->backupBeforeSaveCheckBox->setChecked(config()->get("BackupBeforeSave").toBool());
- m_generalUi->useAtomicSavesCheckBox->setChecked(config()->get("UseAtomicSaves").toBool());
- m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get("AutoReloadOnChange").toBool());
- m_generalUi->minimizeAfterUnlockCheckBox->setChecked(config()->get("MinimizeAfterUnlock").toBool());
- m_generalUi->minimizeOnOpenUrlCheckBox->setChecked(config()->get("MinimizeOnOpenUrl").toBool());
- m_generalUi->hideWindowOnCopyCheckBox->setChecked(config()->get("HideWindowOnCopy").toBool());
- m_generalUi->minimizeOnCopyRadioButton->setChecked(config()->get("MinimizeOnCopy").toBool());
- m_generalUi->dropToBackgroundOnCopyRadioButton->setChecked(config()->get("DropToBackgroundOnCopy").toBool());
- m_generalUi->useGroupIconOnEntryCreationCheckBox->setChecked(config()->get("UseGroupIconOnEntryCreation").toBool());
- m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get("AutoTypeEntryTitleMatch").toBool());
- m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get("AutoTypeEntryURLMatch").toBool());
- m_generalUi->ignoreGroupExpansionCheckBox->setChecked(config()->get("IgnoreGroupExpansion").toBool());
- m_generalUi->faviconTimeoutSpinBox->setValue(config()->get("FaviconDownloadTimeout").toInt());
-
- if (!m_generalUi->hideWindowOnCopyCheckBox->isChecked()) {
- hideWindowOnCopyCheckBoxToggled(false);
- }
+ config()->get(Config::OpenPreviousDatabasesOnStartup).toBool());
+ m_generalUi->autoSaveAfterEveryChangeCheckBox->setChecked(config()->get(Config::AutoSaveAfterEveryChange).toBool());
+ m_generalUi->autoSaveOnExitCheckBox->setChecked(config()->get(Config::AutoSaveOnExit).toBool());
+ m_generalUi->backupBeforeSaveCheckBox->setChecked(config()->get(Config::BackupBeforeSave).toBool());
+ m_generalUi->useAtomicSavesCheckBox->setChecked(config()->get(Config::UseAtomicSaves).toBool());
+ m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get(Config::AutoReloadOnChange).toBool());
+ m_generalUi->minimizeAfterUnlockCheckBox->setChecked(config()->get(Config::MinimizeAfterUnlock).toBool());
+ m_generalUi->minimizeOnOpenUrlCheckBox->setChecked(config()->get(Config::MinimizeOnOpenUrl).toBool());
+ m_generalUi->hideWindowOnCopyCheckBox->setChecked(config()->get(Config::HideWindowOnCopy).toBool());
+ hideWindowOnCopyCheckBoxToggled(m_generalUi->hideWindowOnCopyCheckBox->isChecked());
+ m_generalUi->minimizeOnCopyRadioButton->setChecked(config()->get(Config::MinimizeOnCopy).toBool());
+ m_generalUi->dropToBackgroundOnCopyRadioButton->setChecked(config()->get(Config::DropToBackgroundOnCopy).toBool());
+ m_generalUi->useGroupIconOnEntryCreationCheckBox->setChecked(
+ config()->get(Config::UseGroupIconOnEntryCreation).toBool());
+ m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryTitleMatch).toBool());
+ m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryURLMatch).toBool());
+ m_generalUi->trackNonDataChangesCheckBox->setChecked(config()->get(Config::TrackNonDataChanges).toBool());
+ m_generalUi->faviconTimeoutSpinBox->setValue(config()->get(Config::FaviconDownloadTimeout).toInt());
m_generalUi->languageComboBox->clear();
QList<QPair<QString, QString>> languages = Translator::availableLanguages();
for (const auto& language : languages) {
m_generalUi->languageComboBox->addItem(language.second, language.first);
}
- int defaultIndex = m_generalUi->languageComboBox->findData(config()->get("GUI/Language"));
+ int defaultIndex = m_generalUi->languageComboBox->findData(config()->get(Config::GUI_Language));
if (defaultIndex > 0) {
m_generalUi->languageComboBox->setCurrentIndex(defaultIndex);
}
- m_generalUi->previewHideCheckBox->setChecked(config()->get("GUI/HidePreviewPanel").toBool());
- m_generalUi->toolbarHideCheckBox->setChecked(config()->get("GUI/HideToolbar").toBool());
- m_generalUi->toolbarMovableCheckBox->setChecked(config()->get("GUI/MovableToolbar").toBool());
- m_generalUi->monospaceNotesCheckBox->setChecked(config()->get("GUI/MonospaceNotes").toBool());
+ m_generalUi->toolbarMovableCheckBox->setChecked(config()->get(Config::GUI_MovableToolbar).toBool());
+ m_generalUi->monospaceNotesCheckBox->setChecked(config()->get(Config::GUI_MonospaceNotes).toBool());
m_generalUi->toolButtonStyleComboBox->clear();
m_generalUi->toolButtonStyleComboBox->addItem(tr("Icon only"), Qt::ToolButtonIconOnly);
@@ -219,55 +217,71 @@ void ApplicationSettingsWidget::loadSettings()
m_generalUi->toolButtonStyleComboBox->addItem(tr("Text beside icon"), Qt::ToolButtonTextBesideIcon);
m_generalUi->toolButtonStyleComboBox->addItem(tr("Text under icon"), Qt::ToolButtonTextUnderIcon);
m_generalUi->toolButtonStyleComboBox->addItem(tr("Follow style"), Qt::ToolButtonFollowStyle);
- int toolButtonStyleIndex = m_generalUi->toolButtonStyleComboBox->findData(config()->get("GUI/ToolButtonStyle"));
+ int toolButtonStyleIndex =
+ m_generalUi->toolButtonStyleComboBox->findData(config()->get(Config::GUI_ToolButtonStyle));
if (toolButtonStyleIndex > 0) {
m_generalUi->toolButtonStyleComboBox->setCurrentIndex(toolButtonStyleIndex);
}
- m_generalUi->systrayShowCheckBox->setChecked(config()->get("GUI/ShowTrayIcon").toBool());
- m_generalUi->systrayDarkIconCheckBox->setChecked(config()->get("GUI/DarkTrayIcon").toBool());
- m_generalUi->systrayMinimizeToTrayCheckBox->setChecked(config()->get("GUI/MinimizeToTray").toBool());
- m_generalUi->minimizeOnCloseCheckBox->setChecked(config()->get("GUI/MinimizeOnClose").toBool());
- m_generalUi->systrayMinimizeOnStartup->setChecked(config()->get("GUI/MinimizeOnStartup").toBool());
- m_generalUi->checkForUpdatesOnStartupCheckBox->setChecked(config()->get("GUI/CheckForUpdates").toBool());
+ m_generalUi->systrayShowCheckBox->setChecked(config()->get(Config::GUI_ShowTrayIcon).toBool());
+ systrayToggled(m_generalUi->systrayShowCheckBox->isChecked());
+ m_generalUi->systrayMinimizeToTrayCheckBox->setChecked(config()->get(Config::GUI_MinimizeToTray).toBool());
+ m_generalUi->minimizeOnCloseCheckBox->setChecked(config()->get(Config::GUI_MinimizeOnClose).toBool());
+ m_generalUi->systrayMinimizeOnStartup->setChecked(config()->get(Config::GUI_MinimizeOnStartup).toBool());
+ m_generalUi->checkForUpdatesOnStartupCheckBox->setChecked(config()->get(Config::GUI_CheckForUpdates).toBool());
+ checkUpdatesToggled(m_generalUi->checkForUpdatesOnStartupCheckBox->isChecked());
m_generalUi->checkForUpdatesIncludeBetasCheckBox->setChecked(
- config()->get("GUI/CheckForUpdatesIncludeBetas").toBool());
- m_generalUi->autoTypeAskCheckBox->setChecked(config()->get("security/autotypeask").toBool());
+ config()->get(Config::GUI_CheckForUpdatesIncludeBetas).toBool());
+
+ m_generalUi->autoTypeAskCheckBox->setChecked(config()->get(Config::Security_AutoTypeAsk).toBool());
if (autoType()->isAvailable()) {
- m_globalAutoTypeKey = static_cast<Qt::Key>(config()->get("GlobalAutoTypeKey").toInt());
+ m_globalAutoTypeKey = static_cast<Qt::Key>(config()->get(Config::GlobalAutoTypeKey).toInt());
m_globalAutoTypeModifiers =
- static_cast<Qt::KeyboardModifiers>(config()->get("GlobalAutoTypeModifiers").toInt());
+ static_cast<Qt::KeyboardModifiers>(config()->get(Config::GlobalAutoTypeModifiers).toInt());
if (m_globalAutoTypeKey > 0 && m_globalAutoTypeModifiers > 0) {
m_generalUi->autoTypeShortcutWidget->setShortcut(m_globalAutoTypeKey, m_globalAutoTypeModifiers);
}
m_generalUi->autoTypeShortcutWidget->setAttribute(Qt::WA_MacShowFocusRect, true);
- m_generalUi->autoTypeDelaySpinBox->setValue(config()->get("AutoTypeDelay").toInt());
- m_generalUi->autoTypeStartDelaySpinBox->setValue(config()->get("AutoTypeStartDelay").toInt());
+ m_generalUi->autoTypeDelaySpinBox->setValue(config()->get(Config::AutoTypeDelay).toInt());
+ m_generalUi->autoTypeStartDelaySpinBox->setValue(config()->get(Config::AutoTypeStartDelay).toInt());
}
- m_secUi->clearClipboardCheckBox->setChecked(config()->get("security/clearclipboard").toBool());
- m_secUi->clearClipboardSpinBox->setValue(config()->get("security/clearclipboardtimeout").toInt());
-
- m_secUi->clearSearchCheckBox->setChecked(config()->get("security/clearsearch").toBool());
- m_secUi->clearSearchSpinBox->setValue(config()->get("security/clearsearchtimeout").toInt());
-
- m_secUi->lockDatabaseIdleCheckBox->setChecked(config()->get("security/lockdatabaseidle").toBool());
- m_secUi->lockDatabaseIdleSpinBox->setValue(config()->get("security/lockdatabaseidlesec").toInt());
- m_secUi->lockDatabaseMinimizeCheckBox->setChecked(config()->get("security/lockdatabaseminimize").toBool());
- m_secUi->lockDatabaseOnScreenLockCheckBox->setChecked(config()->get("security/lockdatabasescreenlock").toBool());
- m_secUi->relockDatabaseAutoTypeCheckBox->setChecked(config()->get("security/relockautotype").toBool());
- m_secUi->fallbackToSearch->setChecked(config()->get("security/IconDownloadFallback").toBool());
-
- m_secUi->passwordCleartextCheckBox->setChecked(config()->get("security/passwordscleartext").toBool());
- m_secUi->passwordShowDotsCheckBox->setChecked(config()->get("security/passwordemptynodots").toBool());
- m_secUi->passwordPreviewCleartextCheckBox->setChecked(config()->get("security/HidePasswordPreviewPanel").toBool());
- m_secUi->passwordRepeatCheckBox->setChecked(config()->get("security/passwordsrepeat").toBool());
- m_secUi->hideNotesCheckBox->setChecked(config()->get("security/hidenotes").toBool());
+ m_generalUi->trayIconAppearance->clear();
+ m_generalUi->trayIconAppearance->addItem(tr("Monochrome (light)"), "monochrome-light");
+ m_generalUi->trayIconAppearance->addItem(tr("Monochrome (dark)"), "monochrome-dark");
+ m_generalUi->trayIconAppearance->addItem(tr("Colorful"), "colorful");
+ int trayIconIndex = m_generalUi->trayIconAppearance->findData(resources()->trayIconAppearance());
+ if (trayIconIndex > 0) {
+ m_generalUi->trayIconAppearance->setCurrentIndex(trayIconIndex);
+ }
- m_secUi->touchIDResetCheckBox->setChecked(config()->get("security/resettouchid").toBool());
- m_secUi->touchIDResetSpinBox->setValue(config()->get("security/resettouchidtimeout").toInt());
- m_secUi->touchIDResetOnScreenLockCheckBox->setChecked(config()->get("security/resettouchidscreenlock").toBool());
+ m_secUi->clearClipboardCheckBox->setChecked(config()->get(Config::Security_ClearClipboard).toBool());
+ m_secUi->clearClipboardSpinBox->setValue(config()->get(Config::Security_ClearClipboardTimeout).toInt());
+
+ m_secUi->clearSearchCheckBox->setChecked(config()->get(Config::Security_ClearSearch).toBool());
+ m_secUi->clearSearchSpinBox->setValue(config()->get(Config::Security_ClearSearchTimeout).toInt());
+
+ m_secUi->lockDatabaseIdleCheckBox->setChecked(config()->get(Config::Security_LockDatabaseIdle).toBool());
+ m_secUi->lockDatabaseIdleSpinBox->setValue(config()->get(Config::Security_LockDatabaseIdleSeconds).toInt());
+ m_secUi->lockDatabaseMinimizeCheckBox->setChecked(config()->get(Config::Security_LockDatabaseMinimize).toBool());
+ m_secUi->lockDatabaseOnScreenLockCheckBox->setChecked(
+ config()->get(Config::Security_LockDatabaseScreenLock).toBool());
+ m_secUi->relockDatabaseAutoTypeCheckBox->setChecked(config()->get(Config::Security_RelockAutoType).toBool());
+ m_secUi->fallbackToSearch->setChecked(config()->get(Config::Security_IconDownloadFallback).toBool());
+
+ m_secUi->passwordsHiddenCheckBox->setChecked(config()->get(Config::Security_PasswordsHidden).toBool());
+ m_secUi->passwordShowDotsCheckBox->setChecked(config()->get(Config::Security_PasswordEmptyPlaceholder).toBool());
+ m_secUi->passwordPreviewCleartextCheckBox->setChecked(
+ config()->get(Config::Security_HidePasswordPreviewPanel).toBool());
+ m_secUi->passwordsRepeatVisibleCheckBox->setChecked(
+ config()->get(Config::Security_PasswordsRepeatVisible).toBool());
+ m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool());
+
+ m_secUi->touchIDResetCheckBox->setChecked(config()->get(Config::Security_ResetTouchId).toBool());
+ m_secUi->touchIDResetSpinBox->setValue(config()->get(Config::Security_ResetTouchIdTimeout).toInt());
+ m_secUi->touchIDResetOnScreenLockCheckBox->setChecked(
+ config()->get(Config::Security_ResetTouchIdScreenlock).toBool());
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings();
@@ -285,89 +299,91 @@ void ApplicationSettingsWidget::saveSettings()
return;
}
- config()->set("SingleInstance", m_generalUi->singleInstanceCheckBox->isChecked());
- config()->set("RememberLastDatabases", m_generalUi->rememberLastDatabasesCheckBox->isChecked());
- config()->set("RememberLastKeyFiles", m_generalUi->rememberLastKeyFilesCheckBox->isChecked());
- config()->set("OpenPreviousDatabasesOnStartup", m_generalUi->openPreviousDatabasesOnStartupCheckBox->isChecked());
- config()->set("AutoSaveAfterEveryChange", m_generalUi->autoSaveAfterEveryChangeCheckBox->isChecked());
- config()->set("AutoSaveOnExit", m_generalUi->autoSaveOnExitCheckBox->isChecked());
- config()->set("BackupBeforeSave", m_generalUi->backupBeforeSaveCheckBox->isChecked());
- config()->set("UseAtomicSaves", m_generalUi->useAtomicSavesCheckBox->isChecked());
- config()->set("AutoReloadOnChange", m_generalUi->autoReloadOnChangeCheckBox->isChecked());
- config()->set("MinimizeAfterUnlock", m_generalUi->minimizeAfterUnlockCheckBox->isChecked());
- config()->set("MinimizeOnOpenUrl", m_generalUi->minimizeOnOpenUrlCheckBox->isChecked());
- config()->set("HideWindowOnCopy", m_generalUi->hideWindowOnCopyCheckBox->isChecked());
- config()->set("MinimizeOnCopy", m_generalUi->minimizeOnCopyRadioButton->isChecked());
- config()->set("DropToBackgroundOnCopy", m_generalUi->dropToBackgroundOnCopyRadioButton->isChecked());
- config()->set("UseGroupIconOnEntryCreation", m_generalUi->useGroupIconOnEntryCreationCheckBox->isChecked());
- config()->set("IgnoreGroupExpansion", m_generalUi->ignoreGroupExpansionCheckBox->isChecked());
- config()->set("AutoTypeEntryTitleMatch", m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked());
- config()->set("AutoTypeEntryURLMatch", m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked());
- int currentLangIndex = m_generalUi->languageComboBox->currentIndex();
- config()->set("FaviconDownloadTimeout", m_generalUi->faviconTimeoutSpinBox->value());
-
- config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString());
-
- config()->set("GUI/HidePreviewPanel", m_generalUi->previewHideCheckBox->isChecked());
- config()->set("GUI/HideToolbar", m_generalUi->toolbarHideCheckBox->isChecked());
- config()->set("GUI/MovableToolbar", m_generalUi->toolbarMovableCheckBox->isChecked());
- config()->set("GUI/MonospaceNotes", m_generalUi->monospaceNotesCheckBox->isChecked());
-
- int currentToolButtonStyleIndex = m_generalUi->toolButtonStyleComboBox->currentIndex();
- config()->set("GUI/ToolButtonStyle",
- m_generalUi->toolButtonStyleComboBox->itemData(currentToolButtonStyleIndex).toString());
-
- config()->set("GUI/ShowTrayIcon", m_generalUi->systrayShowCheckBox->isChecked());
- config()->set("GUI/DarkTrayIcon", m_generalUi->systrayDarkIconCheckBox->isChecked());
- config()->set("GUI/MinimizeToTray", m_generalUi->systrayMinimizeToTrayCheckBox->isChecked());
- config()->set("GUI/MinimizeOnClose", m_generalUi->minimizeOnCloseCheckBox->isChecked());
- config()->set("GUI/MinimizeOnStartup", m_generalUi->systrayMinimizeOnStartup->isChecked());
- config()->set("GUI/CheckForUpdates", m_generalUi->checkForUpdatesOnStartupCheckBox->isChecked());
- config()->set("GUI/CheckForUpdatesIncludeBetas", m_generalUi->checkForUpdatesIncludeBetasCheckBox->isChecked());
-
- config()->set("security/autotypeask", m_generalUi->autoTypeAskCheckBox->isChecked());
+#ifndef QT_DEBUG
+ osUtils->setLaunchAtStartup(m_generalUi->launchAtStartup->isChecked());
+#endif
+
+ config()->set(Config::SingleInstance, m_generalUi->singleInstanceCheckBox->isChecked());
+ config()->set(Config::RememberLastDatabases, m_generalUi->rememberLastDatabasesCheckBox->isChecked());
+ config()->set(Config::RememberLastKeyFiles, m_generalUi->rememberLastKeyFilesCheckBox->isChecked());
+ config()->set(Config::OpenPreviousDatabasesOnStartup,
+ m_generalUi->openPreviousDatabasesOnStartupCheckBox->isChecked());
+ config()->set(Config::AutoSaveAfterEveryChange, m_generalUi->autoSaveAfterEveryChangeCheckBox->isChecked());
+ config()->set(Config::AutoSaveOnExit, m_generalUi->autoSaveOnExitCheckBox->isChecked());
+ config()->set(Config::BackupBeforeSave, m_generalUi->backupBeforeSaveCheckBox->isChecked());
+ config()->set(Config::UseAtomicSaves, m_generalUi->useAtomicSavesCheckBox->isChecked());
+ config()->set(Config::AutoReloadOnChange, m_generalUi->autoReloadOnChangeCheckBox->isChecked());
+ config()->set(Config::MinimizeAfterUnlock, m_generalUi->minimizeAfterUnlockCheckBox->isChecked());
+ config()->set(Config::MinimizeOnOpenUrl, m_generalUi->minimizeOnOpenUrlCheckBox->isChecked());
+ config()->set(Config::HideWindowOnCopy, m_generalUi->hideWindowOnCopyCheckBox->isChecked());
+ config()->set(Config::MinimizeOnCopy, m_generalUi->minimizeOnCopyRadioButton->isChecked());
+ config()->set(Config::DropToBackgroundOnCopy, m_generalUi->dropToBackgroundOnCopyRadioButton->isChecked());
+ config()->set(Config::UseGroupIconOnEntryCreation, m_generalUi->useGroupIconOnEntryCreationCheckBox->isChecked());
+ config()->set(Config::TrackNonDataChanges, m_generalUi->trackNonDataChangesCheckBox->isChecked());
+ config()->set(Config::AutoTypeEntryTitleMatch, m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked());
+ config()->set(Config::AutoTypeEntryURLMatch, m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked());
+ config()->set(Config::FaviconDownloadTimeout, m_generalUi->faviconTimeoutSpinBox->value());
+
+ config()->set(Config::GUI_Language, m_generalUi->languageComboBox->currentData().toString());
+ config()->set(Config::GUI_MovableToolbar, m_generalUi->toolbarMovableCheckBox->isChecked());
+ config()->set(Config::GUI_MonospaceNotes, m_generalUi->monospaceNotesCheckBox->isChecked());
+
+ config()->set(Config::GUI_ToolButtonStyle, m_generalUi->toolButtonStyleComboBox->currentData().toString());
+
+ config()->set(Config::GUI_ShowTrayIcon, m_generalUi->systrayShowCheckBox->isChecked());
+ config()->set(Config::GUI_TrayIconAppearance, m_generalUi->trayIconAppearance->currentData().toString());
+ config()->set(Config::GUI_MinimizeToTray, m_generalUi->systrayMinimizeToTrayCheckBox->isChecked());
+ config()->set(Config::GUI_MinimizeOnClose, m_generalUi->minimizeOnCloseCheckBox->isChecked());
+ config()->set(Config::GUI_MinimizeOnStartup, m_generalUi->systrayMinimizeOnStartup->isChecked());
+ config()->set(Config::GUI_CheckForUpdates, m_generalUi->checkForUpdatesOnStartupCheckBox->isChecked());
+ config()->set(Config::GUI_CheckForUpdatesIncludeBetas,
+ m_generalUi->checkForUpdatesIncludeBetasCheckBox->isChecked());
+
+ config()->set(Config::Security_AutoTypeAsk, m_generalUi->autoTypeAskCheckBox->isChecked());
if (autoType()->isAvailable()) {
- config()->set("GlobalAutoTypeKey", m_generalUi->autoTypeShortcutWidget->key());
- config()->set("GlobalAutoTypeModifiers", static_cast<int>(m_generalUi->autoTypeShortcutWidget->modifiers()));
- config()->set("AutoTypeDelay", m_generalUi->autoTypeDelaySpinBox->value());
- config()->set("AutoTypeStartDelay", m_generalUi->autoTypeStartDelaySpinBox->value());
+ config()->set(Config::GlobalAutoTypeKey, m_generalUi->autoTypeShortcutWidget->key());
+ config()->set(Config::GlobalAutoTypeModifiers,
+ static_cast<int>(m_generalUi->autoTypeShortcutWidget->modifiers()));
+ config()->set(Config::AutoTypeDelay, m_generalUi->autoTypeDelaySpinBox->value());
+ config()->set(Config::AutoTypeStartDelay, m_generalUi->autoTypeStartDelaySpinBox->value());
}
- config()->set("security/clearclipboard", m_secUi->clearClipboardCheckBox->isChecked());
- config()->set("security/clearclipboardtimeout", m_secUi->clearClipboardSpinBox->value());
+ config()->set(Config::Security_ClearClipboard, m_secUi->clearClipboardCheckBox->isChecked());
+ config()->set(Config::Security_ClearClipboardTimeout, m_secUi->clearClipboardSpinBox->value());
- config()->set("security/clearsearch", m_secUi->clearSearchCheckBox->isChecked());
- config()->set("security/clearsearchtimeout", m_secUi->clearSearchSpinBox->value());
+ config()->set(Config::Security_ClearSearch, m_secUi->clearSearchCheckBox->isChecked());
+ config()->set(Config::Security_ClearSearchTimeout, m_secUi->clearSearchSpinBox->value());
- config()->set("security/lockdatabaseidle", m_secUi->lockDatabaseIdleCheckBox->isChecked());
- config()->set("security/lockdatabaseidlesec", m_secUi->lockDatabaseIdleSpinBox->value());
- config()->set("security/lockdatabaseminimize", m_secUi->lockDatabaseMinimizeCheckBox->isChecked());
- config()->set("security/lockdatabasescreenlock", m_secUi->lockDatabaseOnScreenLockCheckBox->isChecked());
- config()->set("security/relockautotype", m_secUi->relockDatabaseAutoTypeCheckBox->isChecked());
- config()->set("security/IconDownloadFallback", m_secUi->fallbackToSearch->isChecked());
+ config()->set(Config::Security_LockDatabaseIdle, m_secUi->lockDatabaseIdleCheckBox->isChecked());
+ config()->set(Config::Security_LockDatabaseIdleSeconds, m_secUi->lockDatabaseIdleSpinBox->value());
+ config()->set(Config::Security_LockDatabaseMinimize, m_secUi->lockDatabaseMinimizeCheckBox->isChecked());
+ config()->set(Config::Security_LockDatabaseScreenLock, m_secUi->lockDatabaseOnScreenLockCheckBox->isChecked());
+ config()->set(Config::Security_RelockAutoType, m_secUi->relockDatabaseAutoTypeCheckBox->isChecked());
+ config()->set(Config::Security_IconDownloadFallback, m_secUi->fallbackToSearch->isChecked());
- config()->set("security/passwordscleartext", m_secUi->passwordCleartextCheckBox->isChecked());
- config()->set("security/passwordemptynodots", m_secUi->passwordShowDotsCheckBox->isChecked());
+ config()->set(Config::Security_PasswordsHidden, m_secUi->passwordsHiddenCheckBox->isChecked());
+ config()->set(Config::Security_PasswordEmptyPlaceholder, m_secUi->passwordShowDotsCheckBox->isChecked());
- config()->set("security/HidePasswordPreviewPanel", m_secUi->passwordPreviewCleartextCheckBox->isChecked());
- config()->set("security/passwordsrepeat", m_secUi->passwordRepeatCheckBox->isChecked());
- config()->set("security/hidenotes", m_secUi->hideNotesCheckBox->isChecked());
+ config()->set(Config::Security_HidePasswordPreviewPanel, m_secUi->passwordPreviewCleartextCheckBox->isChecked());
+ config()->set(Config::Security_PasswordsRepeatVisible, m_secUi->passwordsRepeatVisibleCheckBox->isChecked());
+ config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked());
- config()->set("security/resettouchid", m_secUi->touchIDResetCheckBox->isChecked());
- config()->set("security/resettouchidtimeout", m_secUi->touchIDResetSpinBox->value());
- config()->set("security/resettouchidscreenlock", m_secUi->touchIDResetOnScreenLockCheckBox->isChecked());
+ config()->set(Config::Security_ResetTouchId, m_secUi->touchIDResetCheckBox->isChecked());
+ config()->set(Config::Security_ResetTouchIdTimeout, m_secUi->touchIDResetSpinBox->value());
+ config()->set(Config::Security_ResetTouchIdScreenlock, m_secUi->touchIDResetOnScreenLockCheckBox->isChecked());
// Security: clear storage if related settings are disabled
- if (!config()->get("RememberLastDatabases").toBool()) {
- config()->set("LastDatabases", {});
- config()->set("OpenPreviousDatabasesOnStartup", {});
- config()->set("LastActiveDatabase", {});
- config()->set("LastAttachmentDir", {});
+ if (!config()->get(Config::RememberLastDatabases).toBool()) {
+ config()->remove(Config::LastDatabases);
+ config()->remove(Config::OpenPreviousDatabasesOnStartup);
+ config()->remove(Config::LastActiveDatabase);
+ config()->remove(Config::LastAttachmentDir);
}
- if (!config()->get("RememberLastKeyFiles").toBool()) {
- config()->set("LastKeyFiles", {});
- config()->set("LastDir", "");
+ if (!config()->get(Config::RememberLastKeyFiles).toBool()) {
+ config()->remove(Config::LastKeyFiles);
+ config()->remove(Config::LastChallengeResponse);
+ config()->remove(Config::LastDir);
}
for (const ExtraPage& page : asConst(m_extraPages)) {
@@ -398,12 +414,12 @@ void ApplicationSettingsWidget::resetSettings()
config()->resetToDefaults();
// Clear recently used data
- config()->set("LastDatabases", {});
- config()->set("OpenPreviousDatabasesOnStartup", {});
- config()->set("LastActiveDatabase", {});
- config()->set("LastAttachmentDir", {});
- config()->set("LastKeyFiles", {});
- config()->set("LastDir", "");
+ config()->remove(Config::LastDatabases);
+ config()->remove(Config::OpenPreviousDatabasesOnStartup);
+ config()->remove(Config::LastActiveDatabase);
+ config()->remove(Config::LastAttachmentDir);
+ config()->remove(Config::LastKeyFiles);
+ config()->remove(Config::LastDir);
// Save the Extra Pages (these are NOT reset)
for (const ExtraPage& page : asConst(m_extraPages)) {
@@ -442,17 +458,11 @@ void ApplicationSettingsWidget::hideWindowOnCopyCheckBoxToggled(bool checked)
void ApplicationSettingsWidget::systrayToggled(bool checked)
{
- m_generalUi->systrayDarkIconCheckBox->setEnabled(checked);
+ m_generalUi->trayIconAppearance->setEnabled(checked);
+ m_generalUi->trayIconAppearanceLabel->setEnabled(checked);
m_generalUi->systrayMinimizeToTrayCheckBox->setEnabled(checked);
}
-void ApplicationSettingsWidget::toolbarSettingsToggled(bool checked)
-{
- m_generalUi->toolbarMovableCheckBox->setEnabled(!checked);
- m_generalUi->toolButtonStyleComboBox->setEnabled(!checked);
- m_generalUi->toolButtonStyleLabel->setEnabled(!checked);
-}
-
void ApplicationSettingsWidget::rememberDatabasesToggled(bool checked)
{
if (!checked) {
diff --git a/src/gui/ApplicationSettingsWidget.h b/src/gui/ApplicationSettingsWidget.h
index 63487e1b5..f36e5ef12 100644
--- a/src/gui/ApplicationSettingsWidget.h
+++ b/src/gui/ApplicationSettingsWidget.h
@@ -60,7 +60,6 @@ private slots:
void autoSaveToggled(bool checked);
void hideWindowOnCopyCheckBoxToggled(bool checked);
void systrayToggled(bool checked);
- void toolbarSettingsToggled(bool checked);
void rememberDatabasesToggled(bool checked);
void checkUpdatesToggled(bool checked);
diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui
index 3c6de499a..7324c5ab7 100644
--- a/src/gui/ApplicationSettingsWidgetGeneral.ui
+++ b/src/gui/ApplicationSettingsWidgetGeneral.ui
@@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
- <width>684</width>
- <height>951</height>
+ <width>605</width>
+ <height>1279</height>
</rect>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_3">
+ <layout class="QVBoxLayout" name="verticalLayout_3" stretch="0">
<property name="leftMargin">
<number>0</number>
</property>
@@ -32,741 +32,775 @@
<attribute name="title">
<string>Basic Settings</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0">
<item>
- <widget class="QGroupBox" name="startupGroup">
- <property name="title">
- <string>Startup</string>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <item>
- <widget class="QCheckBox" name="singleInstanceCheckBox">
- <property name="text">
- <string>Start only a single instance of KeePassXC</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="systrayMinimizeOnStartup">
- <property name="text">
- <string>Minimize window at application startup</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="minimizeAfterUnlockCheckBox">
- <property name="text">
- <string>Minimize window after unlocking database</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="rememberLastDatabasesCheckBox">
- <property name="text">
- <string>Remember previously used databases</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="rememberDbSubLayout_2">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <spacer name="toolbarMovableSpacer_3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="openPreviousDatabasesOnStartupCheckBox">
- <property name="text">
- <string>Load previously open databases on startup</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="rememberDbSubLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <spacer name="toolbarMovableSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="rememberLastKeyFilesCheckBox">
- <property name="text">
- <string>Remember database key files and security dongles</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="checkForUpdatesOnStartupCheckBox">
- <property name="text">
- <string>Check for updates at application startup once per week</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="checkUpdatesSubLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <spacer name="checkUpdatesSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="checkForUpdatesIncludeBetasCheckBox">
- <property name="text">
- <string>Include beta releases when checking for updates</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QGroupBox" name="saveGroup">
- <property name="title">
- <string>File Management</string>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <item>
- <widget class="QCheckBox" name="useAtomicSavesCheckBox">
- <property name="text">
- <string>Safely save database files (may be incompatible with Dropbox, etc)</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="backupBeforeSaveCheckBox">
- <property name="text">
- <string>Backup database file before saving</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="autoSaveAfterEveryChangeCheckBox">
- <property name="text">
- <string>Automatically save after every change</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="autoSaveOnExitCheckBox">
- <property name="text">
- <string>Automatically save on exit</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="ignoreGroupExpansionCheckBox">
- <property name="text">
- <string>Don't mark database as modified for non-data changes (e.g., expanding groups)</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="autoReloadOnChangeCheckBox">
- <property name="text">
- <string>Automatically reload the database when modified externally</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QGroupBox" name="entryGroup">
- <property name="title">
- <string>Entry Management</string>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
- <item>
- <widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
- <property name="text">
- <string>Use group icon on entry creation</string>
- </property>
- <property name="checked">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="previewHideCheckBox">
- <property name="text">
- <string>Hide the entry preview panel</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="minimizeOnOpenUrlCheckBox">
- <property name="text">
- <string>Minimize when opening a URL</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="hideWindowOnCopyCheckBox">
- <property name="text">
- <string>Hide window when copying to clipboard</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="hideWindowLayout_1">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <spacer name="hideWindowSpacer_1">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QRadioButton" name="minimizeOnCopyRadioButton">
- <property name="text">
- <string>Minimize</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="hideWindowLayout_2">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <spacer name="hideWindowSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QRadioButton" name="dropToBackgroundOnCopyRadioButton">
- <property name="text">
- <string>Drop to background</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1">
- <item>
- <widget class="QLabel" name="faviconTimeoutLabel">
- <property name="text">
- <string>Favicon download timeout:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QSpinBox" name="faviconTimeoutSpinBox">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="accessibleName">
- <string>Website icon download timeout in seconds</string>
- </property>
- <property name="suffix">
- <string comment="Seconds"> sec</string>
- </property>
- <property name="minimum">
- <number>1</number>
- </property>
- <property name="maximum">
- <number>60</number>
- </property>
- <property name="value">
- <number>10</number>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QGroupBox" name="generalGroup">
- <property name="title">
- <string>General</string>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_7">
- <item>
- <widget class="QCheckBox" name="toolbarHideCheckBox">
- <property name="text">
- <string>Hide toolbar (icons)</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="toolbarMovableLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <spacer name="toolbarMovableSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="toolbarMovableCheckBox">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Movable toolbar</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="toolButtonStyleLayout" stretch="0,0,0,1">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <spacer name="toolButtonStyleSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item alignment="Qt::AlignRight">
- <widget class="QLabel" name="toolButtonStyleLabel">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="styleSheet">
- <string notr="true">margin-right: 5px</string>
- </property>
- <property name="text">
- <string>Button style:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="toolButtonStyleComboBox">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="accessibleName">
- <string>Toolbar button style</string>
- </property>
- <property name="sizeAdjustPolicy">
- <enum>QComboBox::AdjustToContents</enum>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QCheckBox" name="monospaceNotesCheckBox">
- <property name="text">
- <string>Use monospaced font for Notes</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="minimizeOnCloseCheckBox">
- <property name="text">
- <string>Minimize instead of app exit</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="systrayShowCheckBox">
- <property name="text">
- <string>Show a system tray icon</string>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <spacer name="horizontalSpacer_4">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="systrayDarkIconCheckBox">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="text">
- <string>Dark system tray icon</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QWidget" name="systraySettings" native="true">
- <layout class="QVBoxLayout" name="systrayLayout">
- <property name="leftMargin">
- <number>0</number>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>581</width>
+ <height>1235</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="startupGroup">
+ <property name="title">
+ <string>Startup</string>
</property>
- <property name="topMargin">
- <number>0</number>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QCheckBox" name="singleInstanceCheckBox">
+ <property name="text">
+ <string>Start only a single instance of KeePassXC</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="launchAtStartup">
+ <property name="text">
+ <string>Automatically launch KeePassXC at system startup</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="systrayMinimizeOnStartup">
+ <property name="text">
+ <string>Minimize window at application startup</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="minimizeAfterUnlockCheckBox">
+ <property name="text">
+ <string>Minimize window after unlocking database</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="rememberLastDatabasesCheckBox">
+ <property name="text">
+ <string>Remember previously used databases</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="rememberDbSubLayout_2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <item>
+ <spacer name="toolbarMovableSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="openPreviousDatabasesOnStartupCheckBox">
+ <property name="text">
+ <string>Load previously open databases on startup</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="rememberDbSubLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <item>
+ <spacer name="toolbarMovableSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="rememberLastKeyFilesCheckBox">
+ <property name="text">
+ <string>Remember database key files and security dongles</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkForUpdatesOnStartupCheckBox">
+ <property name="text">
+ <string>Check for updates at application startup once per week</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="checkUpdatesSubLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="checkUpdatesSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkForUpdatesIncludeBetasCheckBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Include beta releases when checking for updates</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="saveGroup">
+ <property name="title">
+ <string>File Management</string>
</property>
- <property name="rightMargin">
- <number>0</number>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <widget class="QCheckBox" name="autoSaveAfterEveryChangeCheckBox">
+ <property name="text">
+ <string>Automatically save after every change</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoSaveOnExitCheckBox">
+ <property name="text">
+ <string>Automatically save on exit</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="trackNonDataChangesCheckBox">
+ <property name="text">
+ <string>Mark database as modified for non-data changes (e.g., expanding groups)</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="backupBeforeSaveCheckBox">
+ <property name="text">
+ <string>Backup database file before saving</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="autoReloadOnChangeCheckBox">
+ <property name="text">
+ <string>Automatically reload the database when modified externally</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="useAtomicSavesCheckBox">
+ <property name="text">
+ <string>Safely save database files (disable if experiencing problems with Dropbox, etc.)</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="entryGroup">
+ <property name="title">
+ <string>Entry Management</string>
</property>
- <property name="bottomMargin">
- <number>0</number>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
+ <property name="text">
+ <string>Use group icon on entry creation</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="minimizeOnOpenUrlCheckBox">
+ <property name="text">
+ <string>Minimize when opening a URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="hideWindowOnCopyCheckBox">
+ <property name="text">
+ <string>Hide window when copying to clipboard</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="hideWindowLayout_1">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="hideWindowSpacer_1">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="minimizeOnCopyRadioButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Minimize</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="hideWindowLayout_2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="hideWindowSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="dropToBackgroundOnCopyRadioButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Drop to background</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="faviconTimeoutLabel">
+ <property name="text">
+ <string>Favicon download timeout:</string>
+ </property>
+ <property name="buddy">
+ <cstring>faviconTimeoutSpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="faviconTimeoutSpinBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <property name="accessibleName">
+ <string>Website icon download timeout in seconds</string>
+ </property>
+ <property name="suffix">
+ <string comment="Seconds"> sec</string>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>60</number>
+ </property>
+ <property name="value">
+ <number>10</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="generalGroup">
+ <property name="title">
+ <string>User Interface</string>
</property>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="toolButtonStyleComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <property name="accessibleName">
+ <string>Toolbar button style</string>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QComboBox::AdjustToContents</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QCheckBox" name="toolbarMovableCheckBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Movable toolbar</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="languageComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::StrongFocus</enum>
+ </property>
+ <property name="accessibleName">
+ <string>Language selection</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="languageLabel_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Language:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>languageComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLabel" name="languageLabel_3">
+ <property name="text">
+ <string>(restart program to activate)</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="toolButtonStyleLabel">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">margin-right: 5px</string>
+ </property>
+ <property name="text">
+ <string>Toolbar button style:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>toolButtonStyleComboBox</cstring>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="monospaceNotesCheckBox">
+ <property name="text">
+ <string>Use monospaced font for notes</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="minimizeOnCloseCheckBox">
+ <property name="text">
+ <string>Minimize instead of app exit</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="systrayShowCheckBox">
+ <property name="text">
+ <string>Show a system tray icon</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="0" column="3">
+ <widget class="QComboBox" name="trayIconAppearance">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="trayIconAppearanceLabel">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Tray icon type:</string>
+ </property>
+ <property name="buddy">
+ <cstring>trayIconAppearance</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="4">
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="0" column="2">
+ <spacer name="verticalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>6</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QWidget" name="systraySettings" native="true">
+ <layout class="QVBoxLayout" name="systrayLayout">
+ <property name="leftMargin">
+ <number>0</number>
</property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="systrayMinimizeToTrayCheckBox">
- <property name="enabled">
- <bool>false</bool>
+ <property name="topMargin">
+ <number>0</number>
</property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <property name="rightMargin">
+ <number>0</number>
</property>
- <property name="text">
- <string>Hide window to system tray when minimized</string>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- </layout>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="systrayMinimizeToTrayCheckBox">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Hide window to system tray when minimized</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="resetSettingsSubLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMaximumSize</enum>
+ </property>
+ <item>
+ <widget class="QPushButton" name="resetSettingsButton">
+ <property name="text">
+ <string>Reset settings to default…</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="spacer4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>50</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
</item>
</layout>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="languageLabelLayout_2" stretch="0,0,0,1">
- <property name="spacing">
- <number>8</number>
- </property>
- <item alignment="Qt::AlignRight">
- <widget class="QLabel" name="languageLabel_2">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Language:</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="languageComboBox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="accessibleName">
- <string>Language selection</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="languageLabel_3">
- <property name="text">
- <string>(restart program to activate)</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="spacer3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>15</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <layout class="QHBoxLayout" name="resetSettingsSubLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMaximumSize</enum>
- </property>
- <item>
- <widget class="QPushButton" name="resetSettingsButton">
- <property name="text">
- <string>Reset Settings to Default</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="spacer4">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>50</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Expanding</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
</layout>
</widget>
<widget class="QWidget" name="tabAutotype">
@@ -774,6 +808,18 @@
<string>Auto-Type</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>10</number>
+ </property>
+ <property name="topMargin">
+ <number>10</number>
+ </property>
+ <property name="rightMargin">
+ <number>10</number>
+ </property>
+ <property name="bottomMargin">
+ <number>10</number>
+ </property>
<item>
<widget class="QCheckBox" name="autoTypeEntryTitleMatchCheckBox">
<property name="text">
@@ -799,21 +845,46 @@
</widget>
</item>
<item>
- <layout class="QFormLayout" name="formLayout_2">
- <property name="topMargin">
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="horizontalSpacing">
<number>10</number>
</property>
- <item row="1" column="0">
- <widget class="QLabel" name="autoTypeShortcutLabel">
+ <property name="verticalSpacing">
+ <number>8</number>
+ </property>
+ <item row="2" column="0">
+ <widget class="QLabel" name="autoTypeDelayLabel">
<property name="text">
- <string>Global Auto-Type shortcut</string>
+ <string>Auto-Type typing delay:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>autoTypeDelaySpinBox</cstring>
</property>
</widget>
</item>
- <item row="1" column="1">
+ <item row="0" column="1">
<widget class="ShortcutWidget" name="autoTypeShortcutWidget">
<property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -823,17 +894,10 @@
</property>
</widget>
</item>
- <item row="3" column="0">
- <widget class="QLabel" name="autoTypeDelayLabel">
- <property name="text">
- <string>Auto-Type typing delay</string>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
+ <item row="2" column="1">
<widget class="QSpinBox" name="autoTypeDelaySpinBox">
<property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -855,17 +919,10 @@
</property>
</widget>
</item>
- <item row="2" column="0">
- <widget class="QLabel" name="autoTypeStartDelayLabel">
- <property name="text">
- <string>Auto-Type start delay</string>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
+ <item row="1" column="1">
<widget class="QSpinBox" name="autoTypeStartDelaySpinBox">
<property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -893,6 +950,45 @@
</property>
</widget>
</item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="autoTypeShortcutLabel">
+ <property name="text">
+ <string>Global Auto-Type shortcut:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>autoTypeShortcutWidget</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="autoTypeStartDelayLabel">
+ <property name="text">
+ <string>Auto-Type start delay:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <property name="buddy">
+ <cstring>autoTypeStartDelaySpinBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <spacer name="horizontalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
</layout>
</item>
<item>
@@ -903,7 +999,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
- <height>40</height>
+ <height>0</height>
</size>
</property>
</spacer>
@@ -921,6 +1017,46 @@
<header>autotype/ShortcutWidget.h</header>
</customwidget>
</customwidgets>
+ <tabstops>
+ <tabstop>generalSettingsTabWidget</tabstop>
+ <tabstop>scrollArea</tabstop>
+ <tabstop>singleInstanceCheckBox</tabstop>
+ <tabstop>launchAtStartup</tabstop>
+ <tabstop>systrayMinimizeOnStartup</tabstop>
+ <tabstop>minimizeAfterUnlockCheckBox</tabstop>
+ <tabstop>rememberLastDatabasesCheckBox</tabstop>
+ <tabstop>openPreviousDatabasesOnStartupCheckBox</tabstop>
+ <tabstop>rememberLastKeyFilesCheckBox</tabstop>
+ <tabstop>checkForUpdatesOnStartupCheckBox</tabstop>
+ <tabstop>checkForUpdatesIncludeBetasCheckBox</tabstop>
+ <tabstop>autoSaveAfterEveryChangeCheckBox</tabstop>
+ <tabstop>autoSaveOnExitCheckBox</tabstop>
+ <tabstop>trackNonDataChangesCheckBox</tabstop>
+ <tabstop>backupBeforeSaveCheckBox</tabstop>
+ <tabstop>autoReloadOnChangeCheckBox</tabstop>
+ <tabstop>useAtomicSavesCheckBox</tabstop>
+ <tabstop>useGroupIconOnEntryCreationCheckBox</tabstop>
+ <tabstop>minimizeOnOpenUrlCheckBox</tabstop>
+ <tabstop>hideWindowOnCopyCheckBox</tabstop>
+ <tabstop>minimizeOnCopyRadioButton</tabstop>
+ <tabstop>dropToBackgroundOnCopyRadioButton</tabstop>
+ <tabstop>faviconTimeoutSpinBox</tabstop>
+ <tabstop>languageComboBox</tabstop>
+ <tabstop>toolButtonStyleComboBox</tabstop>
+ <tabstop>monospaceNotesCheckBox</tabstop>
+ <tabstop>toolbarMovableCheckBox</tabstop>
+ <tabstop>minimizeOnCloseCheckBox</tabstop>
+ <tabstop>systrayShowCheckBox</tabstop>
+ <tabstop>trayIconAppearance</tabstop>
+ <tabstop>systrayMinimizeToTrayCheckBox</tabstop>
+ <tabstop>resetSettingsButton</tabstop>
+ <tabstop>autoTypeEntryTitleMatchCheckBox</tabstop>
+ <tabstop>autoTypeEntryURLMatchCheckBox</tabstop>
+ <tabstop>autoTypeAskCheckBox</tabstop>
+ <tabstop>autoTypeShortcutWidget</tabstop>
+ <tabstop>autoTypeStartDelaySpinBox</tabstop>
+ <tabstop>autoTypeDelaySpinBox</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui
index 2310bd07d..bd4af19fa 100644
--- a/src/gui/ApplicationSettingsWidgetSecurity.ui
+++ b/src/gui/ApplicationSettingsWidgetSecurity.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>595</width>
- <height>541</height>
+ <height>567</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -223,23 +223,23 @@
</widget>
</item>
<item>
- <widget class="QCheckBox" name="passwordRepeatCheckBox">
+ <widget class="QCheckBox" name="passwordsRepeatVisibleCheckBox">
<property name="text">
- <string>Don't require password repeat when it is visible</string>
+ <string>Require password repeat when it is visible</string>
</property>
</widget>
</item>
<item>
- <widget class="QCheckBox" name="passwordCleartextCheckBox">
+ <widget class="QCheckBox" name="passwordsHiddenCheckBox">
<property name="text">
- <string>Don't hide passwords when editing them</string>
+ <string>Hide passwords when editing them</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="passwordShowDotsCheckBox">
<property name="text">
- <string>Don't use placeholder for empty password fields</string>
+ <string>Use placeholder for empty password fields</string>
</property>
</widget>
</item>
@@ -297,13 +297,18 @@
<tabstops>
<tabstop>clearClipboardCheckBox</tabstop>
<tabstop>clearClipboardSpinBox</tabstop>
+ <tabstop>lockDatabaseIdleCheckBox</tabstop>
+ <tabstop>lockDatabaseIdleSpinBox</tabstop>
+ <tabstop>clearSearchCheckBox</tabstop>
+ <tabstop>clearSearchSpinBox</tabstop>
+ <tabstop>touchIDResetCheckBox</tabstop>
<tabstop>touchIDResetSpinBox</tabstop>
<tabstop>lockDatabaseOnScreenLockCheckBox</tabstop>
<tabstop>touchIDResetOnScreenLockCheckBox</tabstop>
<tabstop>lockDatabaseMinimizeCheckBox</tabstop>
<tabstop>relockDatabaseAutoTypeCheckBox</tabstop>
- <tabstop>passwordRepeatCheckBox</tabstop>
- <tabstop>passwordCleartextCheckBox</tabstop>
+ <tabstop>passwordsRepeatVisibleCheckBox</tabstop>
+ <tabstop>passwordsHiddenCheckBox</tabstop>
<tabstop>passwordShowDotsCheckBox</tabstop>
<tabstop>passwordPreviewCleartextCheckBox</tabstop>
<tabstop>hideNotesCheckBox</tabstop>
diff --git a/src/gui/CategoryListWidget.cpp b/src/gui/CategoryListWidget.cpp
index c57b19bc0..d0c60dc52 100644
--- a/src/gui/CategoryListWidget.cpp
+++ b/src/gui/CategoryListWidget.cpp
@@ -20,6 +20,7 @@
#include <QListWidget>
#include <QPainter>
+#include <QProxyStyle>
#include <QScrollBar>
#include <QSize>
#include <QStyledItemDelegate>
@@ -158,9 +159,7 @@ CategoryListWidgetDelegate::CategoryListWidgetDelegate(QListWidget* parent)
}
}
-#ifdef Q_OS_WIN
-#include <QProxyStyle>
-class WindowsCorrectedStyle : public QProxyStyle
+class IconSelectionCorrectedStyle : public QProxyStyle
{
public:
void drawPrimitive(PrimitiveElement element,
@@ -169,24 +168,44 @@ public:
const QWidget* widget) const override
{
painter->save();
-
- if (PE_PanelItemViewItem == element) {
- // Qt on Windows draws selection backgrounds only for the actual text/icon
- // bounding box, not over the full width of a list item.
- // We therefore need to translate and stretch the painter before we can
- // tell Qt to draw its native styles.
- // Since we are scaling horizontally, we also need to move the right and left
- // edge pixels outside the drawing area to avoid thick border lines.
- QRect itemRect = subElementRect(QStyle::SE_ItemViewItemFocusRect, option, widget).adjusted(1, 0, 1, 0);
- painter->scale(static_cast<float>(option->rect.width()) / itemRect.width(), 1.0);
- painter->translate(option->rect.left() - itemRect.left() + 1, 0);
+ if (widget && PE_PanelItemViewItem == element) {
+ // Qt on Windows and the Fusion/Phantom base styles draw selection backgrounds only for
+ // the actual text/icon bounding box, not over the full width of a list item.
+ // State_On is relevant only for the Windows hack below.
+ if (option->state & State_HasFocus || option->state & State_On) {
+ painter->fillRect(option->rect, widget->palette().color(QPalette::Active, QPalette::Highlight));
+ } else if (option->state & State_Selected) {
+ painter->fillRect(option->rect, widget->palette().color(QPalette::Inactive, QPalette::Highlight));
+ }
+ } else if (PE_FrameFocusRect == element) {
+ // don't draw the native focus rect
+ } else {
+ QProxyStyle::drawPrimitive(element, option, painter, widget);
}
- QProxyStyle::drawPrimitive(element, option, painter, widget);
painter->restore();
}
-};
+
+#ifdef Q_OS_WIN
+ void drawControl(ControlElement element,
+ const QStyleOption* option,
+ QPainter* painter,
+ const QWidget* widget) const override
+ {
+ // Qt on Windows swallows State_HasFocus somewhere in its intestines,
+ // so we abuse State_On here to indicate the selection focus and
+ // hack into the text colour palette. Forgive me.
+ if (QStyle::CE_ItemViewItem == element && option->state & State_HasFocus) {
+ QStyleOptionViewItem newOpt(*qstyleoption_cast<const QStyleOptionViewItem*>(option));
+ newOpt.state |= State_On;
+ newOpt.palette.setColor(QPalette::All, QPalette::Text, widget->palette().color(QPalette::HighlightedText));
+ QProxyStyle::drawControl(element, &newOpt, painter, widget);
+ return;
+ }
+ QProxyStyle::drawControl(element, option, painter, widget);
+ }
#endif
+};
void CategoryListWidgetDelegate::paint(QPainter* painter,
const QStyleOptionViewItem& option,
@@ -203,12 +222,7 @@ void CategoryListWidgetDelegate::paint(QPainter* painter,
opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignVCenter;
opt.decorationPosition = QStyleOptionViewItem::Top;
-#ifdef Q_OS_WIN
- QScopedPointer<QStyle> style(new WindowsCorrectedStyle());
-#else
- QStyle* style = opt.widget ? opt.widget->style() : QApplication::style();
-#endif
-
+ QScopedPointer<QStyle> style(new IconSelectionCorrectedStyle());
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
QRect fontRect = painter->fontMetrics().boundingRect(
@@ -216,7 +230,16 @@ void CategoryListWidgetDelegate::paint(QPainter* painter,
int paddingTop = fontRect.height() < 30 ? 15 : 10;
int left = opt.rect.left() + opt.rect.width() / 2 - iconSize.width() / 2;
- painter->drawPixmap(left, opt.rect.top() + paddingTop, icon.pixmap(iconSize));
+
+ auto mode = QIcon::Normal;
+ if ((opt.state & QStyle::State_Enabled) == 0) {
+ mode = QIcon::Disabled;
+ } else if (opt.state & QStyle::State_HasFocus) {
+ mode = QIcon::Selected;
+ } else if (opt.state & QStyle::State_Active) {
+ mode = QIcon::Active;
+ }
+ painter->drawPixmap(left, opt.rect.top() + paddingTop, icon.pixmap(iconSize, mode));
painter->restore();
}
@@ -238,7 +261,6 @@ int CategoryListWidgetDelegate::minWidth() const
// add some padding
maxWidth += 10;
-
return maxWidth < m_size.height() ? m_size.height() : maxWidth;
}
@@ -252,5 +274,5 @@ QSize CategoryListWidgetDelegate::sizeHint(const QStyleOptionViewItem& option, c
w = m_listWidget->width();
}
- return QSize(w, m_size.height());
+ return {w, m_size.height()};
}
diff --git a/src/gui/CategoryListWidget.ui b/src/gui/CategoryListWidget.ui
index f16165cdb..f21f47187 100644
--- a/src/gui/CategoryListWidget.ui
+++ b/src/gui/CategoryListWidget.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>182</width>
+ <width>256</width>
<height>418</height>
</rect>
</property>
@@ -113,8 +113,8 @@
</layout>
</widget>
<tabstops>
- <tabstop>categoryList</tabstop>
<tabstop>scrollUp</tabstop>
+ <tabstop>categoryList</tabstop>
<tabstop>scrollDown</tabstop>
</tabstops>
<resources/>
diff --git a/src/gui/Clipboard.cpp b/src/gui/Clipboard.cpp
index 10f959531..ae5d8290f 100644
--- a/src/gui/Clipboard.cpp
+++ b/src/gui/Clipboard.cpp
@@ -67,8 +67,8 @@ void Clipboard::setText(const QString& text, bool clear)
}
#endif
- if (clear && config()->get("security/clearclipboard").toBool()) {
- int timeout = config()->get("security/clearclipboardtimeout").toInt();
+ if (clear && config()->get(Config::Security_ClearClipboard).toBool()) {
+ int timeout = config()->get(Config::Security_ClearClipboardTimeout).toInt();
if (timeout > 0) {
m_lastCopied = text;
m_timer->start(timeout * 1000);
diff --git a/src/gui/CloneDialog.cpp b/src/gui/CloneDialog.cpp
index e91df62c7..2441b3f17 100644
--- a/src/gui/CloneDialog.cpp
+++ b/src/gui/CloneDialog.cpp
@@ -21,7 +21,7 @@
#include "config-keepassx.h"
#include "core/Database.h"
#include "core/Entry.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "crypto/Crypto.h"
#include "gui/DatabaseWidget.h"
diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp
index 620d3652b..5e6e41b7a 100644
--- a/src/gui/DatabaseOpenDialog.cpp
+++ b/src/gui/DatabaseOpenDialog.cpp
@@ -31,7 +31,10 @@ DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent)
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::ForeignWindow);
#endif
connect(m_view, SIGNAL(dialogFinished(bool)), this, SLOT(complete(bool)));
- setLayout(m_view->layout());
+ auto* layout = new QVBoxLayout();
+ layout->setMargin(0);
+ setLayout(layout);
+ layout->addWidget(m_view);
setMinimumWidth(700);
}
diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp
index c610a773d..4dbe9dc9d 100644
--- a/src/gui/DatabaseOpenWidget.cpp
+++ b/src/gui/DatabaseOpenWidget.cpp
@@ -21,7 +21,7 @@
#include "core/Config.h"
#include "core/Database.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "crypto/Random.h"
#include "format/KeePass2Reader.h"
#include "gui/FileDialog.h"
@@ -37,7 +37,11 @@
#include <QDesktopServices>
#include <QFont>
#include <QSharedPointer>
-#include <QtConcurrentRun>
+
+namespace
+{
+ constexpr int clearFormsDelay = 30000;
+}
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
: DialogyWidget(parent)
@@ -48,52 +52,60 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->messageWidget->setHidden(true);
+ m_hideTimer.setInterval(clearFormsDelay);
+ m_hideTimer.setSingleShot(true);
+ connect(&m_hideTimer, &QTimer::timeout, this, [this] {
+ // Reset the password field after being hidden for a set time
+ m_ui->editPassword->setText("");
+ m_ui->editPassword->setShowPassword(false);
+ });
+
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
m_ui->labelHeadline->setFont(font);
m_ui->labelHeadline->setText(tr("Unlock KeePassXC Database"));
- m_ui->comboKeyFile->lineEdit()->addAction(m_ui->keyFileClearIcon, QLineEdit::TrailingPosition);
-
- m_ui->buttonTogglePassword->setIcon(filePath()->onOffIcon("actions", "password-show"));
- connect(m_ui->buttonTogglePassword, SIGNAL(toggled(bool)), m_ui->editPassword, SLOT(setShowPassword(bool)));
- connect(m_ui->buttonTogglePassword, SIGNAL(toggled(bool)), m_ui->editPassword, SLOT(setFocus()));
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
- m_ui->hardwareKeyLabelHelp->setIcon(filePath()->icon("actions", "system-help").pixmap(QSize(12, 12)));
+ m_ui->hardwareKeyLabelHelp->setIcon(resources()->icon("system-help").pixmap(QSize(12, 12)));
connect(m_ui->hardwareKeyLabelHelp, SIGNAL(clicked(bool)), SLOT(openHardwareKeyHelp()));
- m_ui->keyFileLabelHelp->setIcon(filePath()->icon("actions", "system-help").pixmap(QSize(12, 12)));
+ m_ui->keyFileLabelHelp->setIcon(resources()->icon("system-help").pixmap(QSize(12, 12)));
connect(m_ui->keyFileLabelHelp, SIGNAL(clicked(bool)), SLOT(openKeyFileHelp()));
- connect(m_ui->comboKeyFile->lineEdit(), SIGNAL(textChanged(QString)), SLOT(handleKeyFileComboEdited()));
- connect(m_ui->comboKeyFile, SIGNAL(currentIndexChanged(int)), SLOT(handleKeyFileComboChanged()));
- m_ui->keyFileClearIcon->setIcon(filePath()->icon("actions", "edit-clear-locationbar-rtl"));
+ connect(m_ui->keyFileLineEdit, SIGNAL(textChanged(QString)), SLOT(keyFileTextChanged()));
+ m_ui->keyFileLineEdit->addAction(m_ui->keyFileClearIcon, QLineEdit::TrailingPosition);
+ m_ui->keyFileClearIcon->setIcon(resources()->icon("edit-clear-locationbar-rtl"));
m_ui->keyFileClearIcon->setVisible(false);
- connect(m_ui->keyFileClearIcon, SIGNAL(triggered(bool)), SLOT(clearKeyFileEdit()));
+ connect(m_ui->keyFileClearIcon, SIGNAL(triggered(bool)), SLOT(clearKeyFileText()));
#ifdef WITH_XC_YUBIKEY
- m_ui->yubikeyProgress->setVisible(false);
- QSizePolicy sp = m_ui->yubikeyProgress->sizePolicy();
+ m_ui->hardwareKeyProgress->setVisible(false);
+ QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true);
- m_ui->yubikeyProgress->setSizePolicy(sp);
+ m_ui->hardwareKeyProgress->setSizePolicy(sp);
- connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
+ connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollHardwareKey()));
+ connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
+
+ connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
+ // Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog)
+ if (window() != getMainWindow()) {
+ m_ui->messageWidget->showMessage(tr("Please touch the button on your YubiKey!"),
+ MessageWidget::Information,
+ MessageWidget::DisableAutoHide);
+ }
+ });
+ connect(YubiKey::instance(), &YubiKey::challengeCompleted, this, [this] { m_ui->messageWidget->hide(); });
#else
m_ui->hardwareKeyLabel->setVisible(false);
m_ui->hardwareKeyLabelHelp->setVisible(false);
m_ui->buttonRedetectYubikey->setVisible(false);
- m_ui->comboChallengeResponse->setVisible(false);
- m_ui->yubikeyProgress->setVisible(false);
-#endif
-
-#ifdef Q_OS_MACOS
- // add random padding to layouts to align widgets properly
- m_ui->dialogButtonsLayout->setContentsMargins(10, 0, 15, 0);
- m_ui->gridLayout->setContentsMargins(10, 0, 0, 0);
+ m_ui->challengeResponseCombo->setVisible(false);
+ m_ui->hardwareKeyProgress->setVisible(false);
#endif
#ifndef WITH_XC_TOUCHID
@@ -113,68 +125,56 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event)
{
DialogyWidget::showEvent(event);
m_ui->editPassword->setFocus();
-
-#ifdef WITH_XC_YUBIKEY
- // showEvent() may be called twice, so make sure we are only polling once
- if (!m_yubiKeyBeingPolled) {
- // clang-format off
- connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection);
- connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection);
- connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection);
- // clang-format on
-
- pollYubikey();
- m_yubiKeyBeingPolled = true;
- }
-#endif
+ m_hideTimer.stop();
}
void DatabaseOpenWidget::hideEvent(QHideEvent* event)
{
DialogyWidget::hideEvent(event);
-#ifdef WITH_XC_YUBIKEY
- // Don't listen to any Yubikey events if we are hidden
- disconnect(YubiKey::instance(), nullptr, this, nullptr);
- m_yubiKeyBeingPolled = false;
-#endif
-
- if (isVisible()) {
- return;
+ // Schedule form clearing if we are hidden
+ if (!isVisible()) {
+ m_hideTimer.start();
}
-
- clearForms();
}
void DatabaseOpenWidget::load(const QString& filename)
{
+ clearForms();
+
m_filename = filename;
m_ui->fileNameLabel->setRawText(m_filename);
- m_ui->comboKeyFile->addItem(tr("Select key file..."), -1);
- m_ui->comboKeyFile->setCurrentIndex(0);
m_ui->keyFileClearIcon->setVisible(false);
- m_keyFileComboEdited = false;
- if (config()->get("RememberLastKeyFiles").toBool()) {
- QHash<QString, QVariant> lastKeyFiles = config()->get("LastKeyFiles").toHash();
+ if (config()->get(Config::RememberLastKeyFiles).toBool()) {
+ auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
if (lastKeyFiles.contains(m_filename)) {
- m_ui->comboKeyFile->addItem(lastKeyFiles[m_filename].toString());
- m_ui->comboKeyFile->setCurrentIndex(1);
+ m_ui->keyFileLineEdit->setText(lastKeyFiles[m_filename].toString());
}
}
- QHash<QString, QVariant> useTouchID = config()->get("UseTouchID").toHash();
+ QHash<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();
m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
+
+#ifdef WITH_XC_YUBIKEY
+ // Only auto-poll for hardware keys if we previously used one with this database file
+ if (config()->get(Config::RememberLastKeyFiles).toBool()) {
+ auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
+ if (lastChallengeResponse.contains(m_filename)) {
+ pollHardwareKey();
+ }
+ }
+#endif
}
void DatabaseOpenWidget::clearForms()
{
m_ui->editPassword->setText("");
- m_ui->comboKeyFile->clear();
- m_ui->comboKeyFile->setEditText("");
+ m_ui->editPassword->setShowPassword(false);
+ m_ui->keyFileLineEdit->clear();
m_ui->checkTouchID->setChecked(false);
- m_ui->buttonTogglePassword->setChecked(false);
+ m_ui->challengeResponseCombo->clear();
m_db.reset();
}
@@ -183,23 +183,28 @@ QSharedPointer<Database> DatabaseOpenWidget::database()
return m_db;
}
+QString DatabaseOpenWidget::filename()
+{
+ return m_filename;
+}
+
void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
{
m_ui->editPassword->setText(pw);
- m_ui->comboKeyFile->setCurrentIndex(-1);
- m_ui->comboKeyFile->setEditText(keyFile);
+ m_ui->keyFileLineEdit->setText(keyFile);
openDatabase();
}
void DatabaseOpenWidget::openDatabase()
{
- QSharedPointer<CompositeKey> masterKey = databaseKey();
- if (!masterKey) {
+ m_ui->messageWidget->hide();
+
+ QSharedPointer<CompositeKey> databaseKey = buildDatabaseKey();
+ if (!databaseKey) {
return;
}
m_ui->editPassword->setShowPassword(false);
- m_ui->buttonTogglePassword->setChecked(false);
QCoreApplication::processEvents();
m_db.reset(new Database());
@@ -208,11 +213,31 @@ void DatabaseOpenWidget::openDatabase()
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_ui->passwordFormFrame->setEnabled(false);
QCoreApplication::processEvents();
- bool ok = m_db->open(m_filename, masterKey, &error, false);
+ bool ok = m_db->open(m_filename, databaseKey, &error, false);
QApplication::restoreOverrideCursor();
m_ui->passwordFormFrame->setEnabled(true);
- if (!ok) {
+ if (ok) {
+#ifdef WITH_XC_TOUCHID
+ QHash<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();
+
+ // check if TouchID can & should be used to unlock the database next time
+ if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()) {
+ // encrypt and store key blob
+ if (TouchID::getInstance().storeKey(m_filename, PasswordKey(m_ui->editPassword->text()).rawKey())) {
+ useTouchID.insert(m_filename, true);
+ }
+ } else {
+ // when TouchID not available or unchecked, reset for the current database
+ TouchID::getInstance().reset(m_filename);
+ useTouchID.insert(m_filename, false);
+ }
+
+ config()->set(Config::UseTouchID, useTouchID);
+#endif
+ emit dialogFinished(true);
+ clearForms();
+ } else {
if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) {
QScopedPointer<QMessageBox> msgBox(new QMessageBox(this));
msgBox->setIcon(QMessageBox::Critical);
@@ -232,40 +257,12 @@ void DatabaseOpenWidget::openDatabase()
return;
}
}
+
m_retryUnlockWithEmptyPassword = false;
m_ui->messageWidget->showMessage(error, MessageWidget::MessageType::Error);
// Focus on the password field and select the input for easy retry
m_ui->editPassword->selectAll();
m_ui->editPassword->setFocus();
- return;
- }
-
- if (m_db) {
-#ifdef WITH_XC_TOUCHID
- QHash<QString, QVariant> useTouchID = config()->get("UseTouchID").toHash();
-
- // check if TouchID can & should be used to unlock the database next time
- if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()) {
- // encrypt and store key blob
- if (TouchID::getInstance().storeKey(m_filename, PasswordKey(m_ui->editPassword->text()).rawKey())) {
- useTouchID.insert(m_filename, true);
- }
- } else {
- // when TouchID not available or unchecked, reset for the current database
- TouchID::getInstance().reset(m_filename);
- useTouchID.insert(m_filename, false);
- }
-
- config()->set("UseTouchID", useTouchID);
-#endif
-
- if (m_ui->messageWidget->isVisible()) {
- m_ui->messageWidget->animatedHide();
- }
- emit dialogFinished(true);
- } else {
- m_ui->messageWidget->showMessage(error, MessageWidget::Error);
- m_ui->editPassword->setText("");
#ifdef WITH_XC_TOUCHID
// unable to unlock database, reset TouchID for the current database
@@ -274,12 +271,12 @@ void DatabaseOpenWidget::openDatabase()
}
}
-QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
+QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{
- auto masterKey = QSharedPointer<CompositeKey>::create();
+ auto databaseKey = QSharedPointer<CompositeKey>::create();
if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) {
- masterKey->addKey(QSharedPointer<PasswordKey>::create(m_ui->editPassword->text()));
+ databaseKey->addKey(QSharedPointer<PasswordKey>::create(m_ui->editPassword->text()));
}
#ifdef WITH_XC_TOUCHID
@@ -287,7 +284,7 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()
&& m_ui->editPassword->text().isEmpty()) {
// clear empty password from composite key
- masterKey->clear();
+ databaseKey->clear();
// try to get, decrypt and use PasswordKey
QSharedPointer<QByteArray> passwordKey = TouchID::getInstance().getKey(m_filename);
@@ -296,23 +293,23 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
if (passwordKey.isNull())
return QSharedPointer<CompositeKey>();
- masterKey->addKey(PasswordKey::fromRawKey(*passwordKey));
+ databaseKey->addKey(PasswordKey::fromRawKey(*passwordKey));
}
}
#endif
- QHash<QString, QVariant> lastKeyFiles = config()->get("LastKeyFiles").toHash();
+ auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
lastKeyFiles.remove(m_filename);
auto key = QSharedPointer<FileKey>::create();
- QString keyFilename = m_ui->comboKeyFile->currentText();
- if (!m_ui->comboKeyFile->currentText().isEmpty() && m_keyFileComboEdited) {
+ QString keyFilename = m_ui->keyFileLineEdit->text();
+ if (!keyFilename.isEmpty()) {
QString errorMsg;
if (!key->load(keyFilename, &errorMsg)) {
m_ui->messageWidget->showMessage(tr("Failed to open key file: %1").arg(errorMsg), MessageWidget::Error);
return {};
}
- if (key->type() != FileKey::Hashed && !config()->get("Messages/NoLegacyKeyFileWarning").toBool()) {
+ if (key->type() != FileKey::Hashed && !config()->get(Config::Messages_NoLegacyKeyFileWarning).toBool()) {
QMessageBox legacyWarning;
legacyWarning.setWindowTitle(tr("Legacy key file format"));
legacyWarning.setText(tr("You are using a legacy key file format which may become\n"
@@ -323,42 +320,40 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
legacyWarning.setDefaultButton(QMessageBox::Ok);
legacyWarning.setCheckBox(new QCheckBox(tr("Don't show this warning again")));
- connect(legacyWarning.checkBox(), &QCheckBox::stateChanged, [](int state) {
- config()->set("Messages/NoLegacyKeyFileWarning", state == Qt::CheckState::Checked);
+ connect(legacyWarning.checkBox(), &QCheckBox::stateChanged, this, [](int state) {
+ config()->set(Config::Messages_NoLegacyKeyFileWarning, state == Qt::CheckState::Checked);
});
legacyWarning.exec();
}
- masterKey->addKey(key);
- lastKeyFiles[m_filename] = keyFilename;
+ databaseKey->addKey(key);
+ lastKeyFiles.insert(m_filename, keyFilename);
}
- if (config()->get("RememberLastKeyFiles").toBool()) {
- config()->set("LastKeyFiles", lastKeyFiles);
+ if (config()->get(Config::RememberLastKeyFiles).toBool()) {
+ config()->set(Config::LastKeyFiles, lastKeyFiles);
}
#ifdef WITH_XC_YUBIKEY
- QHash<QString, QVariant> lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
+ auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
lastChallengeResponse.remove(m_filename);
- int selectionIndex = m_ui->comboChallengeResponse->currentIndex();
+ int selectionIndex = m_ui->challengeResponseCombo->currentIndex();
if (selectionIndex > 0) {
- int comboPayload = m_ui->comboChallengeResponse->itemData(selectionIndex).toInt();
-
- // read blocking mode from LSB and slot index number from second LSB
- bool blocking = comboPayload & 1;
- int slot = comboPayload >> 1;
- auto crKey = QSharedPointer<YkChallengeResponseKey>(new YkChallengeResponseKey(slot, blocking));
- masterKey->addChallengeResponseKey(crKey);
- lastChallengeResponse[m_filename] = true;
+ auto slot = m_ui->challengeResponseCombo->itemData(selectionIndex).value<YubiKeySlot>();
+ auto crKey = QSharedPointer<YkChallengeResponseKey>(new YkChallengeResponseKey(slot));
+ databaseKey->addChallengeResponseKey(crKey);
+
+ // Qt doesn't read custom types in settings so stuff into a QString
+ lastChallengeResponse.insert(m_filename, QStringLiteral("%1:%2").arg(slot.first).arg(slot.second));
}
- if (config()->get("RememberLastKeyFiles").toBool()) {
- config()->set("LastChallengeResponse", lastChallengeResponse);
+ if (config()->get(Config::RememberLastKeyFiles).toBool()) {
+ config()->set(Config::LastChallengeResponse, lastChallengeResponse);
}
#endif
- return masterKey;
+ return databaseKey;
}
void DatabaseOpenWidget::reject()
@@ -369,7 +364,7 @@ void DatabaseOpenWidget::reject()
void DatabaseOpenWidget::browseKeyFile()
{
QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files"));
- if (!config()->get("RememberLastKeyFiles").toBool()) {
+ if (!config()->get(Config::RememberLastKeyFiles).toBool()) {
fileDialog()->setNextForgetDialog();
}
QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters);
@@ -384,69 +379,76 @@ void DatabaseOpenWidget::browseKeyFile()
}
if (!filename.isEmpty()) {
- m_ui->comboKeyFile->setCurrentIndex(-1);
- m_ui->comboKeyFile->setEditText(filename);
+ m_ui->keyFileLineEdit->setText(filename);
}
}
-void DatabaseOpenWidget::clearKeyFileEdit()
+void DatabaseOpenWidget::clearKeyFileText()
{
- m_ui->comboKeyFile->setCurrentIndex(0);
- // make sure that handler is called even if 0 was the current index already
- handleKeyFileComboChanged();
+ m_ui->keyFileLineEdit->clear();
}
-void DatabaseOpenWidget::handleKeyFileComboEdited()
+void DatabaseOpenWidget::keyFileTextChanged()
{
- m_keyFileComboEdited = true;
- m_ui->keyFileClearIcon->setVisible(true);
+ m_ui->keyFileClearIcon->setVisible(!m_ui->keyFileLineEdit->text().isEmpty());
}
-void DatabaseOpenWidget::handleKeyFileComboChanged()
+void DatabaseOpenWidget::pollHardwareKey()
{
- m_keyFileComboEdited = m_ui->comboKeyFile->currentIndex() != 0;
- m_ui->keyFileClearIcon->setVisible(m_keyFileComboEdited);
-}
+ if (m_pollingHardwareKey) {
+ return;
+ }
+
+ m_ui->challengeResponseCombo->clear();
+ m_ui->challengeResponseCombo->addItem(tr("Detecting hardware keys…"));
-void DatabaseOpenWidget::pollYubikey()
-{
m_ui->buttonRedetectYubikey->setEnabled(false);
- m_ui->comboChallengeResponse->setEnabled(false);
- m_ui->comboChallengeResponse->clear();
- m_ui->comboChallengeResponse->addItem(tr("Select slot..."), -1);
- m_ui->yubikeyProgress->setVisible(true);
+ m_ui->challengeResponseCombo->setEnabled(false);
+ m_ui->hardwareKeyProgress->setVisible(true);
+ m_pollingHardwareKey = true;
- // YubiKey init is slow, detect asynchronously to not block the UI
- QtConcurrent::run(YubiKey::instance(), &YubiKey::detect);
+ YubiKey::instance()->findValidKeys();
}
-void DatabaseOpenWidget::yubikeyDetected(int slot, bool blocking)
+void DatabaseOpenWidget::hardwareKeyResponse(bool found)
{
- YkChallengeResponseKey yk(slot, blocking);
- // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
- m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1) | blocking));
+ m_ui->challengeResponseCombo->clear();
+ m_ui->buttonRedetectYubikey->setEnabled(true);
+ m_ui->hardwareKeyProgress->setVisible(false);
+ m_pollingHardwareKey = false;
- if (config()->get("RememberLastKeyFiles").toBool()) {
- QHash<QString, QVariant> lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
+ if (!found) {
+ m_ui->challengeResponseCombo->addItem(tr("No hardware keys detected"));
+ m_ui->challengeResponseCombo->setEnabled(false);
+ return;
+ } else {
+ m_ui->challengeResponseCombo->addItem(tr("Select hardware key…"));
+ }
+
+ YubiKeySlot lastUsedSlot;
+ if (config()->get(Config::RememberLastKeyFiles).toBool()) {
+ auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
if (lastChallengeResponse.contains(m_filename)) {
- m_ui->comboChallengeResponse->setCurrentIndex(1);
+ // Qt doesn't read custom types in settings so extract from QString
+ auto split = lastChallengeResponse.value(m_filename).toString().split(":");
+ if (split.size() > 1) {
+ lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt());
+ }
}
}
-}
-void DatabaseOpenWidget::yubikeyDetectComplete()
-{
- m_ui->comboChallengeResponse->setEnabled(true);
- m_ui->buttonRedetectYubikey->setEnabled(true);
- m_ui->yubikeyProgress->setVisible(false);
- m_yubiKeyBeingPolled = false;
-}
+ int selectedIndex = 0;
+ for (auto& slot : YubiKey::instance()->foundKeys()) {
+ // add detected YubiKey to combo box
+ m_ui->challengeResponseCombo->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot));
+ // Select this YubiKey + Slot if we used it in the past
+ if (lastUsedSlot == slot) {
+ selectedIndex = m_ui->challengeResponseCombo->count() - 1;
+ }
+ }
-void DatabaseOpenWidget::noYubikeyFound()
-{
- m_ui->buttonRedetectYubikey->setEnabled(true);
- m_ui->yubikeyProgress->setVisible(false);
- m_yubiKeyBeingPolled = false;
+ m_ui->challengeResponseCombo->setCurrentIndex(selectedIndex);
+ m_ui->challengeResponseCombo->setEnabled(true);
}
void DatabaseOpenWidget::openHardwareKeyHelp()
diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h
index aa0a4315b..6d2b688ca 100644
--- a/src/gui/DatabaseOpenWidget.h
+++ b/src/gui/DatabaseOpenWidget.h
@@ -20,6 +20,7 @@
#define KEEPASSX_DATABASEOPENWIDGET_H
#include <QScopedPointer>
+#include <QTimer>
#include "gui/DialogyWidget.h"
#include "keys/CompositeKey.h"
@@ -40,20 +41,23 @@ public:
explicit DatabaseOpenWidget(QWidget* parent = nullptr);
~DatabaseOpenWidget();
void load(const QString& filename);
+ QString filename();
void clearForms();
void enterKey(const QString& pw, const QString& keyFile);
QSharedPointer<Database> database();
-public slots:
- void pollYubikey();
-
signals:
void dialogFinished(bool accepted);
protected:
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
- QSharedPointer<CompositeKey> databaseKey();
+ QSharedPointer<CompositeKey> buildDatabaseKey();
+
+ const QScopedPointer<Ui::DatabaseOpenWidget> m_ui;
+ QSharedPointer<Database> m_db;
+ QString m_filename;
+ bool m_retryUnlockWithEmptyPassword = false;
protected slots:
virtual void openDatabase();
@@ -61,24 +65,17 @@ protected slots:
private slots:
void browseKeyFile();
- void clearKeyFileEdit();
- void handleKeyFileComboEdited();
- void handleKeyFileComboChanged();
- void yubikeyDetected(int slot, bool blocking);
- void yubikeyDetectComplete();
- void noYubikeyFound();
+ void clearKeyFileText();
+ void keyFileTextChanged();
+ void pollHardwareKey();
+ void hardwareKeyResponse(bool found);
void openHardwareKeyHelp();
void openKeyFileHelp();
-protected:
- const QScopedPointer<Ui::DatabaseOpenWidget> m_ui;
- QSharedPointer<Database> m_db;
- QString m_filename;
- bool m_retryUnlockWithEmptyPassword = false;
-
private:
- bool m_yubiKeyBeingPolled = false;
- bool m_keyFileComboEdited = false;
+ bool m_pollingHardwareKey = false;
+ QTimer m_hideTimer;
+
Q_DISABLE_COPY(DatabaseOpenWidget)
};
diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui
index 60b2feadc..a7510baba 100644
--- a/src/gui/DatabaseOpenWidget.ui
+++ b/src/gui/DatabaseOpenWidget.ui
@@ -2,6 +2,14 @@
<ui version="4.0">
<class>DatabaseOpenWidget</class>
<widget class="QWidget" name="DatabaseOpenWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>588</width>
+ <height>448</height>
+ </rect>
+ </property>
<property name="accessibleName">
<string>Unlock KeePassXC Database</string>
</property>
@@ -99,7 +107,7 @@
</spacer>
</item>
<item>
- <widget class="QFrame" name="horizontalFrame">
+ <widget class="QFrame" name="loginFrame">
<property name="minimumSize">
<size>
<width>550</width>
@@ -157,31 +165,14 @@
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="passwordLayout">
- <item>
- <widget class="PasswordEdit" name="editPassword">
- <property name="accessibleName">
- <string>Password field</string>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="buttonTogglePassword">
- <property name="toolTip">
- <string>Toggle password visibility</string>
- </property>
- <property name="accessibleName">
- <string>Toggle password visibility</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="PasswordEdit" name="editPassword">
+ <property name="accessibleName">
+ <string>Password field</string>
+ </property>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
</item>
<item>
<spacer name="verticalSpacer_4">
@@ -235,13 +226,64 @@
<property name="topMargin">
<number>3</number>
</property>
+ <item row="0" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <property name="spacing">
+ <number>5</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="keyFileLabel">
+ <property name="text">
+ <string>Key File:</string>
+ </property>
+ <property name="buddy">
+ <cstring>keyFileLineEdit</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="keyFileLabelHelp">
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::ClickFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database's security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;br&gt;If you do not have a key file, leave this field empty.&lt;/p&gt;&lt;p&gt;Click for more information...&lt;/p&gt;</string>
+ </property>
+ <property name="accessibleName">
+ <string>Key file help</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QToolButton {
+ border: none;
+ background: none;
+}</string>
+ </property>
+ <property name="text">
+ <string>?</string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>12</width>
+ <height>12</height>
+ </size>
+ </property>
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
<item row="1" column="3">
<layout class="QGridLayout" name="gridLayout">
<property name="spacing">
<number>0</number>
</property>
<item row="1" column="2">
- <widget class="QProgressBar" name="yubikeyProgress">
+ <widget class="QProgressBar" name="hardwareKeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
@@ -263,7 +305,7 @@
</widget>
</item>
<item row="0" column="2">
- <widget class="QComboBox" name="comboChallengeResponse">
+ <widget class="QComboBox" name="challengeResponseCombo">
<property name="enabled">
<bool>false</bool>
</property>
@@ -283,18 +325,80 @@
</item>
</layout>
</item>
- <item row="0" column="4">
- <widget class="QPushButton" name="buttonBrowseFile">
- <property name="toolTip">
- <string>Browse for key file</string>
- </property>
- <property name="accessibleName">
- <string>Browse for key file</string>
- </property>
- <property name="text">
- <string>Browse...</string>
+ <item row="1" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="spacing">
+ <number>2</number>
</property>
- </widget>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <property name="spacing">
+ <number>5</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="hardwareKeyLabel">
+ <property name="text">
+ <string>Hardware Key:</string>
+ </property>
+ <property name="buddy">
+ <cstring>challengeResponseCombo</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="hardwareKeyLabelHelp">
+ <property name="cursor">
+ <cursorShape>PointingHandCursor</cursorShape>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::ClickFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>&lt;p&gt;You can use a hardware security key such as a &lt;strong&gt;YubiKey&lt;/strong&gt; or &lt;strong&gt;OnlyKey&lt;/strong&gt; with slots configured for HMAC-SHA1.&lt;/p&gt;
+&lt;p&gt;Click for more information...&lt;/p&gt;</string>
+ </property>
+ <property name="accessibleName">
+ <string>Hardware key help</string>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QToolButton {
+ border: none;
+ background: none;
+}</string>
+ </property>
+ <property name="text">
+ <string notr="true">?</string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>12</width>
+ <height>12</height>
+ </size>
+ </property>
+ <property name="popupMode">
+ <enum>QToolButton::InstantPopup</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_6">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>2</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
</item>
<item row="0" column="3">
<layout class="QGridLayout" name="gridLayout_2">
@@ -302,10 +406,7 @@
<number>0</number>
</property>
<item row="0" column="1">
- <widget class="QComboBox" name="comboKeyFile">
- <property name="enabled">
- <bool>true</bool>
- </property>
+ <widget class="QLineEdit" name="keyFileLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@@ -313,128 +414,64 @@
</sizepolicy>
</property>
<property name="accessibleName">
- <string>Key file selection</string>
- </property>
- <property name="editable">
- <bool>true</bool>
+ <string>Key file to unlock the database</string>
</property>
</widget>
</item>
</layout>
</item>
- <item row="1" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item row="0" column="4">
+ <widget class="QPushButton" name="buttonBrowseFile">
+ <property name="toolTip">
+ <string>Browse for key file</string>
+ </property>
+ <property name="accessibleName">
+ <string>Browse for key file</string>
+ </property>
+ <property name="text">
+ <string>Browse...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="4">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
- <number>5</number>
+ <number>0</number>
</property>
<item>
- <widget class="QLabel" name="hardwareKeyLabel">
- <property name="text">
- <string>Hardware Key:</string>
- </property>
- <property name="buddy">
- <cstring>comboChallengeResponse</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="hardwareKeyLabelHelp">
- <property name="cursor">
- <cursorShape>PointingHandCursor</cursorShape>
+ <widget class="QPushButton" name="buttonRedetectYubikey">
+ <property name="enabled">
+ <bool>true</bool>
</property>
<property name="toolTip">
- <string>&lt;p&gt;You can use a hardware security key such as a &lt;strong&gt;YubiKey&lt;/strong&gt; or &lt;strong&gt;OnlyKey&lt;/strong&gt; with slots configured for HMAC-SHA1.&lt;/p&gt;
-&lt;p&gt;Click for more information...&lt;/p&gt;</string>
+ <string>Refresh hardware tokens</string>
</property>
<property name="accessibleName">
- <string>Hardware key help</string>
- </property>
- <property name="styleSheet">
- <string notr="true">QToolButton {
- border: none;
- background: none;
-}</string>
+ <string>Refresh hardware tokens</string>
</property>
<property name="text">
- <string notr="true">?</string>
- </property>
- <property name="iconSize">
- <size>
- <width>12</width>
- <height>12</height>
- </size>
- </property>
- <property name="popupMode">
- <enum>QToolButton::InstantPopup</enum>
+ <string>Refresh</string>
</property>
</widget>
</item>
- </layout>
- </item>
- <item row="0" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <property name="spacing">
- <number>5</number>
- </property>
<item>
- <widget class="QLabel" name="keyFileLabel">
- <property name="text">
- <string>Key File:</string>
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
</property>
- <property name="buddy">
- <cstring>comboKeyFile</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="keyFileLabelHelp">
- <property name="cursor">
- <cursorShape>PointingHandCursor</cursorShape>
- </property>
- <property name="toolTip">
- <string>&lt;p&gt;In addition to your master password, you can use a secret file to enhance the security of your database. Such a file can be generated in your database's security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;br&gt;If you do not have a key file, leave the field empty.&lt;/p&gt;&lt;p&gt;Click for more information...&lt;/p&gt;</string>
- </property>
- <property name="accessibleName">
- <string>Key file help</string>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
</property>
- <property name="styleSheet">
- <string notr="true">QToolButton {
- border: none;
- background: none;
-}</string>
- </property>
- <property name="text">
- <string>?</string>
- </property>
- <property name="iconSize">
+ <property name="sizeHint" stdset="0">
<size>
- <width>12</width>
- <height>12</height>
+ <width>0</width>
+ <height>2</height>
</size>
</property>
- <property name="popupMode">
- <enum>QToolButton::InstantPopup</enum>
- </property>
- </widget>
+ </spacer>
</item>
</layout>
</item>
- <item row="1" column="4">
- <widget class="QPushButton" name="buttonRedetectYubikey">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="toolTip">
- <string>Refresh hardware tokens</string>
- </property>
- <property name="accessibleName">
- <string>Refresh hardware tokens</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- </widget>
- </item>
</layout>
</item>
</layout>
@@ -549,11 +586,6 @@
</widget>
<customwidgets>
<customwidget>
- <class>PasswordEdit</class>
- <extends>QLineEdit</extends>
- <header>gui/PasswordEdit.h</header>
- </customwidget>
- <customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
@@ -564,14 +596,17 @@
<extends>QLabel</extends>
<header>gui/widgets/ElidedLabel.h</header>
</customwidget>
+ <customwidget>
+ <class>PasswordEdit</class>
+ <extends>QLineEdit</extends>
+ <header>gui/PasswordEdit.h</header>
+ </customwidget>
</customwidgets>
<tabstops>
<tabstop>editPassword</tabstop>
- <tabstop>buttonTogglePassword</tabstop>
- <tabstop>comboKeyFile</tabstop>
+ <tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop>
- <tabstop>hardwareKeyLabelHelp</tabstop>
- <tabstop>comboChallengeResponse</tabstop>
+ <tabstop>challengeResponseCombo</tabstop>
<tabstop>buttonRedetectYubikey</tabstop>
<tabstop>checkTouchID</tabstop>
</tabstops>
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index c37e6c5ea..34fe4db72 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -41,7 +41,7 @@
#include "gui/entry/EntryView.h"
#include "gui/group/GroupView.h"
#ifdef Q_OS_MACOS
-#include "gui/macutils/MacUtils.h"
+#include "gui/osutils/macutils/MacUtils.h"
#endif
#include "gui/wizard/NewDatabaseWizard.h"
@@ -49,7 +49,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
, m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
, m_dbWidgetPendingLock(nullptr)
- , m_databaseOpenDialog(new DatabaseOpenDialog())
+ , m_databaseOpenDialog(new DatabaseOpenDialog(this))
{
auto* tabBar = new DragTabBar(this);
setTabBar(tabBar);
@@ -80,8 +80,10 @@ void DatabaseTabWidget::toggleTabbar()
{
if (count() > 1) {
tabBar()->show();
+ emit tabVisibilityChanged(true);
} else {
tabBar()->hide();
+ emit tabVisibilityChanged(false);
}
}
@@ -273,7 +275,11 @@ void DatabaseTabWidget::importKeePass1Database()
void DatabaseTabWidget::importOpVaultDatabase()
{
- QString fileName = fileDialog()->getExistingDirectory(this, "Open .opvault database");
+#ifdef Q_MACOS
+ QString fileName = fileDialog()->getOpenFileName(this, tr("Open OPVault"), {}, "OPVault (*.opvault)");
+#else
+ QString fileName = fileDialog()->getExistingDirectory(this, tr("Open OPVault"));
+#endif
if (fileName.isEmpty()) {
return;
@@ -351,13 +357,17 @@ bool DatabaseTabWidget::closeDatabaseTab(DatabaseWidget* dbWidget)
*/
bool DatabaseTabWidget::closeAllDatabaseTabs()
{
- while (count() > 0) {
- if (!closeDatabaseTab(0)) {
- return false;
+ // Attempt to lock all databases first to prevent closing only a portion of tabs
+ if (lockDatabases()) {
+ while (count() > 0) {
+ if (!closeDatabaseTab(0)) {
+ return false;
+ }
}
+ return true;
}
- return true;
+ return false;
}
bool DatabaseTabWidget::saveDatabase(int index)
@@ -383,6 +393,20 @@ bool DatabaseTabWidget::saveDatabaseAs(int index)
return ok;
}
+bool DatabaseTabWidget::saveDatabaseBackup(int index)
+{
+ if (index == -1) {
+ index = currentIndex();
+ }
+
+ auto* dbWidget = databaseWidgetFromIndex(index);
+ bool ok = dbWidget->saveBackup();
+ if (ok) {
+ updateLastDatabases(dbWidget->database()->filePath());
+ }
+ return ok;
+}
+
void DatabaseTabWidget::closeDatabaseFromSender()
{
auto* dbWidget = qobject_cast<DatabaseWidget*>(sender());
@@ -452,12 +476,17 @@ bool DatabaseTabWidget::warnOnExport()
return ans == MessageBox::Yes;
}
-void DatabaseTabWidget::changeMasterKey()
+void DatabaseTabWidget::showDatabaseSecurity()
+{
+ currentDatabaseWidget()->switchToDatabaseSecurity();
+}
+
+void DatabaseTabWidget::showDatabaseReports()
{
- currentDatabaseWidget()->switchToMasterKeyChange();
+ currentDatabaseWidget()->switchToDatabaseReports();
}
-void DatabaseTabWidget::changeDatabaseSettings()
+void DatabaseTabWidget::showDatabaseSettings()
{
currentDatabaseWidget()->switchToDatabaseSettings();
}
@@ -590,15 +619,27 @@ DatabaseWidget* DatabaseTabWidget::currentDatabaseWidget()
return qobject_cast<DatabaseWidget*>(currentWidget());
}
-void DatabaseTabWidget::lockDatabases()
+/**
+ * Attempt to lock all open databases
+ *
+ * @return return true if all databases are locked
+ */
+bool DatabaseTabWidget::lockDatabases()
{
- for (int i = 0, c = count(); i < c; ++i) {
+ int numLocked = 0;
+ int c = count();
+ for (int i = 0; i < c; ++i) {
auto dbWidget = databaseWidgetFromIndex(i);
- if (dbWidget->lock() && dbWidget->database()->filePath().isEmpty()) {
- // If we locked a database without a file close the tab
- closeDatabaseTab(dbWidget);
+ if (dbWidget->lock()) {
+ ++numLocked;
+ if (dbWidget->database()->filePath().isEmpty()) {
+ // If we locked a database without a file close the tab
+ closeDatabaseTab(dbWidget);
+ }
}
}
+
+ return numLocked == c;
}
/**
@@ -645,11 +686,11 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget,
*/
void DatabaseTabWidget::relockPendingDatabase()
{
- if (!m_dbWidgetPendingLock || !config()->get("security/relockautotype").toBool()) {
+ if (!m_dbWidgetPendingLock || !config()->get(Config::Security_RelockAutoType).toBool()) {
return;
}
- if (m_dbWidgetPendingLock->isLocked() || !m_dbWidgetPendingLock->database()->hasKey()) {
+ if (m_dbWidgetPendingLock->isLocked() || !m_dbWidgetPendingLock->database()->isInitialized()) {
m_dbWidgetPendingLock = nullptr;
return;
}
@@ -660,17 +701,17 @@ void DatabaseTabWidget::relockPendingDatabase()
void DatabaseTabWidget::updateLastDatabases(const QString& filename)
{
- if (!config()->get("RememberLastDatabases").toBool()) {
- config()->set("LastDatabases", QVariant());
+ if (!config()->get(Config::RememberLastDatabases).toBool()) {
+ config()->remove(Config::LastDatabases);
} else {
- QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList();
+ QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
lastDatabases.prepend(filename);
lastDatabases.removeDuplicates();
- while (lastDatabases.count() > config()->get("NumberOfRememberedLastDatabases").toInt()) {
+ while (lastDatabases.count() > config()->get(Config::NumberOfRememberedLastDatabases).toInt()) {
lastDatabases.removeLast();
}
- config()->set("LastDatabases", lastDatabases);
+ config()->set(Config::LastDatabases, lastDatabases);
}
}
@@ -709,9 +750,17 @@ void DatabaseTabWidget::performGlobalAutoType()
if (!unlockedDatabases.isEmpty()) {
autoType()->performGlobalAutoType(unlockedDatabases);
} else if (count() > 0) {
- if (config()->get("security/relockautotype").toBool()) {
+ if (config()->get(Config::Security_RelockAutoType).toBool()) {
m_dbWidgetPendingLock = currentDatabaseWidget();
}
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::AutoType);
}
}
+
+void DatabaseTabWidget::performBrowserUnlock()
+{
+ auto dbWidget = currentDatabaseWidget();
+ if (dbWidget && dbWidget->isLocked()) {
+ unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::Browser);
+ }
+}
diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h
index 5c55bc63c..e59681ea7 100644
--- a/src/gui/DatabaseTabWidget.h
+++ b/src/gui/DatabaseTabWidget.h
@@ -68,18 +68,21 @@ public slots:
void importOpVaultDatabase();
bool saveDatabase(int index = -1);
bool saveDatabaseAs(int index = -1);
+ bool saveDatabaseBackup(int index = -1);
void exportToCsv();
void exportToHtml();
- void lockDatabases();
+ bool lockDatabases();
void closeDatabaseFromSender();
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent);
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath);
void relockPendingDatabase();
- void changeMasterKey();
- void changeDatabaseSettings();
+ void showDatabaseSecurity();
+ void showDatabaseReports();
+ void showDatabaseSettings();
void performGlobalAutoType();
+ void performBrowserUnlock();
signals:
void databaseOpened(DatabaseWidget* dbWidget);
@@ -88,6 +91,7 @@ signals:
void databaseLocked(DatabaseWidget* dbWidget);
void activateDatabaseChanged(DatabaseWidget* dbWidget);
void tabNameChanged();
+ void tabVisibilityChanged(bool tabsVisible);
void messageGlobal(const QString&, MessageWidget::MessageType type);
void messageDismissGlobal();
void databaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget);
@@ -104,7 +108,7 @@ private:
QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock;
- QScopedPointer<DatabaseOpenDialog> m_databaseOpenDialog;
+ QPointer<DatabaseOpenDialog> m_databaseOpenDialog;
};
#endif // KEEPASSX_DATABASETABWIDGET_H
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 990cf4e34..61f2b2163 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -25,21 +25,23 @@
#include <QFile>
#include <QHBoxLayout>
#include <QHeaderView>
+#include <QHostInfo>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QProcess>
#include <QSplitter>
+#include <QTextEdit>
#include "autotype/AutoType.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/EntrySearcher.h"
-#include "core/FilePath.h"
#include "core/FileWatcher.h"
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
+#include "core/Resources.h"
#include "core/Tools.h"
#include "format/KeePass2Reader.h"
#include "gui/Clipboard.h"
@@ -49,6 +51,7 @@
#include "gui/EntryPreviewWidget.h"
#include "gui/FileDialog.h"
#include "gui/KeePass1OpenWidget.h"
+#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/OpVaultOpenWidget.h"
#include "gui/TotpDialog.h"
@@ -59,6 +62,7 @@
#include "gui/entry/EntryView.h"
#include "gui/group/EditGroupWidget.h"
#include "gui/group/GroupView.h"
+#include "gui/reports/ReportsDialog.h"
#include "keeshare/KeeShare.h"
#include "touchid/TouchID.h"
@@ -88,6 +92,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
, m_editEntryWidget(new EditEntryWidget(this))
, m_editGroupWidget(new EditGroupWidget(this))
, m_historyEditEntryWidget(new EditEntryWidget(this))
+ , m_reportsDialog(new ReportsDialog(this))
, m_databaseSettingDialog(new DatabaseSettingsDialog(this))
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
@@ -165,6 +170,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_editEntryWidget->setObjectName("editEntryWidget");
m_editGroupWidget->setObjectName("editGroupWidget");
m_csvImportWizard->setObjectName("csvImportWizard");
+ m_reportsDialog->setObjectName("reportsDialog");
m_databaseSettingDialog->setObjectName("databaseSettingsDialog");
m_databaseOpenWidget->setObjectName("databaseOpenWidget");
m_keepass1OpenWidget->setObjectName("keepass1OpenWidget");
@@ -173,6 +179,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
addChildWidget(m_mainWidget);
addChildWidget(m_editEntryWidget);
addChildWidget(m_editGroupWidget);
+ addChildWidget(m_reportsDialog);
addChildWidget(m_databaseSettingDialog);
addChildWidget(m_historyEditEntryWidget);
addChildWidget(m_databaseOpenWidget);
@@ -187,8 +194,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
connect(m_previewView, SIGNAL(errorOccurred(QString)), SLOT(showErrorMessage(QString)));
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
connect(m_entryView, SIGNAL(viewStateChanged()), SIGNAL(entryViewStateChanged()));
- connect(m_groupView, SIGNAL(groupSelectionChanged(Group*)), SLOT(onGroupChanged(Group*)));
- connect(m_groupView, SIGNAL(groupSelectionChanged(Group*)), SIGNAL(groupChanged()));
+ connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
+ connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
+ connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
@@ -196,6 +204,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
connect(m_editEntryWidget, SIGNAL(historyEntryActivated(Entry*)), SLOT(switchToHistoryView(Entry*)));
connect(m_historyEditEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchBackToEntryEdit()));
connect(m_editGroupWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
+ connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
@@ -209,12 +218,12 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
m_blockAutoSave = false;
m_EntrySearcher = new EntrySearcher(false);
- m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool();
+ m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
#ifdef WITH_XC_SSHAGENT
- if (config()->get("SSHAgent", false).toBool()) {
- connect(this, SIGNAL(databaseLocked()), SSHAgent::instance(), SLOT(databaseModeChanged()));
- connect(this, SIGNAL(databaseUnlocked()), SSHAgent::instance(), SLOT(databaseModeChanged()));
+ if (sshAgent()->isEnabled()) {
+ connect(this, SIGNAL(databaseLockRequested()), sshAgent(), SLOT(databaseLocked()));
+ connect(this, SIGNAL(databaseUnlocked()), sshAgent(), SLOT(databaseUnlocked()));
}
#endif
@@ -249,15 +258,15 @@ QSharedPointer<Database> DatabaseWidget::database() const
DatabaseWidget::Mode DatabaseWidget::currentMode() const
{
if (currentWidget() == nullptr) {
- return DatabaseWidget::Mode::None;
+ return Mode::None;
} else if (currentWidget() == m_mainWidget) {
- return DatabaseWidget::Mode::ViewMode;
+ return Mode::ViewMode;
} else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) {
- return DatabaseWidget::Mode::LockedMode;
+ return Mode::LockedMode;
} else if (currentWidget() == m_csvImportWizard) {
- return DatabaseWidget::Mode::ImportMode;
+ return Mode::ImportMode;
} else {
- return DatabaseWidget::Mode::EditMode;
+ return Mode::EditMode;
}
}
@@ -266,11 +275,26 @@ bool DatabaseWidget::isLocked() const
return currentMode() == Mode::LockedMode;
}
+bool DatabaseWidget::isSaving() const
+{
+ return m_db->isSaving();
+}
+
+bool DatabaseWidget::isSorted() const
+{
+ return m_entryView->isSorted();
+}
+
bool DatabaseWidget::isSearchActive() const
{
return m_entryView->inSearchMode();
}
+bool DatabaseWidget::isEntryViewActive() const
+{
+ return currentWidget() == m_mainWidget;
+}
+
bool DatabaseWidget::isEntryEditActive() const
{
return currentWidget() == m_editEntryWidget;
@@ -604,9 +628,45 @@ bool DatabaseWidget::confirmDeleteEntries(QList<Entry*> entries, bool permanent)
}
}
-void DatabaseWidget::setFocus()
+void DatabaseWidget::setFocus(Qt::FocusReason reason)
{
- m_entryView->setFocus();
+ if (reason == Qt::BacktabFocusReason) {
+ m_previewView->setFocus();
+ } else {
+ m_groupView->setFocus();
+ }
+}
+
+void DatabaseWidget::focusOnEntries()
+{
+ if (isEntryViewActive()) {
+ m_entryView->setFocus();
+ }
+}
+
+void DatabaseWidget::focusOnGroups()
+{
+ if (isEntryViewActive()) {
+ m_groupView->setFocus();
+ }
+}
+
+void DatabaseWidget::moveEntryUp()
+{
+ auto currentEntry = currentSelectedEntry();
+ if (currentEntry) {
+ currentEntry->moveUp();
+ m_entryView->setCurrentEntry(currentEntry);
+ }
+}
+
+void DatabaseWidget::moveEntryDown()
+{
+ auto currentEntry = currentSelectedEntry();
+ if (currentEntry) {
+ currentEntry->moveDown();
+ m_entryView->setCurrentEntry(currentEntry);
+ }
}
void DatabaseWidget::copyTitle()
@@ -627,6 +687,14 @@ void DatabaseWidget::copyUsername()
void DatabaseWidget::copyPassword()
{
+ // QTextEdit does not properly trap Ctrl+C copy shortcut
+ // if a text edit has focus pass the copy operation to it
+ auto textEdit = qobject_cast<QTextEdit*>(focusWidget());
+ if (textEdit) {
+ textEdit->copy();
+ return;
+ }
+
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->password()));
@@ -670,15 +738,59 @@ void DatabaseWidget::showTotpKeyQrCode()
void DatabaseWidget::setClipboardTextAndMinimize(const QString& text)
{
clipboard()->setText(text);
- if (config()->get("HideWindowOnCopy").toBool()) {
- if (config()->get("MinimizeOnCopy").toBool()) {
- window()->showMinimized();
- } else if (config()->get("DropToBackgroundOnCopy").toBool()) {
+ if (config()->get(Config::HideWindowOnCopy).toBool()) {
+ if (config()->get(Config::MinimizeOnCopy).toBool()) {
+ getMainWindow()->minimizeOrHide();
+ } else if (config()->get(Config::DropToBackgroundOnCopy).toBool()) {
window()->lower();
}
}
}
+#ifdef WITH_XC_SSHAGENT
+void DatabaseWidget::addToAgent()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ Q_ASSERT(currentEntry);
+ if (!currentEntry) {
+ return;
+ }
+
+ KeeAgentSettings settings;
+ if (!settings.fromEntry(currentEntry)) {
+ return;
+ }
+
+ OpenSSHKey key;
+ if (settings.toOpenSSHKey(currentEntry, key, true)) {
+ SSHAgent::instance()->addIdentity(key, settings, database()->uuid());
+ } else {
+ m_messageWidget->showMessage(key.errorString(), MessageWidget::Error);
+ }
+}
+
+void DatabaseWidget::removeFromAgent()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ Q_ASSERT(currentEntry);
+ if (!currentEntry) {
+ return;
+ }
+
+ KeeAgentSettings settings;
+ if (!settings.fromEntry(currentEntry)) {
+ return;
+ }
+
+ OpenSSHKey key;
+ if (settings.toOpenSSHKey(currentEntry, key, false)) {
+ SSHAgent::instance()->removeIdentity(key);
+ } else {
+ m_messageWidget->showMessage(key.errorString(), MessageWidget::Error);
+ }
+}
+#endif
+
void DatabaseWidget::performAutoType()
{
auto currentEntry = currentSelectedEntry();
@@ -776,17 +888,19 @@ void DatabaseWidget::openUrlForEntry(Entry* entry)
if (launch) {
QProcess::startDetached(cmdString.mid(6));
- if (config()->get("MinimizeOnOpenUrl").toBool()) {
- window()->showMinimized();
+ if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
+ getMainWindow()->minimizeOrHide();
}
}
+ } else if (cmdString.startsWith("kdbx://")) {
+ openDatabaseFromEntry(entry, false);
} else {
QUrl url = QUrl::fromUserInput(entry->resolveMultiplePlaceholders(entry->url()));
if (!url.isEmpty()) {
QDesktopServices::openUrl(url);
- if (config()->get("MinimizeOnOpenUrl").toBool()) {
- window()->showMinimized();
+ if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
+ getMainWindow()->minimizeOrHide();
}
}
}
@@ -861,6 +975,8 @@ int DatabaseWidget::addChildWidget(QWidget* w)
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
{
+ setCurrentWidget(m_mainWidget);
+
if (m_newGroup) {
if (previousDialogAccepted) {
m_newGroup->setParent(m_newParent);
@@ -886,12 +1002,10 @@ void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
m_entryView->setFocus();
}
- setCurrentWidget(m_mainWidget);
-
if (sender() == m_entryView || sender() == m_editEntryWidget) {
onEntryChanged(m_entryView->currentEntry());
} else if (sender() == m_groupView || sender() == m_editGroupWidget) {
- onGroupChanged(m_groupView->currentGroup());
+ onGroupChanged();
}
}
@@ -966,8 +1080,8 @@ void DatabaseWidget::loadDatabase(bool accepted)
processAutoOpen();
m_saveAttempts = 0;
emit databaseUnlocked();
- if (config()->get("MinimizeAfterUnlock").toBool()) {
- window()->showMinimized();
+ if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
+ getMainWindow()->minimizeOrHide();
}
} else {
if (m_databaseOpenWidget->database()) {
@@ -1105,6 +1219,12 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
}
}
+void DatabaseWidget::switchToDatabaseReports()
+{
+ m_reportsDialog->load(m_db);
+ setCurrentWidget(m_reportsDialog);
+}
+
void DatabaseWidget::switchToDatabaseSettings()
{
m_databaseSettingDialog->load(m_db);
@@ -1113,7 +1233,9 @@ void DatabaseWidget::switchToDatabaseSettings()
void DatabaseWidget::switchToOpenDatabase()
{
- switchToOpenDatabase(m_db->filePath());
+ if (currentWidget() != m_databaseOpenWidget || m_databaseOpenWidget->filename() != m_db->filePath()) {
+ switchToOpenDatabase(m_db->filePath());
+ }
}
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
@@ -1185,10 +1307,10 @@ void DatabaseWidget::sortGroupsDesc()
m_groupView->sortGroups(true);
}
-void DatabaseWidget::switchToMasterKeyChange()
+void DatabaseWidget::switchToDatabaseSecurity()
{
switchToDatabaseSettings();
- m_databaseSettingDialog->showMasterKeySettings();
+ m_databaseSettingDialog->showDatabaseKeySettings();
}
void DatabaseWidget::performUnlockDatabase(const QString& password, const QString& keyfile)
@@ -1253,8 +1375,10 @@ void DatabaseWidget::setSearchLimitGroup(bool state)
refreshSearch();
}
-void DatabaseWidget::onGroupChanged(Group* group)
+void DatabaseWidget::onGroupChanged()
{
+ auto group = m_groupView->currentGroup();
+
// Intercept group changes if in search mode
if (isSearchActive() && m_searchLimitGroup) {
search(m_lastSearchText);
@@ -1279,7 +1403,7 @@ void DatabaseWidget::onGroupChanged(Group* group)
void DatabaseWidget::onDatabaseModified()
{
- if (!m_blockAutoSave && config()->get("AutoSaveAfterEveryChange").toBool() && !m_db->isReadOnly()) {
+ if (!m_blockAutoSave && config()->get(Config::AutoSaveAfterEveryChange).toBool() && !m_db->isReadOnly()) {
save();
} else {
// Only block once, then reset
@@ -1295,13 +1419,11 @@ QString DatabaseWidget::getCurrentSearch()
void DatabaseWidget::endSearch()
{
if (isSearchActive()) {
- emit listModeAboutToActivate();
-
// Show the normal entry view of the current group
+ emit listModeAboutToActivate();
m_entryView->displayGroup(currentGroup());
- onGroupChanged(currentGroup());
-
emit listModeActivated();
+ m_entryView->setFirstEntryActive();
}
m_searchingLabel->setVisible(false);
@@ -1362,12 +1484,43 @@ void DatabaseWidget::showEvent(QShowEvent* event)
event->accept();
}
+bool DatabaseWidget::focusNextPrevChild(bool next)
+{
+ // [parent] <-> GroupView <-> EntryView <-> EntryPreview <-> [parent]
+ if (next) {
+ if (m_groupView->hasFocus()) {
+ m_entryView->setFocus();
+ return true;
+ } else if (m_entryView->hasFocus()) {
+ m_previewView->setFocus();
+ return true;
+ }
+ } else {
+ if (m_previewView->hasFocus()) {
+ m_entryView->setFocus();
+ return true;
+ } else if (m_entryView->hasFocus()) {
+ m_groupView->setFocus();
+ return true;
+ }
+ }
+
+ // Defer to the parent widget to make a decision
+ return QStackedWidget::focusNextPrevChild(next);
+}
+
bool DatabaseWidget::lock()
{
if (isLocked()) {
return true;
}
+ // Don't try to lock the database while saving, this will cause a deadlock
+ if (m_db->isSaving()) {
+ QTimer::singleShot(200, this, SLOT(lock()));
+ return false;
+ }
+
emit databaseLockRequested();
clipboard()->clearCopiedText();
@@ -1383,10 +1536,11 @@ bool DatabaseWidget::lock()
}
}
- if (m_db->isModified()) {
+ if (m_db->isModified(true)) {
bool saved = false;
// Attempt to save on exit, but don't block locking if it fails
- if (config()->get("AutoSaveOnExit").toBool() || config()->get("AutoSaveAfterEveryChange").toBool()) {
+ if (config()->get(Config::AutoSaveOnExit).toBool()
+ || config()->get(Config::AutoSaveAfterEveryChange).toBool()) {
saved = save();
}
@@ -1444,7 +1598,7 @@ void DatabaseWidget::reloadDatabaseFile()
m_blockAutoSave = true;
- if (!config()->get("AutoReloadOnChange").toBool()) {
+ if (!config()->get(Config::AutoReloadOnChange).toBool()) {
// Ask if we want to reload the db
auto result = MessageBox::question(this,
tr("File has changed"),
@@ -1466,7 +1620,7 @@ void DatabaseWidget::reloadDatabaseFile()
QString error;
auto db = QSharedPointer<Database>::create(m_db->filePath());
if (db->open(database()->key(), &error)) {
- if (m_db->isModified()) {
+ if (m_db->isModified(true)) {
// Ask if we want to merge changes into new database
auto result = MessageBox::question(
this,
@@ -1513,6 +1667,11 @@ int DatabaseWidget::numberOfSelectedEntries() const
return m_entryView->numberOfSelectedEntries();
}
+int DatabaseWidget::currentEntryIndex() const
+{
+ return m_entryView->currentEntryIndex();
+}
+
QStringList DatabaseWidget::customEntryAttributes() const
{
Entry* entry = m_entryView->currentEntry();
@@ -1543,11 +1702,6 @@ bool DatabaseWidget::isGroupSelected() const
return m_groupView->currentGroup();
}
-bool DatabaseWidget::currentEntryHasFocus()
-{
- return m_entryView->numberOfSelectedEntries() > 0 && m_entryView->hasFocus();
-}
-
bool DatabaseWidget::currentEntryHasTitle()
{
auto currentEntry = currentSelectedEntry();
@@ -1598,6 +1752,19 @@ bool DatabaseWidget::currentEntryHasTotp()
return currentEntry->hasTotp();
}
+#ifdef WITH_XC_SSHAGENT
+bool DatabaseWidget::currentEntryHasSshKey()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ Q_ASSERT(currentEntry);
+ if (!currentEntry) {
+ return false;
+ }
+
+ return KeeAgentSettings::inEntryAttachments(currentEntry->attachments());
+}
+#endif
+
bool DatabaseWidget::currentEntryHasNotes()
{
auto currentEntry = currentSelectedEntry();
@@ -1654,9 +1821,9 @@ bool DatabaseWidget::save()
m_groupView->setDisabled(true);
QApplication::processEvents();
- bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
+ bool useAtomicSaves = config()->get(Config::UseAtomicSaves).toBool();
QString errorMessage;
- bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get("BackupBeforeSave").toBool());
+ bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get(Config::BackupBeforeSave).toBool());
// Return control
m_entryView->setDisabled(false);
@@ -1682,7 +1849,7 @@ bool DatabaseWidget::save()
MessageBox::Disable | MessageBox::Cancel,
MessageBox::Disable);
if (result == MessageBox::Disable) {
- config()->set("UseAtomicSaves", false);
+ config()->set(Config::UseAtomicSaves, false);
return save();
}
}
@@ -1702,14 +1869,75 @@ bool DatabaseWidget::save()
*/
bool DatabaseWidget::saveAs()
{
+ // Never allow saving a locked database; it causes corruption
+ Q_ASSERT(!isLocked());
+ // Release build interlock
+ if (isLocked()) {
+ // We return true since a save is not required
+ return true;
+ }
+
+ QString oldFilePath = m_db->filePath();
+ if (!QFileInfo::exists(oldFilePath)) {
+ oldFilePath =
+ QDir::toNativeSeparators(config()->get(Config::LastDir).toString() + "/" + tr("Passwords").append(".kdbx"));
+ }
+ const QString newFilePath = fileDialog()->getSaveFileName(
+ this, tr("Save database as"), oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"), nullptr, nullptr);
+
+ bool ok = false;
+ if (!newFilePath.isEmpty()) {
+ auto focusWidget = qApp->focusWidget();
+
+ // Lock out interactions
+ m_entryView->setDisabled(true);
+ m_groupView->setDisabled(true);
+ QApplication::processEvents();
+
+ QString errorMessage;
+ ok = m_db->saveAs(newFilePath,
+ &errorMessage,
+ config()->get(Config::UseAtomicSaves).toBool(),
+ config()->get(Config::BackupBeforeSave).toBool());
+
+ // Return control
+ m_entryView->setDisabled(false);
+ m_groupView->setDisabled(false);
+
+ if (focusWidget) {
+ focusWidget->setFocus();
+ }
+
+ if (!ok) {
+ showMessage(tr("Writing the database failed: %1").arg(errorMessage),
+ MessageWidget::Error,
+ true,
+ MessageWidget::LongAutoHideTimeout);
+ }
+ }
+
+ return ok;
+}
+
+/**
+ * Save copy of database under a new user-selected filename.
+ *
+ * @return true on success
+ */
+bool DatabaseWidget::saveBackup()
+{
while (true) {
QString oldFilePath = m_db->filePath();
if (!QFileInfo::exists(oldFilePath)) {
- oldFilePath = QDir::toNativeSeparators(config()->get("LastDir", QDir::homePath()).toString() + "/"
+ oldFilePath = QDir::toNativeSeparators(config()->get(Config::LastDir).toString() + "/"
+ tr("Passwords").append(".kdbx"));
}
- const QString newFilePath = fileDialog()->getSaveFileName(
- this, tr("Save database as"), oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"), nullptr, nullptr);
+ const QString newFilePath = fileDialog()->getSaveFileName(this,
+ tr("Save database backup"),
+ oldFilePath,
+ tr("KeePass 2 Database").append(" (*.kdbx)"),
+ nullptr,
+ nullptr);
if (!newFilePath.isEmpty()) {
// Ensure we don't recurse back into this function
@@ -1717,11 +1945,19 @@ bool DatabaseWidget::saveAs()
m_db->setFilePath(newFilePath);
m_saveAttempts = 0;
+ bool modified = m_db->isModified();
+
if (!save()) {
// Failed to save, try again
+ m_db->setFilePath(oldFilePath);
continue;
}
+ m_db->setFilePath(oldFilePath);
+ if (modified) {
+ // Source database is marked as clean when copy is saved, even if source has unsaved changes
+ m_db->markAsModified();
+ }
return true;
}
@@ -1788,38 +2024,73 @@ void DatabaseWidget::processAutoOpen()
if (entry->url().isEmpty() || (entry->password().isEmpty() && entry->username().isEmpty())) {
continue;
}
- QFileInfo filepath;
- QFileInfo keyfile;
- if (entry->url().startsWith("file://")) {
- QUrl url(entry->url());
- filepath.setFile(url.toLocalFile());
- } else {
- filepath.setFile(entry->url());
- if (filepath.isRelative()) {
- QFileInfo currentpath(m_db->filePath());
- filepath.setFile(currentpath.absoluteDir(), entry->url());
+ // Support ifDevice advanced entry, a comma separated list of computer names
+ // that control whether to perform AutoOpen on this entry or not. Can be
+ // negated using '!'
+ auto ifDevice = entry->attribute("ifDevice");
+ if (!ifDevice.isEmpty()) {
+ bool loadDb = false;
+ auto hostName = QHostInfo::localHostName();
+ for (auto& dev : ifDevice.split(",")) {
+ dev = dev.trimmed();
+ if (dev.startsWith("!") && dev.mid(1).compare(hostName, Qt::CaseInsensitive) == 0) {
+ // Machine name matched an exclusion, don't load this database
+ loadDb = false;
+ break;
+ } else if (dev.compare(hostName, Qt::CaseInsensitive) == 0) {
+ loadDb = true;
+ }
+ }
+ if (!loadDb) {
+ continue;
}
}
- if (!filepath.isFile()) {
- continue;
+ openDatabaseFromEntry(entry);
+ }
+}
+
+void DatabaseWidget::openDatabaseFromEntry(const Entry* entry, bool inBackground)
+{
+ auto keyFile = entry->resolveMultiplePlaceholders(entry->username());
+ auto password = entry->resolveMultiplePlaceholders(entry->password());
+ auto databaseUrl = entry->resolveMultiplePlaceholders(entry->url());
+ if (databaseUrl.startsWith("kdbx://")) {
+ databaseUrl = databaseUrl.mid(7);
+ }
+
+ QFileInfo dbFileInfo;
+ if (databaseUrl.startsWith("file://")) {
+ QUrl url(databaseUrl);
+ dbFileInfo.setFile(url.toLocalFile());
+ } else {
+ dbFileInfo.setFile(databaseUrl);
+ if (dbFileInfo.isRelative()) {
+ QFileInfo currentpath(m_db->filePath());
+ dbFileInfo.setFile(currentpath.absoluteDir(), databaseUrl);
}
+ }
- if (!entry->username().isEmpty()) {
- if (entry->username().startsWith("file://")) {
- QUrl keyfileUrl(entry->username());
- keyfile.setFile(keyfileUrl.toLocalFile());
- } else {
- keyfile.setFile(entry->username());
- if (keyfile.isRelative()) {
- QFileInfo currentpath(m_db->filePath());
- keyfile.setFile(currentpath.absoluteDir(), entry->username());
- }
+ if (!dbFileInfo.isFile()) {
+ showErrorMessage(tr("Could not find database file: %1").arg(databaseUrl));
+ return;
+ }
+
+ QFileInfo keyFileInfo;
+ if (!keyFile.isEmpty()) {
+ if (keyFile.startsWith("file://")) {
+ QUrl keyfileUrl(keyFile);
+ keyFileInfo.setFile(keyfileUrl.toLocalFile());
+ } else {
+ keyFileInfo.setFile(keyFile);
+ if (keyFileInfo.isRelative()) {
+ QFileInfo currentpath(m_db->filePath());
+ keyFileInfo.setFile(currentpath.absoluteDir(), keyFile);
}
}
-
- // Request to open the database file in the background with a password and keyfile
- emit requestOpenDatabase(filepath.canonicalFilePath(), true, entry->password(), keyfile.canonicalFilePath());
}
+
+ // Request to open the database file in the background with a password and keyfile
+ emit requestOpenDatabase(dbFileInfo.canonicalFilePath(), inBackground, password, keyFileInfo.canonicalFilePath());
}
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index 9f0c5c976..a31dfd37b 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -34,6 +34,7 @@ class DatabaseOpenWidget;
class KeePass1OpenWidget;
class OpVaultOpenWidget;
class DatabaseSettingsDialog;
+class ReportsDialog;
class Database;
class FileWatcher;
class EditEntryWidget;
@@ -75,11 +76,16 @@ public:
explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr);
~DatabaseWidget();
+ void setFocus(Qt::FocusReason reason);
+
QSharedPointer<Database> database() const;
DatabaseWidget::Mode currentMode() const;
bool isLocked() const;
+ bool isSaving() const;
+ bool isSorted() const;
bool isSearchActive() const;
+ bool isEntryViewActive() const;
bool isEntryEditActive() const;
bool isGroupEditActive() const;
@@ -94,6 +100,7 @@ public:
bool isGroupSelected() const;
bool isRecycleBinSelected() const;
int numberOfSelectedEntries() const;
+ int currentEntryIndex() const;
QStringList customEntryAttributes() const;
bool isEditWidgetModified() const;
@@ -102,13 +109,15 @@ public:
bool isPasswordsHidden() const;
void setPasswordsHidden(bool hide);
void clearAllWidgets();
- bool currentEntryHasFocus();
bool currentEntryHasTitle();
bool currentEntryHasUsername();
bool currentEntryHasPassword();
bool currentEntryHasUrl();
bool currentEntryHasNotes();
bool currentEntryHasTotp();
+#ifdef WITH_XC_SSHAGENT
+ bool currentEntryHasSshKey();
+#endif
QByteArray entryViewState() const;
bool setEntryViewState(const QByteArray& state) const;
@@ -151,13 +160,17 @@ public slots:
bool lock();
bool save();
bool saveAs();
+ bool saveBackup();
void replaceDatabase(QSharedPointer<Database> db);
void createEntry();
void cloneEntry();
void deleteSelectedEntries();
void deleteEntries(QList<Entry*> entries);
- void setFocus();
+ void focusOnEntries();
+ void focusOnGroups();
+ void moveEntryUp();
+ void moveEntryDown();
void copyTitle();
void copyUsername();
void copyPassword();
@@ -168,6 +181,10 @@ public slots:
void showTotpKeyQrCode();
void copyTotp();
void setupTotp();
+#ifdef WITH_XC_SSHAGENT
+ void addToAgent();
+ void removeFromAgent();
+#endif
void performAutoType();
void openUrl();
void downloadSelectedFavicons();
@@ -180,7 +197,8 @@ public slots:
void switchToGroupEdit();
void sortGroupsAsc();
void sortGroupsDesc();
- void switchToMasterKeyChange();
+ void switchToDatabaseSecurity();
+ void switchToDatabaseReports();
void switchToDatabaseSettings();
void switchToOpenDatabase();
void switchToOpenDatabase(const QString& filePath);
@@ -208,6 +226,7 @@ public slots:
protected:
void closeEvent(QCloseEvent* event) override;
void showEvent(QShowEvent* event) override;
+ bool focusNextPrevChild(bool next) override;
private slots:
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
@@ -219,7 +238,7 @@ private slots:
void emitGroupContextMenuRequested(const QPoint& pos);
void emitEntryContextMenuRequested(const QPoint& pos);
void onEntryChanged(Entry* entry);
- void onGroupChanged(Group* group);
+ void onGroupChanged();
void onDatabaseModified();
void connectDatabaseSignals();
void loadDatabase(bool accepted);
@@ -234,6 +253,7 @@ private:
int addChildWidget(QWidget* w);
void setClipboardTextAndMinimize(const QString& text);
void processAutoOpen();
+ void openDatabaseFromEntry(const Entry* entry, bool inBackground = true);
bool confirmDeleteEntries(QList<Entry*> entries, bool permanent);
void performIconDownloads(const QList<Entry*>& entries, bool force = false);
Entry* currentSelectedEntry();
@@ -251,6 +271,7 @@ private:
QPointer<EditEntryWidget> m_editEntryWidget;
QPointer<EditGroupWidget> m_editGroupWidget;
QPointer<EditEntryWidget> m_historyEditEntryWidget;
+ QPointer<ReportsDialog> m_reportsDialog;
QPointer<DatabaseSettingsDialog> m_databaseSettingDialog;
QPointer<DatabaseOpenWidget> m_databaseOpenWidget;
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
diff --git a/src/gui/DatabaseWidgetStateSync.cpp b/src/gui/DatabaseWidgetStateSync.cpp
index 5579b30cd..e984e3d5f 100644
--- a/src/gui/DatabaseWidgetStateSync.cpp
+++ b/src/gui/DatabaseWidgetStateSync.cpp
@@ -27,12 +27,12 @@ DatabaseWidgetStateSync::DatabaseWidgetStateSync(QObject* parent)
, m_activeDbWidget(nullptr)
, m_blockUpdates(false)
{
- m_mainSplitterSizes = variantToIntList(config()->get("GUI/SplitterState"));
- m_previewSplitterSizes = variantToIntList(config()->get("GUI/PreviewSplitterState"));
- m_hideUsernames = config()->get("GUI/HideUsernames").toBool();
- m_hidePasswords = config()->get("GUI/HidePasswords").toBool();
- m_listViewState = config()->get("GUI/ListViewState").toByteArray();
- m_searchViewState = config()->get("GUI/SearchViewState").toByteArray();
+ m_mainSplitterSizes = variantToIntList(config()->get(Config::GUI_SplitterState));
+ m_previewSplitterSizes = variantToIntList(config()->get(Config::GUI_PreviewSplitterState));
+ m_hideUsernames = config()->get(Config::GUI_HideUsernames).toBool();
+ m_hidePasswords = true;
+ m_listViewState = config()->get(Config::GUI_ListViewState).toByteArray();
+ m_searchViewState = config()->get(Config::GUI_SearchViewState).toByteArray();
connect(qApp, &QCoreApplication::aboutToQuit, this, &DatabaseWidgetStateSync::sync);
}
@@ -46,12 +46,11 @@ DatabaseWidgetStateSync::~DatabaseWidgetStateSync()
*/
void DatabaseWidgetStateSync::sync()
{
- config()->set("GUI/SplitterState", intListToVariant(m_mainSplitterSizes));
- config()->set("GUI/PreviewSplitterState", intListToVariant(m_previewSplitterSizes));
- config()->set("GUI/HideUsernames", m_hideUsernames);
- config()->set("GUI/HidePasswords", m_hidePasswords);
- config()->set("GUI/ListViewState", m_listViewState);
- config()->set("GUI/SearchViewState", m_searchViewState);
+ config()->set(Config::GUI_SplitterState, intListToVariant(m_mainSplitterSizes));
+ config()->set(Config::GUI_PreviewSplitterState, intListToVariant(m_previewSplitterSizes));
+ config()->set(Config::GUI_HideUsernames, m_hideUsernames);
+ config()->set(Config::GUI_ListViewState, m_listViewState);
+ config()->set(Config::GUI_SearchViewState, m_searchViewState);
config()->sync();
}
@@ -101,9 +100,7 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget)
*
* NOTE:
* If m_listViewState is empty, the list view has been activated for the first
- * time after starting with a clean (or invalid) config. Thus, save the current
- * state. Without this, m_listViewState would remain empty until there is an
- * actual view state change (e.g. column is resized)
+ * time after starting with a clean (or invalid) config.
*/
void DatabaseWidgetStateSync::restoreListView()
{
@@ -112,8 +109,6 @@ void DatabaseWidgetStateSync::restoreListView()
if (!m_listViewState.isEmpty()) {
m_activeDbWidget->setEntryViewState(m_listViewState);
- } else {
- m_listViewState = m_activeDbWidget->entryViewState();
}
m_blockUpdates = false;
diff --git a/src/gui/DialogyWidget.cpp b/src/gui/DialogyWidget.cpp
index 597bcc59d..070393933 100644
--- a/src/gui/DialogyWidget.cpp
+++ b/src/gui/DialogyWidget.cpp
@@ -24,6 +24,7 @@
DialogyWidget::DialogyWidget(QWidget* parent)
: QWidget(parent)
{
+ setAutoFillBackground(true);
}
void DialogyWidget::keyPressEvent(QKeyEvent* e)
diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp
index f7030c9d7..68a8d7d4a 100644
--- a/src/gui/EditWidget.cpp
+++ b/src/gui/EditWidget.cpp
@@ -22,7 +22,7 @@
#include <QPushButton>
#include <QScrollArea>
-#include "core/FilePath.h"
+#include "core/Resources.h"
EditWidget::EditWidget(QWidget* parent)
: DialogyWidget(parent)
@@ -70,7 +70,16 @@ void EditWidget::addPage(const QString& labelText, const QIcon& icon, QWidget* w
void EditWidget::setPageHidden(QWidget* widget, bool hidden)
{
- int index = m_ui->stackedWidget->indexOf(widget);
+ int index = -1;
+
+ for (int i = 0; i < m_ui->stackedWidget->count(); i++) {
+ auto* scrollArea = qobject_cast<QScrollArea*>(m_ui->stackedWidget->widget(i));
+ if (scrollArea && scrollArea->widget() == widget) {
+ index = i;
+ break;
+ }
+ }
+
if (index != -1) {
m_ui->categoryList->setCategoryHidden(index, hidden);
}
diff --git a/src/gui/EditWidget.ui b/src/gui/EditWidget.ui
index b8ac5f3eb..8bbec46bc 100644
--- a/src/gui/EditWidget.ui
+++ b/src/gui/EditWidget.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>612</width>
- <height>255</height>
+ <width>527</width>
+ <height>391</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -15,11 +15,27 @@
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
- <widget class="QLabel" name="headerLabel">
- <property name="text">
- <string/>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="leftMargin">
+ <number>2</number>
</property>
- </widget>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="headerLabel">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
<spacer name="verticalSpacer">
@@ -39,6 +55,9 @@
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0">
+ <property name="spacing">
+ <number>18</number>
+ </property>
<item>
<widget class="CategoryListWidget" name="categoryList" native="true">
<property name="sizePolicy">
diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp
index 83bc0fc35..e348e8803 100644
--- a/src/gui/EditWidgetIcons.cpp
+++ b/src/gui/EditWidgetIcons.cpp
@@ -35,6 +35,7 @@
IconStruct::IconStruct()
: uuid(QUuid())
, number(0)
+ , applyTo(ApplyIconToOptions::THIS_ONLY)
{
}
@@ -131,7 +132,7 @@ void EditWidgetIcons::load(const QUuid& currentUuid,
m_currentUuid = currentUuid;
setUrl(url);
- m_customIconModel->setIcons(database->metadata()->customIconsScaledPixmaps(),
+ m_customIconModel->setIcons(database->metadata()->customIconsPixmaps(IconSize::Default),
database->metadata()->customIconsOrder());
QUuid iconUuid = iconStruct.uuid;
@@ -162,7 +163,7 @@ void EditWidgetIcons::setShowApplyIconToButton(bool state)
QMenu* EditWidgetIcons::createApplyIconToMenu()
{
auto* applyIconToMenu = new QMenu(this);
- QAction* defaultAction = applyIconToMenu->addAction(tr("Apply to this only"));
+ QAction* defaultAction = applyIconToMenu->addAction(tr("Apply to this group only"));
defaultAction->setData(QVariant::fromValue(ApplyIconToOptions::THIS_ONLY));
applyIconToMenu->setDefaultAction(defaultAction);
applyIconToMenu->addSeparator();
@@ -202,7 +203,7 @@ void EditWidgetIcons::iconReceived(const QString& url, const QImage& icon)
Q_UNUSED(url);
if (icon.isNull()) {
QString message(tr("Unable to fetch favicon."));
- if (!config()->get("security/IconDownloadFallback", false).toBool()) {
+ if (!config()->get(Config::Security_IconDownloadFallback).toBool()) {
message.append("\n").append(
tr("You can enable the DuckDuckGo website icon service under Tools -> Settings -> Security"));
}
@@ -293,7 +294,7 @@ bool EditWidgetIcons::addCustomIcon(const QImage& icon)
if (uuid.isNull()) {
uuid = QUuid::createUuid();
m_db->metadata()->addCustomIcon(uuid, scaledicon);
- m_customIconModel->setIcons(m_db->metadata()->customIconsScaledPixmaps(),
+ m_customIconModel->setIcons(m_db->metadata()->customIconsPixmaps(IconSize::Default),
m_db->metadata()->customIconsOrder());
added = true;
}
@@ -377,7 +378,7 @@ void EditWidgetIcons::removeCustomIcon()
// Remove the icon from the database
m_db->metadata()->removeCustomIcon(iconUuid);
- m_customIconModel->setIcons(m_db->metadata()->customIconsScaledPixmaps(),
+ m_customIconModel->setIcons(m_db->metadata()->customIconsPixmaps(IconSize::Default),
m_db->metadata()->customIconsOrder());
// Reset the current icon view
diff --git a/src/gui/EditWidgetIcons.ui b/src/gui/EditWidgetIcons.ui
index 9648cca08..b23f5fba3 100644
--- a/src/gui/EditWidgetIcons.ui
+++ b/src/gui/EditWidgetIcons.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>437</width>
- <height>300</height>
+ <height>316</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -26,7 +26,7 @@
<item>
<widget class="QRadioButton" name="defaultIconsRadio">
<property name="text">
- <string>&amp;Use default icon</string>
+ <string>Use default icon</string>
</property>
</widget>
</item>
@@ -48,17 +48,33 @@
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
- <number>8</number>
+ <number>4</number>
</property>
<property name="viewMode">
- <enum>QListView::IconMode</enum>
+ <enum>QListView::ListMode</enum>
</property>
</widget>
</item>
<item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
<widget class="QRadioButton" name="customIconsRadio">
<property name="text">
- <string>Use custo&amp;m icon</string>
+ <string>Use custom icon</string>
</property>
</widget>
</item>
@@ -80,10 +96,10 @@
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
- <number>8</number>
+ <number>4</number>
</property>
<property name="viewMode">
- <enum>QListView::IconMode</enum>
+ <enum>QListView::ListMode</enum>
</property>
</widget>
</item>
@@ -151,7 +167,7 @@
<string notr="true">padding: 4px 10px</string>
</property>
<property name="text">
- <string>Apply icon &amp;to ...</string>
+ <string>Apply icon to...</string>
</property>
</widget>
</item>
@@ -166,6 +182,7 @@
<tabstop>customIconsView</tabstop>
<tabstop>addButton</tabstop>
<tabstop>deleteButton</tabstop>
+ <tabstop>faviconButton</tabstop>
<tabstop>applyIconToPushButton</tabstop>
</tabstops>
<resources/>
diff --git a/src/gui/EditWidgetProperties.ui b/src/gui/EditWidgetProperties.ui
index d80bf1584..0caa9ef27 100644
--- a/src/gui/EditWidgetProperties.ui
+++ b/src/gui/EditWidgetProperties.ui
@@ -6,11 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
- <width>614</width>
- <height>328</height>
+ <width>364</width>
+ <height>408</height>
</rect>
</property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
<property name="spacing">
<number>5</number>
</property>
@@ -27,18 +27,21 @@
<number>0</number>
</property>
<item>
- <layout class="QFormLayout" name="formLayout_2">
- <property name="fieldGrowthPolicy">
- <enum>QFormLayout::ExpandingFieldsGrow</enum>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="verticalSpacing">
+ <number>8</number>
</property>
- <item row="1" column="0">
+ <item row="0" column="0">
<widget class="QLabel" name="labelCreated">
<property name="text">
<string>Created:</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
</widget>
</item>
- <item row="1" column="1">
+ <item row="0" column="1">
<widget class="QLineEdit" name="createdEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -54,14 +57,17 @@
</property>
</widget>
</item>
- <item row="2" column="0">
+ <item row="1" column="0">
<widget class="QLabel" name="labelModfied">
<property name="text">
<string>Modified:</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
</widget>
</item>
- <item row="2" column="1">
+ <item row="1" column="1">
<widget class="QLineEdit" name="modifiedEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@@ -77,14 +83,17 @@
</property>
</widget>
</item>
- <item row="3" column="0">
+ <item row="2" column="0">
<widget class="QLabel" name="labelAccessed">
<property name="text">
<string>Accessed:</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
</widget>
</item>
- <item row="3" column="1">
+ <item row="2" column="1">
<widget class="QLineEdit" name="accessedEdit">
<property name="accessibleName">
<string>Datetime accessed</string>
@@ -94,14 +103,17 @@
</property>
</widget>
</item>
- <item row="4" column="0">
+ <item row="3" column="0">
<widget class="QLabel" name="labelUuid">
<property name="text">
<string>Uuid:</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
</widget>
</item>
- <item row="4" column="1">
+ <item row="3" column="1">
<widget class="QLineEdit" name="uuidEdit">
<property name="accessibleName">
<string>Unique ID</string>
@@ -114,6 +126,22 @@
</layout>
</item>
<item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Plugin Data</string>
diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp
index 2e2e37dbc..b873800a8 100644
--- a/src/gui/EntryPreviewWidget.cpp
+++ b/src/gui/EntryPreviewWidget.cpp
@@ -24,7 +24,7 @@
#include <QDir>
#include "core/Config.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "entry/EntryAttachmentsModel.h"
#include "gui/Clipboard.h"
#if defined(WITH_XC_KEESHARE)
@@ -48,12 +48,12 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
m_ui->setupUi(this);
// Entry
- m_ui->entryTotpButton->setIcon(filePath()->icon("actions", "chronometer"));
- m_ui->entryCloseButton->setIcon(filePath()->icon("actions", "dialog-close"));
+ m_ui->entryTotpButton->setIcon(resources()->icon("chronometer"));
+ m_ui->entryCloseButton->setIcon(resources()->icon("dialog-close"));
m_ui->entryPasswordLabel->setFont(Font::fixedFont());
- m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
- m_ui->toggleEntryNotesButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
- m_ui->toggleGroupNotesButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
+ m_ui->togglePasswordButton->setIcon(resources()->onOffIcon("password-show"));
+ m_ui->toggleEntryNotesButton->setIcon(resources()->onOffIcon("password-show"));
+ m_ui->toggleGroupNotesButton->setIcon(resources()->onOffIcon("password-show"));
m_ui->entryAttachmentsWidget->setReadOnly(true);
m_ui->entryAttachmentsWidget->setButtonsVisible(false);
@@ -77,11 +77,19 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
connect(&m_totpTimer, SIGNAL(timeout()), SLOT(updateTotpLabel()));
+ connect(config(), &Config::changed, this, [this](Config::ConfigKey key) {
+ if (key == Config::GUI_HidePreviewPanel) {
+ setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
+ }
+ });
+
// Group
- m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close"));
+ m_ui->groupCloseButton->setIcon(resources()->icon("dialog-close"));
connect(m_ui->groupCloseButton, SIGNAL(clicked()), SLOT(hide()));
connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
+ setFocusProxy(m_ui->entryTabWidget);
+
#if !defined(WITH_XC_KEESHARE)
removeTab(m_ui->groupTabWidget, m_ui->groupShareTab);
#endif
@@ -106,7 +114,7 @@ void EntryPreviewWidget::setEntry(Entry* selectedEntry)
updateEntryAdvancedTab();
updateEntryAutotypeTab();
- setVisible(!config()->get("GUI/HidePreviewPanel").toBool());
+ setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry : GeneralTabIndex;
@@ -129,7 +137,7 @@ void EntryPreviewWidget::setGroup(Group* selectedGroup)
updateGroupSharingTab();
#endif
- setVisible(!config()->get("GUI/HidePreviewPanel").toBool());
+ setVisible(!config()->get(Config::GUI_HidePreviewPanel).toBool());
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup : GeneralTabIndex;
@@ -160,7 +168,7 @@ void EntryPreviewWidget::updateEntryHeaderLine()
Q_ASSERT(m_currentEntry);
const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title());
m_ui->entryTitleLabel->setRawText(hierarchy(m_currentEntry->group(), title));
- m_ui->entryIcon->setPixmap(preparePixmap(m_currentEntry->iconPixmap(), 16));
+ m_ui->entryIcon->setPixmap(m_currentEntry->iconPixmap(IconSize::Large));
}
void EntryPreviewWidget::updateEntryTotp()
@@ -186,7 +194,7 @@ void EntryPreviewWidget::setPasswordVisible(bool state)
if (state) {
m_ui->entryPasswordLabel->setText(password);
m_ui->entryPasswordLabel->setCursorPosition(0);
- } else if (password.isEmpty() && config()->get("security/passwordemptynodots").toBool()) {
+ } else if (password.isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) {
m_ui->entryPasswordLabel->setText("");
} else {
m_ui->entryPasswordLabel->setText(QString("\u25cf").repeated(6));
@@ -222,7 +230,7 @@ void EntryPreviewWidget::updateEntryGeneralTab()
m_ui->entryUsernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username()));
m_ui->entryUsernameLabel->setCursorPosition(0);
- if (config()->get("security/HidePasswordPreviewPanel").toBool()) {
+ if (config()->get(Config::Security_HidePasswordPreviewPanel).toBool()) {
// Hide password
setPasswordVisible(false);
// Show the password toggle button if there are dots in the label
@@ -234,7 +242,7 @@ void EntryPreviewWidget::updateEntryGeneralTab()
m_ui->togglePasswordButton->setVisible(false);
}
- if (config()->get("security/hidenotes").toBool()) {
+ if (config()->get(Config::Security_HideNotes).toBool()) {
setEntryNotesVisible(false);
m_ui->toggleEntryNotesButton->setVisible(!m_ui->entryNotesTextEdit->toPlainText().isEmpty());
m_ui->toggleEntryNotesButton->setChecked(false);
@@ -243,7 +251,7 @@ void EntryPreviewWidget::updateEntryGeneralTab()
m_ui->toggleEntryNotesButton->setVisible(false);
}
- if (config()->get("GUI/MonospaceNotes", false).toBool()) {
+ if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
m_ui->entryNotesTextEdit->setFont(Font::fixedFont());
} else {
m_ui->entryNotesTextEdit->setFont(Font::defaultFont());
@@ -313,7 +321,7 @@ void EntryPreviewWidget::updateGroupHeaderLine()
{
Q_ASSERT(m_currentGroup);
m_ui->groupTitleLabel->setRawText(hierarchy(m_currentGroup, {}));
- m_ui->groupIcon->setPixmap(preparePixmap(m_currentGroup->iconPixmap(), 32));
+ m_ui->groupIcon->setPixmap(m_currentGroup->iconPixmap(IconSize::Large));
}
void EntryPreviewWidget::updateGroupGeneralTab()
@@ -330,7 +338,7 @@ void EntryPreviewWidget::updateGroupGeneralTab()
groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) : tr("Never");
m_ui->groupExpirationLabel->setText(expiresText);
- if (config()->get("security/hidenotes").toBool()) {
+ if (config()->get(Config::Security_HideNotes).toBool()) {
setGroupNotesVisible(false);
m_ui->toggleGroupNotesButton->setVisible(!m_ui->groupNotesTextEdit->toPlainText().isEmpty());
m_ui->toggleGroupNotesButton->setChecked(false);
@@ -339,7 +347,7 @@ void EntryPreviewWidget::updateGroupGeneralTab()
m_ui->toggleGroupNotesButton->setVisible(false);
}
- if (config()->get("GUI/MonospaceNotes", false).toBool()) {
+ if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
m_ui->groupNotesTextEdit->setFont(Font::fixedFont());
} else {
m_ui->groupNotesTextEdit->setFont(Font::defaultFont());
@@ -397,19 +405,8 @@ void EntryPreviewWidget::setTabEnabled(QTabWidget* tabWidget, QWidget* widget, b
tabWidget->setTabEnabled(tabIndex, enabled);
}
-QPixmap EntryPreviewWidget::preparePixmap(const QPixmap& pixmap, int size)
-{
- if (pixmap.width() > size || pixmap.height() > size) {
- return pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
- }
- return pixmap;
-}
-
QString EntryPreviewWidget::hierarchy(const Group* group, const QString& title)
{
- const QString separator("] > [");
- QStringList hierarchy = group->hierarchy();
- QString groupList = QString("[%1]").arg(hierarchy.join(separator));
-
- return title.isEmpty() ? groupList : QString("%1 > %2").arg(groupList, title);
+ QString groupList = QString("%1").arg(group->hierarchy().join(" / "));
+ return title.isEmpty() ? groupList : QStringLiteral("%1 / %2").arg(groupList, title);
}
diff --git a/src/gui/EntryPreviewWidget.h b/src/gui/EntryPreviewWidget.h
index e1a7aff38..17bd62eb6 100644
--- a/src/gui/EntryPreviewWidget.h
+++ b/src/gui/EntryPreviewWidget.h
@@ -72,7 +72,6 @@ private:
void removeTab(QTabWidget* tabWidget, QWidget* widget);
void setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled);
- static QPixmap preparePixmap(const QPixmap& pixmap, int size);
static QString hierarchy(const Group* group, const QString& title);
const QScopedPointer<Ui::EntryPreviewWidget> m_ui;
diff --git a/src/gui/EntryPreviewWidget.ui b/src/gui/EntryPreviewWidget.ui
index 4ac3702d5..d78b04a43 100644
--- a/src/gui/EntryPreviewWidget.ui
+++ b/src/gui/EntryPreviewWidget.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>566</width>
- <height>169</height>
+ <height>247</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
@@ -46,49 +46,62 @@
<number>0</number>
</property>
<item>
- <layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,1,0,0,0">
+ <layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
- <number>9</number>
+ <number>5</number>
+ </property>
+ <property name="topMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>3</number>
</property>
<item>
- <widget class="QLabel" name="entryIcon">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>16</width>
- <height>0</height>
- </size>
- </property>
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item>
- <widget class="ElidedLabel" name="entryTitleLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>12</number>
</property>
- <property name="font">
- <font>
- <pointsize>12</pointsize>
- </font>
- </property>
- <property name="textFormat">
- <enum>Qt::AutoText</enum>
- </property>
- </widget>
+ <item>
+ <widget class="QLabel" name="entryIcon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>16</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ElidedLabel" name="entryTitleLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>12</pointsize>
+ </font>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::AutoText</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
<widget class="QLabel" name="entryTotpLabel">
@@ -106,9 +119,6 @@
</item>
<item>
<widget class="QToolButton" name="entryTotpButton">
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
- </property>
<property name="toolTip">
<string>Display current TOTP value</string>
</property>
@@ -122,9 +132,6 @@
</item>
<item>
<widget class="QToolButton" name="entryCloseButton">
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
- </property>
<property name="toolTip">
<string>Close</string>
</property>
@@ -137,9 +144,6 @@
</item>
<item>
<widget class="QTabWidget" name="entryTabWidget">
- <property name="focusPolicy">
- <enum>Qt::ClickFocus</enum>
- </property>
<property name="currentIndex">
<number>0</number>
</property>
@@ -766,43 +770,56 @@
<number>0</number>
</property>
<item>
- <layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0,0">
+ <layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
- <number>9</number>
+ <number>5</number>
+ </property>
+ <property name="topMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>3</number>
</property>
<item>
- <widget class="QLabel" name="groupIcon">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item>
- <widget class="ElidedLabel" name="groupTitleLabel">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="spacing">
+ <number>12</number>
</property>
- <property name="font">
- <font>
- <pointsize>12</pointsize>
- </font>
- </property>
- <property name="textFormat">
- <enum>Qt::AutoText</enum>
- </property>
- </widget>
+ <item>
+ <widget class="QLabel" name="groupIcon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="ElidedLabel" name="groupTitleLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>12</pointsize>
+ </font>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::AutoText</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
<widget class="QToolButton" name="groupCloseButton">
@@ -1146,13 +1163,14 @@
</customwidget>
</customwidgets>
<tabstops>
- <tabstop>entryCloseButton</tabstop>
<tabstop>entryTotpButton</tabstop>
+ <tabstop>entryCloseButton</tabstop>
+ <tabstop>entryTabWidget</tabstop>
<tabstop>togglePasswordButton</tabstop>
<tabstop>toggleEntryNotesButton</tabstop>
- <tabstop>entryAutotypeTree</tabstop>
- <tabstop>groupCloseButton</tabstop>
<tabstop>groupTabWidget</tabstop>
+ <tabstop>toggleGroupNotesButton</tabstop>
+ <tabstop>groupCloseButton</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp
index 12f582775..df713c44b 100644
--- a/src/gui/FileDialog.cpp
+++ b/src/gui/FileDialog.cpp
@@ -21,6 +21,20 @@
#include <QDir>
+namespace
+{
+ QString modFilter(const QString& filter)
+ {
+#ifdef Q_OS_MACOS
+ // Fix macOS bug that causes the file dialog to freeze when a dot is included in the filters
+ // See https://github.com/keepassxreboot/keepassxc/issues/3895#issuecomment-586724167
+ auto mod = filter;
+ return mod.replace("*.", "*");
+#endif
+ return filter;
+ }
+} // namespace
+
FileDialog* FileDialog::m_instance(nullptr);
QString FileDialog::getOpenFileName(QWidget* parent,
@@ -35,9 +49,9 @@ QString FileDialog::getOpenFileName(QWidget* parent,
m_nextFileName.clear();
return result;
} else {
- const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir;
+ const auto& workingDir = dir.isEmpty() ? config()->get(Config::LastDir).toString() : dir;
const auto result = QDir::toNativeSeparators(
- QFileDialog::getOpenFileName(parent, caption, workingDir, filter, selectedFilter, options));
+ QFileDialog::getOpenFileName(parent, caption, workingDir, modFilter(filter), selectedFilter, options));
#ifdef Q_OS_MACOS
// on Mac OS X the focus is lost after closing the native dialog
@@ -62,11 +76,13 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent,
m_nextFileNames.clear();
return results;
} else {
- const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir;
- auto results = QFileDialog::getOpenFileNames(parent, caption, workingDir, filter, selectedFilter, options);
+ const auto& workingDir = dir.isEmpty() ? config()->get(Config::LastDir).toString() : dir;
+ auto results =
+ QFileDialog::getOpenFileNames(parent, caption, workingDir, modFilter(filter), selectedFilter, options);
- for (auto& path : results)
+ for (auto& path : results) {
path = QDir::toNativeSeparators(path);
+ }
#ifdef Q_OS_MACOS
// on Mac OS X the focus is lost after closing the native dialog
@@ -81,33 +97,6 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent,
}
}
-QString FileDialog::getFileName(QWidget* parent,
- const QString& caption,
- const QString& dir,
- const QString& filter,
- QString* selectedFilter,
- const QFileDialog::Options options)
-{
- if (!m_nextFileName.isEmpty()) {
- const QString result = m_nextFileName;
- m_nextFileName.clear();
- return result;
- } else {
- const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir;
- const auto result = QDir::toNativeSeparators(
- QFileDialog::getSaveFileName(parent, caption, workingDir, filter, selectedFilter, options));
-
-#ifdef Q_OS_MACOS
- // on Mac OS X the focus is lost after closing the native dialog
- if (parent) {
- parent->activateWindow();
- }
-#endif
- saveLastDir(result);
- return result;
- }
-}
-
QString FileDialog::getSaveFileName(QWidget* parent,
const QString& caption,
const QString& dir,
@@ -120,9 +109,9 @@ QString FileDialog::getSaveFileName(QWidget* parent,
m_nextFileName.clear();
return result;
} else {
- const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir;
+ const auto& workingDir = dir.isEmpty() ? config()->get(Config::LastDir).toString() : dir;
const auto result = QDir::toNativeSeparators(
- QFileDialog::getSaveFileName(parent, caption, workingDir, filter, selectedFilter, options));
+ QFileDialog::getSaveFileName(parent, caption, workingDir, modFilter(filter), selectedFilter, options));
#ifdef Q_OS_MACOS
// on Mac OS X the focus is lost after closing the native dialog
@@ -145,7 +134,7 @@ QString FileDialog::getExistingDirectory(QWidget* parent,
m_nextDirName.clear();
return result;
} else {
- const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir;
+ const auto& workingDir = dir.isEmpty() ? config()->get(Config::LastDir).toString() : dir;
const auto result =
QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, caption, workingDir, options));
@@ -187,7 +176,7 @@ FileDialog::FileDialog()
void FileDialog::saveLastDir(const QString& dir)
{
if (!dir.isEmpty() && !m_forgetLastDir) {
- config()->set("LastDir", QFileInfo(dir).absolutePath());
+ config()->set(Config::LastDir, QFileInfo(dir).absolutePath());
}
m_forgetLastDir = false;
diff --git a/src/gui/FileDialog.h b/src/gui/FileDialog.h
index 7d03d8046..4221a620a 100644
--- a/src/gui/FileDialog.h
+++ b/src/gui/FileDialog.h
@@ -37,13 +37,6 @@ public:
QString* selectedFilter = nullptr,
const QFileDialog::Options options = {});
- QString getFileName(QWidget* parent = nullptr,
- const QString& caption = QString(),
- const QString& dir = QString(),
- const QString& filter = QString(),
- QString* selectedFilter = nullptr,
- const QFileDialog::Options options = {});
-
QString getSaveFileName(QWidget* parent = nullptr,
const QString& caption = QString(),
const QString& dir = QString(),
diff --git a/src/gui/IconDownloaderDialog.cpp b/src/gui/IconDownloaderDialog.cpp
index ebe6980a2..1eb4b855c 100644
--- a/src/gui/IconDownloaderDialog.cpp
+++ b/src/gui/IconDownloaderDialog.cpp
@@ -28,7 +28,7 @@
#include "core/Tools.h"
#include "gui/IconModels.h"
#ifdef Q_OS_MACOS
-#include "gui/macutils/MacUtils.h"
+#include "gui/osutils/macutils/MacUtils.h"
#endif
#include <QMutexLocker>
@@ -159,7 +159,7 @@ void IconDownloaderDialog::downloadFinished(const QString& url, const QImage& ic
void IconDownloaderDialog::showFallbackMessage(bool state)
{
// Show fallback message if the option is not active
- bool show = state && !config()->get("security/IconDownloadFallback").toBool();
+ bool show = state && !config()->get(Config::Security_IconDownloadFallback).toBool();
m_ui->fallbackLabel->setVisible(show);
}
diff --git a/src/gui/IconDownloaderDialog.ui b/src/gui/IconDownloaderDialog.ui
index ed9fddd1e..fbcfb01f1 100644
--- a/src/gui/IconDownloaderDialog.ui
+++ b/src/gui/IconDownloaderDialog.ui
@@ -149,6 +149,11 @@ You can enable the DuckDuckGo website icon service in the security section of th
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
+ <tabstops>
+ <tabstop>cancelButton</tabstop>
+ <tabstop>tableView</tabstop>
+ <tabstop>closeButton</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/IconModels.cpp b/src/gui/IconModels.cpp
index 39732c502..3bdd9a5e6 100644
--- a/src/gui/IconModels.cpp
+++ b/src/gui/IconModels.cpp
@@ -29,7 +29,7 @@ DefaultIconModel::DefaultIconModel(QObject* parent)
int DefaultIconModel::rowCount(const QModelIndex& parent) const
{
if (!parent.isValid()) {
- return DatabaseIcons::IconCount;
+ return databaseIcons()->count();
} else {
return 0;
}
@@ -41,10 +41,10 @@ QVariant DefaultIconModel::data(const QModelIndex& index, int role) const
return QVariant();
}
- Q_ASSERT(index.row() < DatabaseIcons::IconCount);
+ Q_ASSERT(index.row() < databaseIcons()->count());
if (role == Qt::DecorationRole) {
- return databaseIcons()->iconPixmap(index.row());
+ return databaseIcons()->icon(index.row(), IconSize::Medium);
}
return QVariant();
@@ -101,7 +101,6 @@ QModelIndex CustomIconModel::indexFromUuid(const QUuid& uuid) const
int idx = m_iconsOrder.indexOf(uuid);
if (idx > -1) {
return index(idx, 0);
- } else {
- return QModelIndex();
}
+ return {};
}
diff --git a/src/gui/KMessageWidget.cpp b/src/gui/KMessageWidget.cpp
index 80f302858..5e11b354c 100644
--- a/src/gui/KMessageWidget.cpp
+++ b/src/gui/KMessageWidget.cpp
@@ -20,7 +20,7 @@
*/
#include "KMessageWidget.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "core/Global.h"
#include <QAction>
@@ -94,7 +94,7 @@ void KMessageWidgetPrivate::init(KMessageWidget *q_ptr)
QAction *closeAction = new QAction(q);
closeAction->setText(KMessageWidget::tr("&Close"));
closeAction->setToolTip(KMessageWidget::tr("Close message"));
- closeAction->setIcon(FilePath::instance()->icon("actions", "message-close", false));
+ closeAction->setIcon(Resources::instance()->icon("message-close"));
QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide()));
@@ -102,12 +102,6 @@ void KMessageWidgetPrivate::init(KMessageWidget *q_ptr)
closeButton->setAutoRaise(true);
closeButton->setDefaultAction(closeAction);
closeButtonPixmap = QPixmap(closeButton->icon().pixmap(closeButton->icon().actualSize(QSize(16, 16))));
-#ifdef Q_OS_MACOS
- closeButton->setStyleSheet("QToolButton { background: transparent;"
- "border-radius: 2px; padding: 3px; }"
- "QToolButton::hover, QToolButton::focus {"
- "border: 1px solid rgb(90, 200, 250); }");
-#endif
q->setMessageType(KMessageWidget::Information);
}
@@ -263,7 +257,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type)
{
d->messageType = type;
QColor bg0, bg1, bg2, border;
- QColor fg = palette().light().color();
+ QColor fg = QColor(238, 238, 238);
switch (type) {
case Positive:
bg1.setRgb(37, 163, 83);
@@ -273,7 +267,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type)
break;
case Warning:
bg1.setRgb(252, 193, 57);
- fg = palette().windowText().color();
+ fg = QColor(48, 48, 48);
break;
case Error:
bg1.setRgb(198, 69, 21);
@@ -294,9 +288,15 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type)
painter.fillRect(QRect(0, 0, 16, 16), fg);
painter.end();
d->closeButton->setIcon(closeButtonPixmap);
+ d->closeButton->setStyleSheet(QStringLiteral("QToolButton {"
+ " background: transparent;"
+ " border-radius: 2px;"
+ " border: none; }"
+ "QToolButton:hover, QToolButton:focus {"
+ " border: 1px solid %1; }").arg(fg.name()));
d->content->setStyleSheet(
- QString(QLatin1String(".QFrame {"
+ QStringLiteral(".QFrame {"
"background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,"
" stop: 0 %1,"
" stop: 0.1 %2,"
@@ -307,7 +307,7 @@ void KMessageWidget::setMessageType(KMessageWidget::MessageType type)
" padding: 5px;"
"}"
".QLabel { color: %6; }"
- ))
+ )
.arg(bg0.name(),
bg1.name(),
bg2.name(),
diff --git a/src/gui/KeePass1OpenWidget.cpp b/src/gui/KeePass1OpenWidget.cpp
index 6b369b9e5..d35edc2c6 100644
--- a/src/gui/KeePass1OpenWidget.cpp
+++ b/src/gui/KeePass1OpenWidget.cpp
@@ -37,16 +37,12 @@ void KeePass1OpenWidget::openDatabase()
KeePass1Reader reader;
QString password;
- QString keyFileName;
+ QString keyFileName = m_ui->keyFileLineEdit->text();
if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) {
password = m_ui->editPassword->text();
}
- if (!m_ui->comboKeyFile->currentText().isEmpty() && m_ui->comboKeyFile->currentData() != -1) {
- keyFileName = m_ui->comboKeyFile->currentText();
- }
-
QFile file(m_filename);
if (!file.open(QIODevice::ReadOnly)) {
m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(file.errorString()),
diff --git a/src/gui/LineEdit.cpp b/src/gui/LineEdit.cpp
index 4ad18fef5..ec5cb7f9c 100644
--- a/src/gui/LineEdit.cpp
+++ b/src/gui/LineEdit.cpp
@@ -22,7 +22,7 @@
#include <QStyle>
#include <QToolButton>
-#include "core/FilePath.h"
+#include "core/Resources.h"
LineEdit::LineEdit(QWidget* parent)
: QLineEdit(parent)
@@ -30,16 +30,10 @@ LineEdit::LineEdit(QWidget* parent)
{
m_clearButton->setObjectName("clearButton");
- QIcon icon;
QString iconNameDirected =
QString("edit-clear-locationbar-").append((layoutDirection() == Qt::LeftToRight) ? "rtl" : "ltr");
- icon = QIcon::fromTheme(iconNameDirected);
- if (icon.isNull()) {
- icon = QIcon::fromTheme("edit-clear");
- if (icon.isNull()) {
- icon = filePath()->icon("actions", iconNameDirected);
- }
- }
+
+ const auto icon = resources()->icon(iconNameDirected);
m_clearButton->setIcon(icon);
m_clearButton->setCursor(Qt::ArrowCursor);
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);
+ });
+}
diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h
index 89501eff3..95e8e5a8b 100644
--- a/src/gui/MainWindow.h
+++ b/src/gui/MainWindow.h
@@ -48,6 +48,8 @@ public:
MainWindow();
~MainWindow();
+ QList<DatabaseWidget*> getOpenDatabases();
+
enum StackedWidgetIndex
{
DatabaseTabScreen = 0,
@@ -70,22 +72,28 @@ public slots:
void hideGlobalMessage();
void showYubiKeyPopup();
void hideYubiKeyPopup();
+ void hide();
+ void show();
void hideWindow();
+ void minimizeOrHide();
void toggleWindow();
void bringToFront();
void closeAllDatabases();
void lockAllDatabases();
void displayDesktopNotification(const QString& msg, QString title = "", int msTimeoutHint = 10000);
+ void restartApp(const QString& message);
protected:
void closeEvent(QCloseEvent* event) override;
void changeEvent(QEvent* event) override;
+ bool focusNextPrevChild(bool next) override;
private slots:
void setMenuActionState(DatabaseWidget::Mode mode = DatabaseWidget::Mode::None);
+ void updateToolbarSeparatorVisibility();
void updateWindowTitle();
void showAboutDialog();
- void showUpdateCheckStartup();
+ void performUpdateCheck();
void showUpdateCheckDialog();
void focusWindowChanged(QWindow* focusWindow);
void hasUpdateAvailable(bool hasUpdate, const QString& version, bool isManuallyRequested);
@@ -97,14 +105,13 @@ private slots:
void openKeyboardShortcuts();
void switchToDatabases();
void switchToSettings(bool enabled);
- void switchToPasswordGen(bool enabled);
+ void togglePasswordGenerator(bool enabled);
void switchToNewDatabase();
void switchToOpenDatabase();
void switchToDatabaseFile(const QString& file);
void switchToKeePass1Database();
void switchToOpVaultDatabase();
void switchToCsvImport();
- void closePasswordGen();
void databaseStatusChanged(DatabaseWidget* dbWidget);
void databaseTabChanged(int tabIndex);
void openRecentDatabase(QAction* action);
@@ -122,10 +129,15 @@ private slots:
void showErrorMessage(const QString& message);
void selectNextDatabaseTab();
void selectPreviousDatabaseTab();
+ void selectDatabaseTab(int tabIndex, bool wrap = false);
void togglePasswordsHidden();
void toggleUsernamesHidden();
void obtainContextFocusLock();
void releaseContextFocusLock();
+ void agentEnabled(bool enabled);
+
+private slots:
+ void updateTrayIcon();
private:
static void setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback = 0);
@@ -134,7 +146,6 @@ private:
void saveWindowInformation();
bool saveLastDatabases();
- void updateTrayIcon();
bool isTrayIconEnabled() const;
void customOpenUrl(QString url);
@@ -142,6 +153,8 @@ private:
void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override;
+ void initViewMenu();
+
const QScopedPointer<Ui::MainWindow> m_ui;
SignalMultiplexer m_actionMultiplexer;
QPointer<QAction> m_clearHistoryAction;
@@ -161,8 +174,12 @@ private:
bool m_appExitCalled = false;
bool m_appExiting = false;
+ bool m_restartRequested = false;
bool m_contextMenuFocusLock = false;
- uint m_lastFocusOutTime = 0;
+ bool m_showToolbarSeparator = false;
+ qint64 m_lastFocusOutTime = 0;
+ qint64 m_lastShowTime = 0;
+ QTimer m_updateCheckTimer;
QTimer m_trayIconTriggerTimer;
QSystemTrayIcon::ActivationReason m_trayIconTriggerReason;
};
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index 068788ecb..10951f3c0 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -13,6 +13,12 @@
<height>600</height>
</rect>
</property>
+ <property name="minimumSize">
+ <size>
+ <width>800</width>
+ <height>0</height>
+ </size>
+ </property>
<property name="windowTitle">
<string notr="true">KeePassXC</string>
</property>
@@ -37,28 +43,23 @@
<number>0</number>
</property>
<item>
- <widget class="QWidget" name="globalMessageWidgetContainer" native="true">
- <layout class="QVBoxLayout" name="globalMessageWidgetLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="MessageWidget" name="globalMessageWidget" native="true">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="styleSheet">
- <string notr="true">MessageWidget {margin: 90px}</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="MessageWidget" name="globalMessageWidget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="toolbarSeparator">
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
</widget>
</item>
<item>
@@ -69,9 +70,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
- </property>
<property name="currentIndex">
<number>2</number>
</property>
@@ -119,11 +117,7 @@
<number>0</number>
</property>
<item>
- <widget class="ApplicationSettingsWidget" name="settingsWidget" native="true">
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
- </property>
- </widget>
+ <widget class="ApplicationSettingsWidget" name="settingsWidget" native="true"/>
</item>
</layout>
</widget>
@@ -148,11 +142,7 @@
</spacer>
</item>
<item>
- <widget class="WelcomeWidget" name="welcomeWidget" native="true">
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
- </property>
- </widget>
+ <widget class="WelcomeWidget" name="welcomeWidget" native="true"/>
</item>
<item>
<spacer name="horizontalSpacer_2">
@@ -176,12 +166,43 @@
</widget>
<widget class="QWidget" name="pagePasswordGenerator">
<layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>60</number>
+ </property>
+ <property name="topMargin">
+ <number>30</number>
+ </property>
+ <property name="rightMargin">
+ <number>60</number>
+ </property>
<item>
- <widget class="PasswordGeneratorWidget" name="passwordGeneratorWidget" native="true">
- <property name="focusPolicy">
- <enum>Qt::TabFocus</enum>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
</property>
- </widget>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="PasswordGeneratorWidget" name="passwordGeneratorWidget" native="true"/>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
</item>
</layout>
</widget>
@@ -195,12 +216,9 @@
<x>0</x>
<y>0</y>
<width>800</width>
- <height>21</height>
+ <height>22</height>
</rect>
</property>
- <property name="focusPolicy">
- <enum>Qt::NoFocus</enum>
- </property>
<property name="contextMenuPolicy">
<enum>Qt::PreventContextMenu</enum>
</property>
@@ -210,16 +228,16 @@
</property>
<widget class="QMenu" name="menuRecentDatabases">
<property name="title">
- <string>&amp;Recent databases</string>
+ <string>&amp;Recent Databases</string>
</property>
</widget>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>&amp;Import</string>
</property>
- <addaction name="actionImportKeePass1"/>
- <addaction name="actionImportOpVault"/>
<addaction name="actionImportCsv"/>
+ <addaction name="actionImportOpVault"/>
+ <addaction name="actionImportKeePass1"/>
</widget>
<widget class="QMenu" name="menuExport">
<property name="title">
@@ -233,10 +251,12 @@
<addaction name="menuRecentDatabases"/>
<addaction name="actionDatabaseSave"/>
<addaction name="actionDatabaseSaveAs"/>
+ <addaction name="actionDatabaseSaveBackup"/>
<addaction name="actionDatabaseClose"/>
<addaction name="separator"/>
- <addaction name="actionChangeMasterKey"/>
- <addaction name="actionChangeDatabaseSettings"/>
+ <addaction name="actionReports"/>
+ <addaction name="actionDatabaseSettings"/>
+ <addaction name="actionDatabaseSecurity"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="menuImport"/>
@@ -248,7 +268,6 @@
<property name="title">
<string>&amp;Help</string>
</property>
- <addaction name="actionAbout"/>
<addaction name="separator"/>
<addaction name="actionGettingStarted"/>
<addaction name="actionUserGuide"/>
@@ -258,10 +277,11 @@
<addaction name="actionCheckForUpdates"/>
<addaction name="actionDonate"/>
<addaction name="actionBugReport"/>
+ <addaction name="actionAbout"/>
</widget>
<widget class="QMenu" name="menuEntries">
<property name="title">
- <string>E&amp;ntries</string>
+ <string>&amp;Entries</string>
</property>
<widget class="QMenu" name="menuEntryCopyAttribute">
<property name="enabled">
@@ -271,7 +291,7 @@
<string/>
</property>
<property name="title">
- <string>Copy att&amp;ribute...</string>
+ <string>Copy Att&amp;ribute</string>
</property>
<addaction name="actionEntryCopyTitle"/>
<addaction name="actionEntryCopyURL"/>
@@ -283,7 +303,7 @@
<bool>false</bool>
</property>
<property name="title">
- <string>TOTP...</string>
+ <string>TOTP</string>
</property>
<addaction name="actionEntryCopyTotp"/>
<addaction name="actionEntryTotp"/>
@@ -295,6 +315,9 @@
<addaction name="actionEntryClone"/>
<addaction name="actionEntryDelete"/>
<addaction name="separator"/>
+ <addaction name="actionEntryMoveUp"/>
+ <addaction name="actionEntryMoveDown"/>
+ <addaction name="separator"/>
<addaction name="actionEntryCopyUsername"/>
<addaction name="actionEntryCopyPassword"/>
<addaction name="menuEntryCopyAttribute"/>
@@ -304,13 +327,15 @@
<addaction name="separator"/>
<addaction name="actionEntryOpenUrl"/>
<addaction name="actionEntryDownloadIcon"/>
+ <addaction name="separator"/>
+ <addaction name="actionEntryAddToAgent"/>
+ <addaction name="actionEntryRemoveFromAgent"/>
</widget>
<widget class="QMenu" name="menuGroups">
<property name="title">
<string>&amp;Groups</string>
</property>
<addaction name="actionGroupNew"/>
- <addaction name="separator"/>
<addaction name="actionGroupEdit"/>
<addaction name="actionGroupDelete"/>
<addaction name="actionGroupEmptyRecycleBin"/>
@@ -328,16 +353,32 @@
<addaction name="actionPasswordGenerator"/>
<addaction name="actionSettings"/>
</widget>
+ <widget class="QMenu" name="menuView">
+ <property name="title">
+ <string>View</string>
+ </property>
+ <widget class="QMenu" name="menuTheme">
+ <property name="title">
+ <string>Theme</string>
+ </property>
+ <addaction name="actionThemeAuto"/>
+ <addaction name="actionThemeLight"/>
+ <addaction name="actionThemeDark"/>
+ <addaction name="actionThemeClassic"/>
+ </widget>
+ <addaction name="menuTheme"/>
+ <addaction name="actionCompactMode"/>
+ <addaction name="actionShowPreviewPanel"/>
+ <addaction name="actionShowToolbar"/>
+ </widget>
<addaction name="menuFile"/>
<addaction name="menuEntries"/>
<addaction name="menuGroups"/>
<addaction name="menuTools"/>
+ <addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="toolBar">
- <property name="focusPolicy">
- <enum>Qt::NoFocus</enum>
- </property>
<property name="contextMenuPolicy">
<enum>Qt::PreventContextMenu</enum>
</property>
@@ -346,8 +387,8 @@
</property>
<property name="iconSize">
<size>
- <width>22</width>
- <height>22</height>
+ <width>26</width>
+ <height>26</height>
</size>
</property>
<attribute name="toolBarArea">
@@ -356,7 +397,6 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
- <addaction name="actionDatabaseNew"/>
<addaction name="actionDatabaseOpen"/>
<addaction name="actionDatabaseSave"/>
<addaction name="separator"/>
@@ -369,9 +409,9 @@
<addaction name="actionEntryCopyURL"/>
<addaction name="actionEntryAutoType"/>
<addaction name="separator"/>
- <addaction name="actionPasswordGenerator"/>
<addaction name="actionLockDatabases"/>
<addaction name="separator"/>
+ <addaction name="actionPasswordGenerator"/>
<addaction name="actionSettings"/>
<addaction name="separator"/>
</widget>
@@ -393,7 +433,7 @@
</action>
<action name="actionCheckForUpdates">
<property name="text">
- <string>&amp;Check for Updates...</string>
+ <string>&amp;Check for Updates</string>
</property>
<property name="menuRole">
<enum>QAction::ApplicationSpecificRole</enum>
@@ -401,7 +441,7 @@
</action>
<action name="actionDatabaseOpen">
<property name="text">
- <string>&amp;Open database...</string>
+ <string>&amp;Open Database…</string>
</property>
</action>
<action name="actionDatabaseSave">
@@ -409,7 +449,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Save database</string>
+ <string>&amp;Save Database</string>
</property>
</action>
<action name="actionDatabaseClose">
@@ -417,12 +457,12 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Close database</string>
+ <string>&amp;Close Database</string>
</property>
</action>
<action name="actionDatabaseNew">
<property name="text">
- <string>&amp;New database...</string>
+ <string>&amp;New Database…</string>
</property>
<property name="toolTip">
<string>Create a new database</string>
@@ -430,7 +470,7 @@
</action>
<action name="actionDatabaseMerge">
<property name="text">
- <string>&amp;Merge from database...</string>
+ <string>&amp;Merge From Database…</string>
</property>
<property name="toolTip">
<string>Merge from another KDBX database</string>
@@ -441,7 +481,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;New entry</string>
+ <string>&amp;New Entry…</string>
</property>
<property name="toolTip">
<string>Add a new entry</string>
@@ -452,7 +492,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Edit entry</string>
+ <string>&amp;Edit Entry…</string>
</property>
<property name="toolTip">
<string>View or edit entry</string>
@@ -463,7 +503,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Delete entry</string>
+ <string>&amp;Delete Entry…</string>
</property>
</action>
<action name="actionGroupNew">
@@ -471,7 +511,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;New group</string>
+ <string>&amp;New Group…</string>
</property>
<property name="toolTip">
<string>Add a new group</string>
@@ -482,7 +522,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Edit group</string>
+ <string>&amp;Edit Group…</string>
</property>
</action>
<action name="actionGroupDelete">
@@ -490,7 +530,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Delete group</string>
+ <string>&amp;Delete Group…</string>
</property>
</action>
<action name="actionGroupDownloadFavicons">
@@ -498,7 +538,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>Downlo&amp;ad all favicons</string>
+ <string>Download All &amp;Favicons…</string>
</property>
</action>
<action name="actionGroupSortAsc">
@@ -522,23 +562,37 @@
<bool>false</bool>
</property>
<property name="text">
- <string>Sa&amp;ve database as...</string>
+ <string>Sa&amp;ve Database As…</string>
</property>
</action>
- <action name="actionChangeMasterKey">
+ <action name="actionDatabaseSecurity">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
- <string>Change master &amp;key...</string>
+ <string>Database &amp;Security…</string>
</property>
</action>
- <action name="actionChangeDatabaseSettings">
+ <action name="actionReports">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Database settings...</string>
+ <string>Database &amp;Reports...</string>
+ </property>
+ <property name="toolTip">
+ <string>Statistics, health check, etc.</string>
+ </property>
+ <property name="menuRole">
+ <enum>QAction::NoRole</enum>
+ </property>
+ </action>
+ <action name="actionDatabaseSettings">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>&amp;Database Settings…</string>
</property>
<property name="toolTip">
<string>Database settings</string>
@@ -552,7 +606,29 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Clone entry</string>
+ <string>&amp;Clone Entry…</string>
+ </property>
+ </action>
+ <action name="actionEntryMoveUp">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Move u&amp;p</string>
+ </property>
+ <property name="toolTip">
+ <string>Move entry one step up</string>
+ </property>
+ </action>
+ <action name="actionEntryMoveDown">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Move do&amp;wn</string>
+ </property>
+ <property name="toolTip">
+ <string>Move entry one step down</string>
</property>
</action>
<action name="actionEntryCopyUsername">
@@ -560,7 +636,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>Copy &amp;username</string>
+ <string>Copy &amp;Username</string>
</property>
<property name="toolTip">
<string>Copy username to clipboard</string>
@@ -571,7 +647,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>Copy &amp;password</string>
+ <string>Copy &amp;Password</string>
</property>
<property name="toolTip">
<string>Copy password to clipboard</string>
@@ -606,7 +682,7 @@
</action>
<action name="actionEntryDownloadIcon">
<property name="text">
- <string>Download favicon</string>
+ <string>Download &amp;Favicon</string>
</property>
</action>
<action name="actionEntryOpenUrl">
@@ -622,7 +698,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Lock databases</string>
+ <string>&amp;Lock Databases</string>
</property>
</action>
<action name="actionEntryCopyTitle">
@@ -663,7 +739,7 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Export to CSV file...</string>
+ <string>&amp;CSV File…</string>
</property>
</action>
<action name="actionExportHtml">
@@ -671,12 +747,12 @@
<bool>false</bool>
</property>
<property name="text">
- <string>&amp;Export to HTML file...</string>
+ <string>&amp;HTML File…</string>
</property>
</action>
<action name="actionImportKeePass1">
<property name="text">
- <string>KeePass 1 database...</string>
+ <string>KeePass 1 Database…</string>
</property>
<property name="toolTip">
<string>Import a KeePass 1 database</string>
@@ -684,7 +760,7 @@
</action>
<action name="actionImportOpVault">
<property name="text">
- <string>1Password Vault...</string>
+ <string>1Password Vault…</string>
</property>
<property name="toolTip">
<string>Import a 1Password Vault</string>
@@ -692,7 +768,7 @@
</action>
<action name="actionImportCsv">
<property name="text">
- <string>CSV file...</string>
+ <string>CSV File…</string>
</property>
<property name="toolTip">
<string>Import a CSV file</string>
@@ -700,17 +776,17 @@
</action>
<action name="actionEntryTotp">
<property name="text">
- <string>Show TOTP...</string>
+ <string>Show TOTP</string>
</property>
</action>
<action name="actionEntryTotpQRCode">
<property name="text">
- <string>Show TOTP QR Code...</string>
+ <string>Show QR Code</string>
</property>
</action>
<action name="actionEntrySetupTotp">
<property name="text">
- <string>Set up TOTP...</string>
+ <string>Set up TOTP…</string>
</property>
</action>
<action name="actionEntryCopyTotp">
@@ -733,7 +809,7 @@
</action>
<action name="actionBugReport">
<property name="text">
- <string>Report a &amp;bug</string>
+ <string>Report a &amp;Bug</string>
</property>
</action>
<action name="actionGettingStarted">
@@ -741,15 +817,15 @@
<string>&amp;Getting Started</string>
</property>
<property name="toolTip">
- <string>Open Getting Started Guide PDF</string>
+ <string>Open Getting Started Guide</string>
</property>
</action>
<action name="actionOnlineHelp">
<property name="text">
- <string>&amp;Online Help...</string>
+ <string>&amp;Online Help</string>
</property>
<property name="toolTip">
- <string>Go to online documentation (opens browser)</string>
+ <string>Go to online documentation</string>
</property>
</action>
<action name="actionUserGuide">
@@ -757,7 +833,7 @@
<string>&amp;User Guide</string>
</property>
<property name="toolTip">
- <string>Open User Guide PDF</string>
+ <string>Open User Guide</string>
</property>
</action>
<action name="actionKeyboardShortcuts">
@@ -768,6 +844,89 @@
<string notr="true">Ctrl+/</string>
</property>
</action>
+ <action name="actionDatabaseSaveBackup">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Save Database Backup...</string>
+ </property>
+ </action>
+ <action name="actionEntryAddToAgent">
+ <property name="text">
+ <string>Add key to SSH Agent</string>
+ </property>
+ </action>
+ <action name="actionEntryRemoveFromAgent">
+ <property name="text">
+ <string>Remove key from SSH Agent</string>
+ </property>
+ </action>
+ <action name="actionCompactMode">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Compact Mode</string>
+ </property>
+ </action>
+ <action name="actionThemeAuto">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Automatic</string>
+ </property>
+ </action>
+ <action name="actionThemeLight">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Light</string>
+ </property>
+ </action>
+ <action name="actionThemeDark">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Dark</string>
+ </property>
+ </action>
+ <action name="actionThemeClassic">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Classic (Platform-native)</string>
+ </property>
+ </action>
+ <action name="actionShowToolbar">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Show Toolbar</string>
+ </property>
+ </action>
+ <action name="actionShowPreviewPanel">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Show Preview Panel</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/src/gui/MessageBox.cpp b/src/gui/MessageBox.cpp
index 7d2b2a516..317754a62 100644
--- a/src/gui/MessageBox.cpp
+++ b/src/gui/MessageBox.cpp
@@ -98,10 +98,10 @@ MessageBox::Button MessageBox::messageBox(QWidget* parent,
for (uint64_t b = First; b <= Last; b <<= 1) {
if (b & buttons) {
- QString text = m_buttonDefs[static_cast<Button>(b)].first;
+ QString buttonText = m_buttonDefs[static_cast<Button>(b)].first;
QMessageBox::ButtonRole role = m_buttonDefs[static_cast<Button>(b)].second;
- auto buttonPtr = msgBox.addButton(text, role);
+ auto buttonPtr = msgBox.addButton(buttonText, role);
m_addedButtonLookup.insert(buttonPtr, static_cast<Button>(b));
}
}
diff --git a/src/gui/MessageWidget.cpp b/src/gui/MessageWidget.cpp
index 4b7e67a22..494a81542 100644
--- a/src/gui/MessageWidget.cpp
+++ b/src/gui/MessageWidget.cpp
@@ -36,6 +36,11 @@ MessageWidget::MessageWidget(QWidget* parent)
connect(this, SIGNAL(hideAnimationFinished()), m_autoHideTimer, SLOT(stop()));
}
+void MessageWidget::setAnimate(bool state)
+{
+ m_animate = state;
+}
+
int MessageWidget::autoHideTimeout() const
{
return m_autoHideTimeout;
@@ -50,8 +55,15 @@ void MessageWidget::showMessage(const QString& text, KMessageWidget::MessageType
{
setMessageType(type);
setText(text);
+
emit showAnimationStarted();
- animatedShow();
+ if (m_animate) {
+ animatedShow();
+ } else {
+ show();
+ emit showAnimationFinished();
+ }
+
if (autoHideTimeout > 0) {
m_autoHideTimer->start(autoHideTimeout);
} else {
@@ -61,7 +73,14 @@ void MessageWidget::showMessage(const QString& text, KMessageWidget::MessageType
void MessageWidget::hideMessage()
{
- animatedHide();
+ emit hideAnimationStarted();
+ if (m_animate) {
+ animatedHide();
+ } else {
+ hide();
+ emit hideAnimationFinished();
+ }
+
m_autoHideTimer->stop();
}
diff --git a/src/gui/MessageWidget.h b/src/gui/MessageWidget.h
index fe4baec4a..2c8c9a217 100644
--- a/src/gui/MessageWidget.h
+++ b/src/gui/MessageWidget.h
@@ -36,8 +36,11 @@ public:
static const int LongAutoHideTimeout;
static const int DisableAutoHide;
+ void setAnimate(bool state);
+
signals:
void showAnimationStarted();
+ void hideAnimationStarted();
public slots:
void showMessage(const QString& text, MessageWidget::MessageType type);
@@ -49,6 +52,7 @@ public slots:
private:
QTimer* m_autoHideTimer;
int m_autoHideTimeout;
+ bool m_animate;
};
#endif // MESSAGEWIDGET_H
diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp
index 37b82ad8b..487db8768 100644
--- a/src/gui/PasswordEdit.cpp
+++ b/src/gui/PasswordEdit.cpp
@@ -19,91 +19,142 @@
#include "PasswordEdit.h"
#include "core/Config.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "gui/Font.h"
+#include "gui/PasswordGeneratorWidget.h"
+#include "gui/osutils/OSUtils.h"
+#include "gui/styles/StateColorPalette.h"
-const QColor PasswordEdit::CorrectSoFarColor = QColor(255, 205, 15);
-const QColor PasswordEdit::ErrorColor = QColor(255, 125, 125);
+#include <QDialog>
+#include <QTimer>
+#include <QToolTip>
+#include <QVBoxLayout>
PasswordEdit::PasswordEdit(QWidget* parent)
: QLineEdit(parent)
- , m_basePasswordEdit(nullptr)
{
- const QIcon errorIcon = filePath()->icon("status", "dialog-error");
+ const QIcon errorIcon = resources()->icon("dialog-error");
m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition);
m_errorAction->setVisible(false);
m_errorAction->setToolTip(tr("Passwords do not match"));
- const QIcon correctIcon = filePath()->icon("actions", "dialog-ok");
+ const QIcon correctIcon = resources()->icon("dialog-ok");
m_correctAction = addAction(correctIcon, QLineEdit::TrailingPosition);
m_correctAction->setVisible(false);
m_correctAction->setToolTip(tr("Passwords match so far"));
setEchoMode(QLineEdit::Password);
- updateStylesheet();
// use a monospace font for the password field
QFont passwordFont = Font::fixedFont();
passwordFont.setLetterSpacing(QFont::PercentageSpacing, 110);
setFont(passwordFont);
+
+ m_toggleVisibleAction = new QAction(
+ resources()->icon("password-show-off"),
+ tr("Toggle Password (%1)").arg(QKeySequence(Qt::CTRL + Qt::Key_H).toString(QKeySequence::NativeText)),
+ nullptr);
+ m_toggleVisibleAction->setCheckable(true);
+ m_toggleVisibleAction->setShortcut(Qt::CTRL + Qt::Key_H);
+ m_toggleVisibleAction->setShortcutContext(Qt::WidgetShortcut);
+ addAction(m_toggleVisibleAction, QLineEdit::TrailingPosition);
+ connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordEdit::setShowPassword);
+
+ m_passwordGeneratorAction = new QAction(
+ resources()->icon("password-generator"),
+ tr("Generate Password (%1)").arg(QKeySequence(Qt::CTRL + Qt::Key_G).toString(QKeySequence::NativeText)),
+ nullptr);
+ m_passwordGeneratorAction->setShortcut(Qt::CTRL + Qt::Key_G);
+ m_passwordGeneratorAction->setShortcutContext(Qt::WidgetShortcut);
+ addAction(m_passwordGeneratorAction, QLineEdit::TrailingPosition);
+ m_passwordGeneratorAction->setVisible(false);
+
+ m_capslockAction =
+ new QAction(resources()->icon("dialog-warning", true, StateColorPalette().color(StateColorPalette::Error)),
+ tr("Warning: Caps Lock enabled!"),
+ nullptr);
+ addAction(m_capslockAction, QLineEdit::LeadingPosition);
+ m_capslockAction->setVisible(false);
}
-void PasswordEdit::enableVerifyMode(PasswordEdit* basePasswordEdit)
+void PasswordEdit::setRepeatPartner(PasswordEdit* repeatEdit)
{
- m_basePasswordEdit = basePasswordEdit;
+ m_repeatPasswordEdit = repeatEdit;
+ m_repeatPasswordEdit->setParentPasswordEdit(this);
- updateStylesheet();
+ connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(autocompletePassword(QString)));
+ connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
+ connect(m_repeatPasswordEdit, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
+}
- connect(m_basePasswordEdit, SIGNAL(textChanged(QString)), SLOT(autocompletePassword(QString)));
- connect(m_basePasswordEdit, SIGNAL(textChanged(QString)), SLOT(updateStylesheet()));
- connect(this, SIGNAL(textChanged(QString)), SLOT(updateStylesheet()));
+void PasswordEdit::setParentPasswordEdit(PasswordEdit* parent)
+{
+ m_parentPasswordEdit = parent;
+ // Hide actions
+ m_toggleVisibleAction->setVisible(false);
+ m_passwordGeneratorAction->setVisible(false);
+}
- connect(m_basePasswordEdit, SIGNAL(showPasswordChanged(bool)), SLOT(setShowPassword(bool)));
+void PasswordEdit::enablePasswordGenerator()
+{
+ if (!m_passwordGeneratorAction->isVisible()) {
+ m_passwordGeneratorAction->setVisible(true);
+ connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::popupPasswordGenerator);
+ }
}
void PasswordEdit::setShowPassword(bool show)
{
setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password);
- // if I have a parent, I'm the child
- if (m_basePasswordEdit) {
- if (config()->get("security/passwordsrepeat").toBool()) {
- setEnabled(!show);
- setReadOnly(show);
- setText(m_basePasswordEdit->text());
+ m_toggleVisibleAction->setIcon(resources()->icon(show ? "password-show-on" : "password-show-off"));
+ m_toggleVisibleAction->setChecked(show);
+
+ if (m_repeatPasswordEdit) {
+ m_repeatPasswordEdit->setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password);
+ if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool()) {
+ m_repeatPasswordEdit->setEnabled(!show);
+ m_repeatPasswordEdit->setText(text());
} else {
- // This fix a bug when the QLineEdit is disabled while switching config
- if (!isEnabled()) {
- setEnabled(true);
- setReadOnly(false);
- }
+ m_repeatPasswordEdit->setEnabled(true);
}
}
- updateStylesheet();
- emit showPasswordChanged(show);
}
bool PasswordEdit::isPasswordVisible() const
{
- return isEnabled();
+ return echoMode() == QLineEdit::Normal;
}
-bool PasswordEdit::passwordsEqual() const
+void PasswordEdit::popupPasswordGenerator()
{
- return text() == m_basePasswordEdit->text();
+ auto generator = PasswordGeneratorWidget::popupGenerator(this);
+ generator->setPasswordVisible(isPasswordVisible());
+ generator->setPasswordLength(text().length());
+
+ connect(generator, SIGNAL(appliedPassword(QString)), SLOT(setText(QString)));
+ if (m_repeatPasswordEdit) {
+ connect(generator, SIGNAL(appliedPassword(QString)), m_repeatPasswordEdit, SLOT(setText(QString)));
+ }
}
-void PasswordEdit::updateStylesheet()
+void PasswordEdit::updateRepeatStatus()
{
- const QString stylesheetTemplate("QLineEdit { background: %1; }");
+ static const auto stylesheetTemplate = QStringLiteral("QLineEdit { background: %1; }");
+ if (!m_parentPasswordEdit) {
+ return;
+ }
- if (m_basePasswordEdit && !passwordsEqual()) {
- bool isCorrect = true;
- if (m_basePasswordEdit->text().startsWith(text())) {
- setStyleSheet(stylesheetTemplate.arg(CorrectSoFarColor.name()));
- } else {
- setStyleSheet(stylesheetTemplate.arg(ErrorColor.name()));
- isCorrect = false;
+ const auto otherPassword = m_parentPasswordEdit->text();
+ const auto password = text();
+ if (otherPassword != password) {
+ bool isCorrect = false;
+ StateColorPalette statePalette;
+ QColor color = statePalette.color(StateColorPalette::ColorRole::Error);
+ if (!password.isEmpty() && otherPassword.startsWith(password)) {
+ color = statePalette.color(StateColorPalette::ColorRole::Incomplete);
+ isCorrect = true;
}
+ setStyleSheet(stylesheetTemplate.arg(color.name()));
m_correctAction->setVisible(isCorrect);
m_errorAction->setVisible(!isCorrect);
} else {
@@ -115,7 +166,38 @@ void PasswordEdit::updateStylesheet()
void PasswordEdit::autocompletePassword(const QString& password)
{
- if (config()->get("security/passwordsrepeat").toBool() && echoMode() == QLineEdit::Normal) {
+ if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool() && echoMode() == QLineEdit::Normal) {
setText(password);
}
}
+
+bool PasswordEdit::event(QEvent* event)
+{
+ if (isVisible()) {
+ checkCapslockState();
+ }
+ return QLineEdit::event(event);
+}
+
+void PasswordEdit::checkCapslockState()
+{
+ if (m_parentPasswordEdit) {
+ return;
+ }
+
+ bool newCapslockState = osUtils->isCapslockEnabled();
+ if (newCapslockState != m_capslockState) {
+ m_capslockState = newCapslockState;
+ m_capslockAction->setVisible(newCapslockState);
+
+ // Force repaint to avoid rendering glitches of QLineEdit contents
+ repaint();
+
+ emit capslockToggled(m_capslockState);
+
+ if (newCapslockState) {
+ QTimer::singleShot(
+ 150, [this]() { QToolTip::showText(mapToGlobal(rect().bottomLeft()), m_capslockAction->text()); });
+ }
+ }
+}
diff --git a/src/gui/PasswordEdit.h b/src/gui/PasswordEdit.h
index b6e74ed00..559394bd0 100644
--- a/src/gui/PasswordEdit.h
+++ b/src/gui/PasswordEdit.h
@@ -23,34 +23,43 @@
#include <QLineEdit>
#include <QPointer>
+class QDialog;
+
class PasswordEdit : public QLineEdit
{
Q_OBJECT
public:
- static const QColor CorrectSoFarColor;
- static const QColor ErrorColor;
-
explicit PasswordEdit(QWidget* parent = nullptr);
- void enableVerifyMode(PasswordEdit* baseEdit);
+ void enablePasswordGenerator();
+ void setRepeatPartner(PasswordEdit* repeatEdit);
bool isPasswordVisible() const;
public slots:
void setShowPassword(bool show);
+ void updateRepeatStatus();
+
+protected:
+ bool event(QEvent* event) override;
signals:
- void showPasswordChanged(bool show);
+ void capslockToggled(bool capslockOn);
private slots:
- void updateStylesheet();
void autocompletePassword(const QString& password);
+ void popupPasswordGenerator();
+ void setParentPasswordEdit(PasswordEdit* parent);
+ void checkCapslockState();
private:
- bool passwordsEqual() const;
-
QPointer<QAction> m_errorAction;
QPointer<QAction> m_correctAction;
- QPointer<PasswordEdit> m_basePasswordEdit;
+ QPointer<QAction> m_toggleVisibleAction;
+ QPointer<QAction> m_passwordGeneratorAction;
+ QPointer<QAction> m_capslockAction;
+ QPointer<PasswordEdit> m_repeatPasswordEdit;
+ QPointer<PasswordEdit> m_parentPasswordEdit;
+ bool m_capslockState = false;
};
#endif // KEEPASSX_PASSWORDEDIT_H
diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp
index e0f8fbe5f..e1c15310f 100644
--- a/src/gui/PasswordGeneratorWidget.cpp
+++ b/src/gui/PasswordGeneratorWidget.cpp
@@ -24,37 +24,52 @@
#include <QLineEdit>
#include "core/Config.h"
-#include "core/FilePath.h"
#include "core/PasswordGenerator.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
#include "gui/Clipboard.h"
+#include "gui/styles/StateColorPalette.h"
PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
: QWidget(parent)
- , m_updatingSpinBox(false)
, m_passwordGenerator(new PasswordGenerator())
, m_dicewareGenerator(new PassphraseGenerator())
, m_ui(new Ui::PasswordGeneratorWidget())
{
m_ui->setupUi(this);
- m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
+ m_ui->buttonGenerate->setIcon(resources()->icon("refresh"));
+ m_ui->buttonGenerate->setToolTip(
+ tr("Regenerate password (%1)").arg(m_ui->buttonGenerate->shortcut().toString(QKeySequence::NativeText)));
+ m_ui->buttonCopy->setIcon(resources()->icon("clipboard-text"));
+ m_ui->buttonClose->setShortcut(Qt::Key_Escape);
+
+ m_ui->clearInclude->setIcon(resources()->icon("edit-clear-locationbar-rtl"));
+ m_ui->editAdditionalChars->addAction(m_ui->clearInclude, QLineEdit::TrailingPosition);
+ m_ui->clearInclude->setVisible(false);
+
+ m_ui->clearExclude->setIcon(resources()->icon("edit-clear-locationbar-rtl"));
+ m_ui->editExcludedChars->addAction(m_ui->clearExclude, QLineEdit::TrailingPosition);
+ m_ui->clearExclude->setVisible(false);
connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updateButtonsEnabled(QString)));
connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updatePasswordStrength(QString)));
- connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), SLOT(setPasswordVisible(bool)));
- connect(m_ui->buttonSimpleMode, SIGNAL(clicked()), SLOT(selectSimpleMode()));
- connect(m_ui->buttonAdvancedMode, SIGNAL(clicked()), SLOT(selectAdvancedMode()));
+ connect(m_ui->buttonAdvancedMode, SIGNAL(toggled(bool)), SLOT(setAdvancedMode(bool)));
connect(m_ui->buttonAddHex, SIGNAL(clicked()), SLOT(excludeHexChars()));
+ connect(m_ui->editAdditionalChars, SIGNAL(textChanged(QString)), SLOT(updateGenerator()));
connect(m_ui->editExcludedChars, SIGNAL(textChanged(QString)), SLOT(updateGenerator()));
connect(m_ui->buttonApply, SIGNAL(clicked()), SLOT(applyPassword()));
connect(m_ui->buttonCopy, SIGNAL(clicked()), SLOT(copyPassword()));
connect(m_ui->buttonGenerate, SIGNAL(clicked()), SLOT(regeneratePassword()));
+ connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed()));
+ connect(m_ui->clearInclude, SIGNAL(triggered(bool)), m_ui->editAdditionalChars, SLOT(clear()));
+ connect(m_ui->clearExclude, SIGNAL(triggered(bool)), m_ui->editExcludedChars, SLOT(clear()));
- connect(m_ui->sliderLength, SIGNAL(valueChanged(int)), SLOT(passwordSliderMoved()));
- connect(m_ui->spinBoxLength, SIGNAL(valueChanged(int)), SLOT(passwordSpinBoxChanged()));
+ connect(m_ui->sliderLength, SIGNAL(valueChanged(int)), SLOT(passwordLengthChanged(int)));
+ connect(m_ui->spinBoxLength, SIGNAL(valueChanged(int)), SLOT(passwordLengthChanged(int)));
- connect(m_ui->sliderWordCount, SIGNAL(valueChanged(int)), SLOT(dicewareSliderMoved()));
- connect(m_ui->spinBoxWordCount, SIGNAL(valueChanged(int)), SLOT(dicewareSpinBoxChanged()));
+ connect(m_ui->sliderWordCount, SIGNAL(valueChanged(int)), SLOT(passphraseLengthChanged(int)));
+ connect(m_ui->spinBoxWordCount, SIGNAL(valueChanged(int)), SLOT(passphraseLengthChanged(int)));
connect(m_ui->editWordSeparator, SIGNAL(textChanged(QString)), SLOT(updateGenerator()));
connect(m_ui->comboBoxWordList, SIGNAL(currentIndexChanged(int)), SLOT(updateGenerator()));
@@ -80,7 +95,7 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
m_ui->wordCaseComboBox->addItem(tr("UPPER CASE"), PassphraseGenerator::UPPERCASE);
m_ui->wordCaseComboBox->addItem(tr("Title Case"), PassphraseGenerator::TITLECASE);
- QDir path(filePath()->wordlistPath(""));
+ QDir path(resources()->wordlistPath(""));
QStringList files = path.entryList(QDir::Files);
m_ui->comboBoxWordList->addItems(files);
if (files.size() > 1) {
@@ -98,125 +113,116 @@ PasswordGeneratorWidget::~PasswordGeneratorWidget()
{
}
-void PasswordGeneratorWidget::showEvent(QShowEvent* event)
+PasswordGeneratorWidget* PasswordGeneratorWidget::popupGenerator(QWidget* parent)
{
- QWidget::showEvent(event);
- reset();
+ auto pwGenerator = new PasswordGeneratorWidget(parent);
+ pwGenerator->setWindowModality(Qt::ApplicationModal);
+ pwGenerator->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+ pwGenerator->setStandaloneMode(false);
+
+ connect(pwGenerator, SIGNAL(closed()), pwGenerator, SLOT(deleteLater()));
+
+ pwGenerator->show();
+ pwGenerator->raise();
+ pwGenerator->activateWindow();
+ pwGenerator->adjustSize();
+
+ return pwGenerator;
}
void PasswordGeneratorWidget::loadSettings()
{
// Password config
- m_ui->checkBoxLower->setChecked(config()->get("generator/LowerCase", PasswordGenerator::DefaultLower).toBool());
- m_ui->checkBoxLowerAdv->setChecked(config()->get("generator/LowerCase", PasswordGenerator::DefaultLower).toBool());
- m_ui->checkBoxUpper->setChecked(config()->get("generator/UpperCase", PasswordGenerator::DefaultUpper).toBool());
- m_ui->checkBoxUpperAdv->setChecked(config()->get("generator/UpperCase", PasswordGenerator::DefaultUpper).toBool());
- m_ui->checkBoxNumbers->setChecked(config()->get("generator/Numbers", PasswordGenerator::DefaultNumbers).toBool());
- m_ui->checkBoxSpecialChars->setChecked(
- config()->get("generator/SpecialChars", PasswordGenerator::DefaultSpecial).toBool());
- m_ui->checkBoxNumbersAdv->setChecked(
- config()->get("generator/Numbers", PasswordGenerator::DefaultNumbers).toBool());
- m_ui->advancedBar->setVisible(
- config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool());
- m_ui->excludedChars->setVisible(
- config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool());
- m_ui->checkBoxExcludeAlike->setVisible(
- config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool());
- m_ui->checkBoxEnsureEvery->setVisible(
- config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool());
- m_ui->editExcludedChars->setText(
- config()->get("generator/ExcludedChars", PasswordGenerator::DefaultExcludedChars).toString());
-
- m_ui->simpleBar->setVisible(
- !(config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool()));
- m_ui->checkBoxBraces->setChecked(config()->get("generator/Braces", PasswordGenerator::DefaultBraces).toBool());
- m_ui->checkBoxQuotes->setChecked(config()->get("generator/Quotes", PasswordGenerator::DefaultQuotes).toBool());
- m_ui->checkBoxPunctuation->setChecked(
- config()->get("generator/Punctuation", PasswordGenerator::DefaultPunctuation).toBool());
- m_ui->checkBoxDashes->setChecked(config()->get("generator/Dashes", PasswordGenerator::DefaultDashes).toBool());
- m_ui->checkBoxMath->setChecked(config()->get("generator/Math", PasswordGenerator::DefaultMath).toBool());
- m_ui->checkBoxLogograms->setChecked(
- config()->get("generator/Logograms", PasswordGenerator::DefaultLogograms).toBool());
- m_ui->checkBoxExtASCII->setChecked(config()->get("generator/EASCII", PasswordGenerator::DefaultEASCII).toBool());
- m_ui->checkBoxExtASCIIAdv->setChecked(config()->get("generator/EASCII", PasswordGenerator::DefaultEASCII).toBool());
- m_ui->checkBoxExcludeAlike->setChecked(
- config()->get("generator/ExcludeAlike", PasswordGenerator::DefaultLookAlike).toBool());
- m_ui->checkBoxEnsureEvery->setChecked(
- config()->get("generator/EnsureEvery", PasswordGenerator::DefaultFromEveryGroup).toBool());
- m_ui->spinBoxLength->setValue(config()->get("generator/Length", PasswordGenerator::DefaultLength).toInt());
+ m_ui->checkBoxLower->setChecked(config()->get(Config::PasswordGenerator_LowerCase).toBool());
+ m_ui->checkBoxLowerAdv->setChecked(config()->get(Config::PasswordGenerator_LowerCase).toBool());
+ m_ui->checkBoxUpper->setChecked(config()->get(Config::PasswordGenerator_UpperCase).toBool());
+ m_ui->checkBoxUpperAdv->setChecked(config()->get(Config::PasswordGenerator_UpperCase).toBool());
+ m_ui->checkBoxNumbers->setChecked(config()->get(Config::PasswordGenerator_Numbers).toBool());
+ m_ui->checkBoxSpecialChars->setChecked(config()->get(Config::PasswordGenerator_SpecialChars).toBool());
+ m_ui->checkBoxNumbersAdv->setChecked(config()->get(Config::PasswordGenerator_Numbers).toBool());
+ m_ui->editAdditionalChars->setText(config()->get(Config::PasswordGenerator_AdditionalChars).toString());
+ m_ui->editExcludedChars->setText(config()->get(Config::PasswordGenerator_ExcludedChars).toString());
+
+ m_ui->buttonAdvancedMode->setChecked(config()->get(Config::PasswordGenerator_AdvancedMode).toBool());
+ setAdvancedMode(m_ui->buttonAdvancedMode->isChecked());
+
+ m_ui->checkBoxBraces->setChecked(config()->get(Config::PasswordGenerator_Braces).toBool());
+ m_ui->checkBoxQuotes->setChecked(config()->get(Config::PasswordGenerator_Quotes).toBool());
+ m_ui->checkBoxPunctuation->setChecked(config()->get(Config::PasswordGenerator_Punctuation).toBool());
+ m_ui->checkBoxDashes->setChecked(config()->get(Config::PasswordGenerator_Dashes).toBool());
+ m_ui->checkBoxMath->setChecked(config()->get(Config::PasswordGenerator_Math).toBool());
+ m_ui->checkBoxLogograms->setChecked(config()->get(Config::PasswordGenerator_Logograms).toBool());
+ m_ui->checkBoxExtASCII->setChecked(config()->get(Config::PasswordGenerator_EASCII).toBool());
+ m_ui->checkBoxExtASCIIAdv->setChecked(config()->get(Config::PasswordGenerator_EASCII).toBool());
+ m_ui->checkBoxExcludeAlike->setChecked(config()->get(Config::PasswordGenerator_ExcludeAlike).toBool());
+ m_ui->checkBoxEnsureEvery->setChecked(config()->get(Config::PasswordGenerator_EnsureEvery).toBool());
+ m_ui->spinBoxLength->setValue(config()->get(Config::PasswordGenerator_Length).toInt());
// Diceware config
- m_ui->spinBoxWordCount->setValue(
- config()->get("generator/WordCount", PassphraseGenerator::DefaultWordCount).toInt());
- m_ui->editWordSeparator->setText(
- config()->get("generator/WordSeparator", PassphraseGenerator::DefaultSeparator).toString());
- m_ui->comboBoxWordList->setCurrentText(
- config()->get("generator/WordList", PassphraseGenerator::DefaultWordList).toString());
- m_ui->wordCaseComboBox->setCurrentIndex(config()->get("generator/WordCase", 0).toInt());
+ m_ui->spinBoxWordCount->setValue(config()->get(Config::PasswordGenerator_WordCount).toInt());
+ m_ui->editWordSeparator->setText(config()->get(Config::PasswordGenerator_WordSeparator).toString());
+ m_ui->comboBoxWordList->setCurrentText(config()->get(Config::PasswordGenerator_WordList).toString());
+ m_ui->wordCaseComboBox->setCurrentIndex(config()->get(Config::PasswordGenerator_WordCase).toInt());
// Password or diceware?
- m_ui->tabWidget->setCurrentIndex(config()->get("generator/Type", 0).toInt());
+ m_ui->tabWidget->setCurrentIndex(config()->get(Config::PasswordGenerator_Type).toInt());
}
void PasswordGeneratorWidget::saveSettings()
{
// Password config
if (m_ui->simpleBar->isVisible()) {
- config()->set("generator/LowerCase", m_ui->checkBoxLower->isChecked());
- config()->set("generator/UpperCase", m_ui->checkBoxUpper->isChecked());
- config()->set("generator/Numbers", m_ui->checkBoxNumbers->isChecked());
- config()->set("generator/EASCII", m_ui->checkBoxExtASCII->isChecked());
+ config()->set(Config::PasswordGenerator_LowerCase, m_ui->checkBoxLower->isChecked());
+ config()->set(Config::PasswordGenerator_UpperCase, m_ui->checkBoxUpper->isChecked());
+ config()->set(Config::PasswordGenerator_Numbers, m_ui->checkBoxNumbers->isChecked());
+ config()->set(Config::PasswordGenerator_EASCII, m_ui->checkBoxExtASCII->isChecked());
} else {
- config()->set("generator/LowerCase", m_ui->checkBoxLowerAdv->isChecked());
- config()->set("generator/UpperCase", m_ui->checkBoxUpperAdv->isChecked());
- config()->set("generator/Numbers", m_ui->checkBoxNumbersAdv->isChecked());
- config()->set("generator/EASCII", m_ui->checkBoxExtASCIIAdv->isChecked());
+ config()->set(Config::PasswordGenerator_LowerCase, m_ui->checkBoxLowerAdv->isChecked());
+ config()->set(Config::PasswordGenerator_UpperCase, m_ui->checkBoxUpperAdv->isChecked());
+ config()->set(Config::PasswordGenerator_Numbers, m_ui->checkBoxNumbersAdv->isChecked());
+ config()->set(Config::PasswordGenerator_EASCII, m_ui->checkBoxExtASCIIAdv->isChecked());
}
- config()->set("generator/AdvancedMode", m_ui->advancedBar->isVisible());
- config()->set("generator/SpecialChars", m_ui->checkBoxSpecialChars->isChecked());
- config()->set("generator/Braces", m_ui->checkBoxBraces->isChecked());
- config()->set("generator/Punctuation", m_ui->checkBoxPunctuation->isChecked());
- config()->set("generator/Quotes", m_ui->checkBoxQuotes->isChecked());
- config()->set("generator/Dashes", m_ui->checkBoxDashes->isChecked());
- config()->set("generator/Math", m_ui->checkBoxMath->isChecked());
- config()->set("generator/Logograms", m_ui->checkBoxLogograms->isChecked());
- config()->set("generator/ExcludedChars", m_ui->editExcludedChars->text());
- config()->set("generator/ExcludeAlike", m_ui->checkBoxExcludeAlike->isChecked());
- config()->set("generator/EnsureEvery", m_ui->checkBoxEnsureEvery->isChecked());
- config()->set("generator/Length", m_ui->spinBoxLength->value());
+ config()->set(Config::PasswordGenerator_AdvancedMode, m_ui->buttonAdvancedMode->isChecked());
+ config()->set(Config::PasswordGenerator_SpecialChars, m_ui->checkBoxSpecialChars->isChecked());
+ config()->set(Config::PasswordGenerator_Braces, m_ui->checkBoxBraces->isChecked());
+ config()->set(Config::PasswordGenerator_Punctuation, m_ui->checkBoxPunctuation->isChecked());
+ config()->set(Config::PasswordGenerator_Quotes, m_ui->checkBoxQuotes->isChecked());
+ config()->set(Config::PasswordGenerator_Dashes, m_ui->checkBoxDashes->isChecked());
+ config()->set(Config::PasswordGenerator_Math, m_ui->checkBoxMath->isChecked());
+ config()->set(Config::PasswordGenerator_Logograms, m_ui->checkBoxLogograms->isChecked());
+ config()->set(Config::PasswordGenerator_AdditionalChars, m_ui->editAdditionalChars->text());
+ config()->set(Config::PasswordGenerator_ExcludedChars, m_ui->editExcludedChars->text());
+ config()->set(Config::PasswordGenerator_ExcludeAlike, m_ui->checkBoxExcludeAlike->isChecked());
+ config()->set(Config::PasswordGenerator_EnsureEvery, m_ui->checkBoxEnsureEvery->isChecked());
+ config()->set(Config::PasswordGenerator_Length, m_ui->spinBoxLength->value());
// Diceware config
- config()->set("generator/WordCount", m_ui->spinBoxWordCount->value());
- config()->set("generator/WordSeparator", m_ui->editWordSeparator->text());
- config()->set("generator/WordList", m_ui->comboBoxWordList->currentText());
- config()->set("generator/WordCase", m_ui->wordCaseComboBox->currentIndex());
+ config()->set(Config::PasswordGenerator_WordCount, m_ui->spinBoxWordCount->value());
+ config()->set(Config::PasswordGenerator_WordSeparator, m_ui->editWordSeparator->text());
+ config()->set(Config::PasswordGenerator_WordList, m_ui->comboBoxWordList->currentText());
+ config()->set(Config::PasswordGenerator_WordCase, m_ui->wordCaseComboBox->currentIndex());
// Password or diceware?
- config()->set("generator/Type", m_ui->tabWidget->currentIndex());
+ config()->set(Config::PasswordGenerator_Type, m_ui->tabWidget->currentIndex());
}
-void PasswordGeneratorWidget::reset(int length)
+void PasswordGeneratorWidget::setPasswordLength(int length)
{
- m_ui->editNewPassword->setText("");
if (length > 0) {
m_ui->spinBoxLength->setValue(length);
} else {
- m_ui->spinBoxLength->setValue(config()->get("generator/Length", PasswordGenerator::DefaultLength).toInt());
+ m_ui->spinBoxLength->setValue(config()->get(Config::PasswordGenerator_Length).toInt());
}
-
- setStandaloneMode(false);
- setPasswordVisible(config()->get("security/passwordscleartext").toBool());
- updateGenerator();
}
void PasswordGeneratorWidget::setStandaloneMode(bool standalone)
{
m_standalone = standalone;
if (standalone) {
- m_ui->buttonApply->setText(tr("Close"));
+ m_ui->buttonApply->setVisible(false);
setPasswordVisible(true);
} else {
- m_ui->buttonApply->setText(tr("Accept"));
+ m_ui->buttonApply->setVisible(true);
}
}
@@ -225,15 +231,6 @@ QString PasswordGeneratorWidget::getGeneratedPassword()
return m_ui->editNewPassword->text();
}
-void PasswordGeneratorWidget::keyPressEvent(QKeyEvent* e)
-{
- if (e->key() == Qt::Key_Escape && m_standalone) {
- emit dialogTerminated();
- } else {
- e->ignore();
- }
-}
-
void PasswordGeneratorWidget::regeneratePassword()
{
if (m_ui->tabWidget->currentIndex() == Password) {
@@ -261,28 +258,24 @@ void PasswordGeneratorWidget::updateButtonsEnabled(const QString& password)
void PasswordGeneratorWidget::updatePasswordStrength(const QString& password)
{
- double entropy = 0.0;
- if (m_ui->tabWidget->currentIndex() == Password) {
- entropy = m_passwordGenerator->estimateEntropy(password);
- } else {
- entropy = m_dicewareGenerator->estimateEntropy();
+ PasswordHealth health(password);
+ if (m_ui->tabWidget->currentIndex() == Diceware) {
+ // Diceware estimates entropy differently
+ health = PasswordHealth(m_dicewareGenerator->estimateEntropy());
}
- m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(entropy, 'f', 2)));
+ m_ui->entropyLabel->setText(tr("Entropy: %1 bit").arg(QString::number(health.entropy(), 'f', 2)));
- if (entropy > m_ui->entropyProgressBar->maximum()) {
- entropy = m_ui->entropyProgressBar->maximum();
- }
- m_ui->entropyProgressBar->setValue(entropy);
+ m_ui->entropyProgressBar->setValue(std::min(int(health.entropy()), m_ui->entropyProgressBar->maximum()));
- colorStrengthIndicator(entropy);
+ colorStrengthIndicator(health);
}
void PasswordGeneratorWidget::applyPassword()
{
saveSettings();
emit appliedPassword(m_ui->editNewPassword->text());
- emit dialogTerminated();
+ emit closed();
}
void PasswordGeneratorWidget::copyPassword()
@@ -290,43 +283,30 @@ void PasswordGeneratorWidget::copyPassword()
clipboard()->setText(m_ui->editNewPassword->text());
}
-void PasswordGeneratorWidget::passwordSliderMoved()
+void PasswordGeneratorWidget::passwordLengthChanged(int length)
{
- if (m_updatingSpinBox) {
- return;
- }
+ m_ui->spinBoxLength->blockSignals(true);
+ m_ui->sliderLength->blockSignals(true);
- m_ui->spinBoxLength->setValue(m_ui->sliderLength->value());
+ m_ui->spinBoxLength->setValue(length);
+ m_ui->sliderLength->setValue(length);
- updateGenerator();
-}
-
-void PasswordGeneratorWidget::passwordSpinBoxChanged()
-{
- if (m_updatingSpinBox) {
- return;
- }
-
- // Interlock so that we don't update twice - this causes issues as the spinbox can go higher than slider
- m_updatingSpinBox = true;
-
- m_ui->sliderLength->setValue(m_ui->spinBoxLength->value());
-
- m_updatingSpinBox = false;
+ m_ui->spinBoxLength->blockSignals(false);
+ m_ui->sliderLength->blockSignals(false);
updateGenerator();
}
-void PasswordGeneratorWidget::dicewareSliderMoved()
+void PasswordGeneratorWidget::passphraseLengthChanged(int length)
{
- m_ui->spinBoxWordCount->setValue(m_ui->sliderWordCount->value());
+ m_ui->spinBoxWordCount->blockSignals(true);
+ m_ui->sliderWordCount->blockSignals(true);
- updateGenerator();
-}
+ m_ui->spinBoxWordCount->setValue(length);
+ m_ui->sliderWordCount->setValue(length);
-void PasswordGeneratorWidget::dicewareSpinBoxChanged()
-{
- m_ui->sliderWordCount->setValue(m_ui->spinBoxWordCount->value());
+ m_ui->spinBoxWordCount->blockSignals(false);
+ m_ui->sliderWordCount->blockSignals(false);
updateGenerator();
}
@@ -334,49 +314,43 @@ void PasswordGeneratorWidget::dicewareSpinBoxChanged()
void PasswordGeneratorWidget::setPasswordVisible(bool visible)
{
m_ui->editNewPassword->setShowPassword(visible);
- bool blockSignals = m_ui->togglePasswordButton->blockSignals(true);
- m_ui->togglePasswordButton->setChecked(visible);
- m_ui->togglePasswordButton->blockSignals(blockSignals);
}
bool PasswordGeneratorWidget::isPasswordVisible() const
{
- return m_ui->togglePasswordButton->isChecked();
+ return m_ui->editNewPassword->isPasswordVisible();
}
-void PasswordGeneratorWidget::selectSimpleMode()
+void PasswordGeneratorWidget::setAdvancedMode(bool state)
{
- m_ui->advancedBar->hide();
- m_ui->excludedChars->hide();
- m_ui->checkBoxExcludeAlike->hide();
- m_ui->checkBoxEnsureEvery->hide();
- m_ui->checkBoxUpper->setChecked(m_ui->checkBoxUpperAdv->isChecked());
- m_ui->checkBoxLower->setChecked(m_ui->checkBoxLowerAdv->isChecked());
- m_ui->checkBoxNumbers->setChecked(m_ui->checkBoxNumbersAdv->isChecked());
- m_ui->checkBoxSpecialChars->setChecked(m_ui->checkBoxBraces->isChecked() | m_ui->checkBoxPunctuation->isChecked()
- | m_ui->checkBoxQuotes->isChecked() | m_ui->checkBoxMath->isChecked()
- | m_ui->checkBoxDashes->isChecked() | m_ui->checkBoxLogograms->isChecked());
- m_ui->checkBoxExtASCII->setChecked(m_ui->checkBoxExtASCIIAdv->isChecked());
- m_ui->simpleBar->show();
-}
+ if (state) {
+ m_ui->simpleBar->hide();
+ m_ui->advancedContainer->show();
+ m_ui->checkBoxUpperAdv->setChecked(m_ui->checkBoxUpper->isChecked());
+ m_ui->checkBoxLowerAdv->setChecked(m_ui->checkBoxLower->isChecked());
+ m_ui->checkBoxNumbersAdv->setChecked(m_ui->checkBoxNumbers->isChecked());
+ m_ui->checkBoxBraces->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxPunctuation->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxQuotes->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxMath->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxDashes->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxLogograms->setChecked(m_ui->checkBoxSpecialChars->isChecked());
+ m_ui->checkBoxExtASCIIAdv->setChecked(m_ui->checkBoxExtASCII->isChecked());
+ } else {
+ m_ui->simpleBar->show();
+ m_ui->advancedContainer->hide();
+ m_ui->checkBoxUpper->setChecked(m_ui->checkBoxUpperAdv->isChecked());
+ m_ui->checkBoxLower->setChecked(m_ui->checkBoxLowerAdv->isChecked());
+ m_ui->checkBoxNumbers->setChecked(m_ui->checkBoxNumbersAdv->isChecked());
+ m_ui->checkBoxSpecialChars->setChecked(
+ m_ui->checkBoxBraces->isChecked() | m_ui->checkBoxPunctuation->isChecked()
+ | m_ui->checkBoxQuotes->isChecked() | m_ui->checkBoxMath->isChecked() | m_ui->checkBoxDashes->isChecked()
+ | m_ui->checkBoxLogograms->isChecked());
+ m_ui->checkBoxExtASCII->setChecked(m_ui->checkBoxExtASCIIAdv->isChecked());
+ }
-void PasswordGeneratorWidget::selectAdvancedMode()
-{
- m_ui->simpleBar->hide();
- m_ui->checkBoxUpperAdv->setChecked(m_ui->checkBoxUpper->isChecked());
- m_ui->checkBoxLowerAdv->setChecked(m_ui->checkBoxLower->isChecked());
- m_ui->checkBoxNumbersAdv->setChecked(m_ui->checkBoxNumbers->isChecked());
- m_ui->checkBoxBraces->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxPunctuation->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxQuotes->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxMath->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxDashes->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxLogograms->setChecked(m_ui->checkBoxSpecialChars->isChecked());
- m_ui->checkBoxExtASCIIAdv->setChecked(m_ui->checkBoxExtASCII->isChecked());
- m_ui->advancedBar->show();
- m_ui->excludedChars->show();
- m_ui->checkBoxExcludeAlike->show();
- m_ui->checkBoxEnsureEvery->show();
+ QApplication::processEvents();
+ adjustSize();
}
void PasswordGeneratorWidget::excludeHexChars()
@@ -384,7 +358,7 @@ void PasswordGeneratorWidget::excludeHexChars()
m_ui->editExcludedChars->setText("GHIJKLMNOPQRSTUVWXYZghijklmnopqrstuvwxyz");
}
-void PasswordGeneratorWidget::colorStrengthIndicator(double entropy)
+void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& health)
{
// Take the existing stylesheet and convert the text and background color to arguments
QString style = m_ui->entropyProgressBar->styleSheet();
@@ -392,21 +366,30 @@ void PasswordGeneratorWidget::colorStrengthIndicator(double entropy)
QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption);
style.replace(re, "\\1 %1;");
- // Set the color and background based on entropy
- // colors are taking from the KDE breeze palette
- // <https://community.kde.org/KDE_Visual_Design_Group/HIG/Color>
- if (entropy < 40) {
- m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b"));
+ StateColorPalette statePalette;
+ switch (health.quality()) {
+ case PasswordHealth::Quality::Bad:
+ case PasswordHealth::Quality::Poor:
+ m_ui->entropyProgressBar->setStyleSheet(
+ style.arg(statePalette.color(StateColorPalette::HealthCritical).name()));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality")));
- } else if (entropy >= 40 && entropy < 65) {
- m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f"));
+ break;
+
+ case PasswordHealth::Quality::Weak:
+ m_ui->entropyProgressBar->setStyleSheet(style.arg(statePalette.color(StateColorPalette::HealthBad).name()));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality")));
- } else if (entropy >= 65 && entropy < 100) {
- m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116"));
+ break;
+
+ case PasswordHealth::Quality::Good:
+ m_ui->entropyProgressBar->setStyleSheet(style.arg(statePalette.color(StateColorPalette::HealthOk).name()));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality")));
- } else {
- m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60"));
+ break;
+
+ case PasswordHealth::Quality::Excellent:
+ m_ui->entropyProgressBar->setStyleSheet(
+ style.arg(statePalette.color(StateColorPalette::HealthExcellent).name()));
m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality")));
+ break;
}
}
@@ -483,12 +466,14 @@ PasswordGenerator::GeneratorFlags PasswordGeneratorWidget::generatorFlags()
{
PasswordGenerator::GeneratorFlags flags;
- if (m_ui->checkBoxExcludeAlike->isChecked()) {
- flags |= PasswordGenerator::ExcludeLookAlike;
- }
+ if (m_ui->buttonAdvancedMode->isChecked()) {
+ if (m_ui->checkBoxExcludeAlike->isChecked()) {
+ flags |= PasswordGenerator::ExcludeLookAlike;
+ }
- if (m_ui->checkBoxEnsureEvery->isChecked()) {
- flags |= PasswordGenerator::CharFromEveryGroup;
+ if (m_ui->checkBoxEnsureEvery->isChecked()) {
+ flags |= PasswordGenerator::CharFromEveryGroup;
+ }
}
return flags;
@@ -497,87 +482,70 @@ PasswordGenerator::GeneratorFlags PasswordGeneratorWidget::generatorFlags()
void PasswordGeneratorWidget::updateGenerator()
{
if (m_ui->tabWidget->currentIndex() == Password) {
- PasswordGenerator::CharClasses classes = charClasses();
- PasswordGenerator::GeneratorFlags flags = generatorFlags();
+ auto classes = charClasses();
+ auto flags = generatorFlags();
- int minLength = 0;
+ int length = 0;
if (flags.testFlag(PasswordGenerator::CharFromEveryGroup)) {
if (classes.testFlag(PasswordGenerator::LowerLetters)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::UpperLetters)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Numbers)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Braces)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Punctuation)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Quotes)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Dashes)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Math)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::Logograms)) {
- minLength++;
+ ++length;
}
if (classes.testFlag(PasswordGenerator::EASCII)) {
- minLength++;
+ ++length;
}
}
- minLength = qMax(minLength, 1);
-
- if (m_ui->spinBoxLength->value() < minLength) {
- m_updatingSpinBox = true;
- m_ui->spinBoxLength->setValue(minLength);
- m_ui->sliderLength->setValue(minLength);
- m_updatingSpinBox = false;
- }
-
- m_ui->spinBoxLength->setMinimum(minLength);
- m_ui->sliderLength->setMinimum(minLength);
- m_passwordGenerator->setLength(m_ui->spinBoxLength->value());
+ length = qMax(length, m_ui->spinBoxLength->value());
+ m_passwordGenerator->setLength(length);
m_passwordGenerator->setCharClasses(classes);
- if (m_ui->simpleBar->isVisible()) {
- m_passwordGenerator->setExcludedChars("");
- } else {
+ m_passwordGenerator->setFlags(flags);
+ if (m_ui->buttonAdvancedMode->isChecked()) {
+ m_passwordGenerator->setAdditionalChars(m_ui->editAdditionalChars->text());
m_passwordGenerator->setExcludedChars(m_ui->editExcludedChars->text());
+ } else {
+ m_passwordGenerator->setAdditionalChars("");
+ m_passwordGenerator->setExcludedChars("");
}
- m_passwordGenerator->setFlags(flags);
if (m_passwordGenerator->isValid()) {
m_ui->buttonGenerate->setEnabled(true);
} else {
m_ui->buttonGenerate->setEnabled(false);
}
- } else {
- int minWordCount = 1;
-
- if (m_ui->spinBoxWordCount->value() < minWordCount) {
- m_updatingSpinBox = true;
- m_ui->spinBoxWordCount->setValue(minWordCount);
- m_ui->sliderWordCount->setValue(minWordCount);
- m_updatingSpinBox = false;
- }
+ m_ui->clearInclude->setVisible(!m_ui->editAdditionalChars->text().isEmpty());
+ m_ui->clearExclude->setVisible(!m_ui->editExcludedChars->text().isEmpty());
+ } else {
m_dicewareGenerator->setWordCase(
static_cast<PassphraseGenerator::PassphraseWordCase>(m_ui->wordCaseComboBox->currentData().toInt()));
- m_ui->spinBoxWordCount->setMinimum(minWordCount);
- m_ui->sliderWordCount->setMinimum(minWordCount);
-
m_dicewareGenerator->setWordCount(m_ui->spinBoxWordCount->value());
if (!m_ui->comboBoxWordList->currentText().isEmpty()) {
- QString path = filePath()->wordlistPath(m_ui->comboBoxWordList->currentText());
+ QString path = resources()->wordlistPath(m_ui->comboBoxWordList->currentText());
m_dicewareGenerator->setWordList(path);
}
m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text());
diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h
index b39a2f10f..08409ed22 100644
--- a/src/gui/PasswordGeneratorWidget.h
+++ b/src/gui/PasswordGeneratorWidget.h
@@ -32,6 +32,7 @@ namespace Ui
}
class PasswordGenerator;
+class PasswordHealth;
class PassphraseGenerator;
class PasswordGeneratorWidget : public QWidget
@@ -48,13 +49,12 @@ public:
~PasswordGeneratorWidget();
void loadSettings();
void saveSettings();
- void reset(int length = 0);
+ void setPasswordLength(int length);
void setStandaloneMode(bool standalone);
QString getGeneratedPassword();
bool isPasswordVisible() const;
-protected:
- void showEvent(QShowEvent* event) override;
+ static PasswordGeneratorWidget* popupGenerator(QWidget* parent = nullptr);
public slots:
void regeneratePassword();
@@ -64,25 +64,21 @@ public slots:
signals:
void appliedPassword(const QString& password);
- void dialogTerminated();
+ void closed();
private slots:
void updateButtonsEnabled(const QString& password);
void updatePasswordStrength(const QString& password);
- void selectSimpleMode();
- void selectAdvancedMode();
+ void setAdvancedMode(bool state);
void excludeHexChars();
- void passwordSliderMoved();
- void passwordSpinBoxChanged();
- void dicewareSliderMoved();
- void dicewareSpinBoxChanged();
- void colorStrengthIndicator(double entropy);
+ void passwordLengthChanged(int length);
+ void passphraseLengthChanged(int length);
+ void colorStrengthIndicator(const PasswordHealth& health);
void updateGenerator();
private:
- bool m_updatingSpinBox;
bool m_standalone = false;
PasswordGenerator::CharClasses charClasses();
@@ -91,9 +87,6 @@ private:
const QScopedPointer<PasswordGenerator> m_passwordGenerator;
const QScopedPointer<PassphraseGenerator> m_dicewareGenerator;
const QScopedPointer<Ui::PasswordGeneratorWidget> m_ui;
-
-protected:
- void keyPressEvent(QKeyEvent* e) override;
};
#endif // KEEPASSX_PASSWORDGENERATORWIDGET_H
diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui
index a30077015..17b2432e5 100644
--- a/src/gui/PasswordGeneratorWidget.ui
+++ b/src/gui/PasswordGeneratorWidget.ui
@@ -6,95 +6,17 @@
<rect>
<x>0</x>
<y>0</y>
- <width>716</width>
- <height>468</height>
+ <width>622</width>
+ <height>455</height>
</rect>
</property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
<property name="windowTitle">
- <string/>
+ <string>Generate Password</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0,0">
<item>
<layout class="QGridLayout" name="passwordFieldLayout">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <property name="verticalSpacing">
- <number>0</number>
- </property>
- <item row="1" column="1">
- <widget class="QProgressBar" name="entropyProgressBar">
- <property name="minimumSize">
- <size>
- <width>50</width>
- <height>5</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>5</height>
- </size>
- </property>
- <property name="styleSheet">
- <string notr="true">QProgressBar {
- border: none;
- height: 2px;
- font-size: 1px;
- background-color: transparent;
- padding: 0 1px;
-}
-QProgressBar::chunk {
- background-color: #c0392b;
- border-radius: 2px;
-}</string>
- </property>
- <property name="maximum">
- <number>200</number>
- </property>
- <property name="value">
- <number>100</number>
- </property>
- <property name="textVisible">
- <bool>false</bool>
- </property>
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="invertedAppearance">
- <bool>false</bool>
- </property>
- <property name="textDirection">
- <enum>QProgressBar::TopToBottom</enum>
- </property>
- <property name="format">
- <string>%p%</string>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="labelNewPassword">
- <property name="text">
- <string>Password:</string>
- </property>
- <property name="buddy">
- <cstring>editNewPassword</cstring>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
+ <item row="2" column="0">
<layout class="QHBoxLayout" name="passwordStrengthTextLayout">
<item>
<widget class="QLabel" name="strengthLabel">
@@ -139,6 +61,12 @@ QProgressBar::chunk {
</item>
<item>
<widget class="QLabel" name="entropyLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
<property name="minimumSize">
<size>
<width>70</width>
@@ -158,1047 +86,1094 @@ QProgressBar::chunk {
</item>
</layout>
</item>
- <item row="0" column="1">
+ <item row="0" column="0">
<widget class="PasswordEdit" name="editNewPassword">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>450</width>
+ <height>0</height>
+ </size>
+ </property>
<property name="accessibleName">
<string>Generated password</string>
</property>
- <property name="maxLength">
- <number>999</number>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QProgressBar" name="entropyProgressBar">
+ <property name="minimumSize">
+ <size>
+ <width>50</width>
+ <height>5</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>5</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QProgressBar {
+ border: none;
+ height: 2px;
+ font-size: 1px;
+ background-color: transparent;
+ padding: 0 1px;
+}
+QProgressBar::chunk {
+ background-color: #c0392b;
+ border-radius: 2px;
+}</string>
+ </property>
+ <property name="maximum">
+ <number>200</number>
+ </property>
+ <property name="value">
+ <number>100</number>
+ </property>
+ <property name="textVisible">
+ <bool>false</bool>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="invertedAppearance">
+ <bool>false</bool>
+ </property>
+ <property name="textDirection">
+ <enum>QProgressBar::TopToBottom</enum>
+ </property>
+ <property name="format">
+ <string>%p%</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QToolButton" name="buttonGenerate">
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="accessibleDescription">
+ <string>Regenerate password</string>
+ </property>
+ <property name="shortcut">
+ <string notr="true">Ctrl+R</string>
</property>
</widget>
</item>
<item row="0" column="2">
- <widget class="QToolButton" name="togglePasswordButton">
- <property name="accessibleName">
- <string>Toggle password visibility</string>
+ <widget class="QToolButton" name="buttonCopy">
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Copy password</string>
</property>
<property name="accessibleDescription">
- <string/>
+ <string>Copy password</string>
</property>
- <property name="checkable">
- <bool>true</bool>
+ <property name="shortcut">
+ <string notr="true">Ctrl+C</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="tabPosition">
- <enum>QTabWidget::North</enum>
- </property>
- <property name="tabShape">
- <enum>QTabWidget::Rounded</enum>
- </property>
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="passwordWidget">
- <attribute name="title">
- <string>Password</string>
- </attribute>
- <layout class="QGridLayout" name="gridLayout">
- <item row="1" column="0">
- <layout class="QHBoxLayout" name="optionsLayout">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
+ <property name="tabPosition">
+ <enum>QTabWidget::North</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="passwordWidget">
+ <attribute name="title">
+ <string>Password</string>
+ </attribute>
+ <layout class="QGridLayout" name="_2">
+ <item row="0" column="0">
+ <layout class="QHBoxLayout" name="passwordLengthSliderLayout">
+ <property name="spacing">
+ <number>15</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="labelLength">
+ <property name="text">
+ <string>&amp;Length:</string>
</property>
- <item>
- <widget class="QGroupBox" name="groupBox">
- <property name="minimumSize">
- <size>
- <width>580</width>
- <height>0</height>
- </size>
- </property>
- <property name="title">
- <string>Character Types</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QWidget" name="simpleBar" native="true">
- <layout class="QHBoxLayout" name="horizontalLayout_4">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <layout class="QHBoxLayout" name="alphabetLayout" stretch="0,0,0,0,0,0,0">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxUpper">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Upper-case letters</string>
- </property>
- <property name="accessibleName">
- <string>Upper-case letters</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string notr="true">A-Z</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxLower">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Lower-case letters</string>
- </property>
- <property name="accessibleName">
- <string>Lower-case letters</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string notr="true">a-z</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxNumbers">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Numbers</string>
- </property>
- <property name="accessibleName">
- <string>Numbers</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string notr="true">0-9</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxSpecialChars">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Special characters</string>
- </property>
- <property name="accessibleName">
- <string>Special characters</string>
- </property>
- <property name="text">
- <string notr="true">/*_&amp; ...</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxExtASCII">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Extended ASCII</string>
- </property>
- <property name="accessibleName">
- <string>Extended ASCII</string>
- </property>
- <property name="text">
- <string>ExtendedASCII</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="buttonAdvancedMode">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Switch to advanced mode</string>
- </property>
- <property name="text">
- <string>Advanced</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="advancedBar" native="true">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout_5">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxUpperAdv">
- <property name="minimumSize">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Upper-case letters</string>
- </property>
- <property name="accessibleName">
- <string>Upper-case letters</string>
- </property>
- <property name="text">
- <string>A-Z</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxLowerAdv">
- <property name="minimumSize">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Lower-case letters</string>
- </property>
- <property name="accessibleName">
- <string>Lower-case letters</string>
- </property>
- <property name="text">
- <string>a-z</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_6">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxNumbersAdv">
- <property name="minimumSize">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Numbers</string>
- </property>
- <property name="text">
- <string>0-9</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxBraces">
- <property name="minimumSize">
- <size>
- <width>40</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Braces</string>
- </property>
- <property name="accessibleName">
- <string>Braces</string>
- </property>
- <property name="text">
- <string>{[(</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_7">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxPunctuation">
- <property name="minimumSize">
- <size>
- <width>35</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Punctuation</string>
- </property>
- <property name="accessibleName">
- <string>Punctuation</string>
- </property>
- <property name="text">
- <string>.,:;</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxQuotes">
- <property name="minimumSize">
- <size>
- <width>35</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Quotes</string>
- </property>
- <property name="text">
- <string>&quot; '</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxMath">
- <property name="minimumSize">
- <size>
- <width>60</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Math Symbols</string>
- </property>
- <property name="accessibleName">
- <string>Math Symbols</string>
- </property>
- <property name="text">
- <string>&lt;*+!?=</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxDashes">
- <property name="minimumSize">
- <size>
- <width>60</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Dashes and Slashes</string>
- </property>
- <property name="accessibleName">
- <string>Dashes and Slashes</string>
- </property>
- <property name="text">
- <string>\_|-/</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_9">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item>
- <widget class="QToolButton" name="checkBoxLogograms">
- <property name="minimumSize">
- <size>
- <width>105</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Logograms</string>
- </property>
- <property name="accessibleName">
- <string>Logograms</string>
- </property>
- <property name="text">
- <string>#$%&amp;&amp;@^`~</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="checkBoxExtASCIIAdv">
- <property name="minimumSize">
- <size>
- <width>105</width>
- <height>25</height>
- </size>
- </property>
- <property name="focusPolicy">
- <enum>Qt::StrongFocus</enum>
- </property>
- <property name="toolTip">
- <string>Extended ASCII</string>
- </property>
- <property name="text">
- <string>ExtendedASCII</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <spacer name="horizontalSpacer_3">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_10">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
- </property>
- <item alignment="Qt::AlignTop">
- <widget class="QPushButton" name="buttonSimpleMode">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Switch to simple mode</string>
- </property>
- <property name="text">
- <string>Simple</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <widget class="QWidget" name="excludedChars" native="true">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <layout class="QGridLayout" name="gridLayout_5">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item row="0" column="1">
- <widget class="QLineEdit" name="editExcludedChars">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Character set to exclude from generated password</string>
- </property>
- <property name="accessibleName">
- <string>Excluded characters</string>
- </property>
- <property name="clearButtonEnabled">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="labelExcludedChars">
- <property name="text">
- <string>Do not include:</string>
- </property>
- </widget>
- </item>
- <item row="0" column="2">
- <widget class="QPushButton" name="buttonAddHex">
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>25</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Add non-hex letters to &quot;do not include&quot; list</string>
- </property>
- <property name="accessibleName">
- <string>Hex Passwords</string>
- </property>
- <property name="text">
- <string>Hex</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>0</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QCheckBox" name="checkBoxExcludeAlike">
- <property name="toolTip">
- <string>Excluded characters: &quot;0&quot;, &quot;1&quot;, &quot;l&quot;, &quot;I&quot;, &quot;O&quot;, &quot;|&quot;, &quot;ï¹’&quot;</string>
- </property>
- <property name="text">
- <string>Exclude look-alike characters</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="checkBoxEnsureEvery">
- <property name="text">
- <string>Pick characters from every group</string>
- </property>
- <attribute name="buttonGroup">
- <string notr="true">optionButtons</string>
- </attribute>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
+ <property name="buddy">
+ <cstring>spinBoxLength</cstring>
+ </property>
+ </widget>
</item>
- <item row="0" column="0">
- <layout class="QHBoxLayout" name="passwordLengthSliderLayout">
- <property name="spacing">
- <number>15</number>
+ <item>
+ <widget class="QSlider" name="sliderLength">
+ <property name="accessibleName">
+ <string>Password length</string>
</property>
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
+ <property name="minimum">
+ <number>1</number>
</property>
- <property name="topMargin">
- <number>6</number>
+ <property name="maximum">
+ <number>128</number>
</property>
- <item>
- <widget class="QLabel" name="labelLength">
- <property name="text">
- <string>&amp;Length:</string>
- </property>
- <property name="buddy">
- <cstring>spinBoxLength</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QSlider" name="sliderLength">
- <property name="accessibleName">
- <string>Password length</string>
- </property>
- <property name="minimum">
- <number>1</number>
- </property>
- <property name="maximum">
- <number>128</number>
- </property>
- <property name="sliderPosition">
- <number>20</number>
- </property>
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="tickPosition">
- <enum>QSlider::TicksBelow</enum>
- </property>
- <property name="tickInterval">
- <number>8</number>
- </property>
- </widget>
- </item>
- <item alignment="Qt::AlignRight">
- <widget class="QSpinBox" name="spinBoxLength">
- <property name="accessibleName">
- <string>Password length</string>
+ <property name="sliderPosition">
+ <number>20</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="tickPosition">
+ <enum>QSlider::TicksBelow</enum>
+ </property>
+ <property name="tickInterval">
+ <number>8</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBoxLength">
+ <property name="accessibleName">
+ <string>Password length</string>
+ </property>
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>128</number>
+ </property>
+ <property name="value">
+ <number>20</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="buttonAdvancedMode">
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Switch to advanced mode</string>
+ </property>
+ <property name="text">
+ <string>Advanced</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="0">
+ <widget class="QGroupBox" name="groupBox">
+ <property name="minimumSize">
+ <size>
+ <width>580</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>Character Types</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QWidget" name="simpleBar" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
</property>
- <property name="minimum">
- <number>1</number>
+ <property name="topMargin">
+ <number>0</number>
</property>
- <property name="maximum">
- <number>999</number>
+ <property name="rightMargin">
+ <number>0</number>
</property>
- <property name="value">
- <number>20</number>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="dicewareWidget">
- <attribute name="title">
- <string>Passphrase</string>
- </attribute>
- <layout class="QGridLayout" name="gridLayout_2">
- <item row="1" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="3" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="wordCaseLabel">
+ <item>
+ <widget class="QToolButton" name="checkBoxUpper">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Upper-case letters</string>
+ </property>
+ <property name="accessibleName">
+ <string>Upper-case letters</string>
+ </property>
+ <property name="accessibleDescription">
+ <string/>
+ </property>
<property name="text">
- <string>Word Case:</string>
+ <string notr="true">A-Z</string>
</property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="0" column="1">
- <widget class="QComboBox" name="comboBoxWordList">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <item>
+ <widget class="QToolButton" name="checkBoxLower">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Lower-case letters</string>
+ </property>
+ <property name="accessibleName">
+ <string>Lower-case letters</string>
+ </property>
+ <property name="accessibleDescription">
+ <string/>
+ </property>
+ <property name="text">
+ <string notr="true">a-z</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
</property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="2" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="labelWordSeparator">
+ <item>
+ <widget class="QToolButton" name="checkBoxNumbers">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Numbers</string>
+ </property>
+ <property name="accessibleName">
+ <string>Numbers</string>
+ </property>
+ <property name="accessibleDescription">
+ <string/>
+ </property>
<property name="text">
- <string>Word Separator:</string>
+ <string notr="true">0-9</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
</property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="0" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="labelWordList">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
+ <item>
+ <widget class="QToolButton" name="checkBoxSpecialChars">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>60</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Special characters</string>
+ </property>
+ <property name="accessibleName">
+ <string>Special characters</string>
</property>
<property name="text">
- <string>Wordlist:</string>
+ <string notr="true">/*_&amp; ...</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
</property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="4" column="1">
- <spacer name="verticalSpacer_3">
+ <item>
+ <widget class="QToolButton" name="checkBoxExtASCII">
+ <property name="minimumSize">
+ <size>
+ <width>105</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Extended ASCII</string>
+ </property>
+ <property name="accessibleName">
+ <string>Extended ASCII</string>
+ </property>
+ <property name="text">
+ <string>ExtendedASCII</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
<property name="orientation">
- <enum>Qt::Vertical</enum>
+ <enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>20</width>
- <height>40</height>
+ <width>0</width>
+ <height>0</height>
</size>
</property>
</spacer>
</item>
- <item row="1" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_3">
- <property name="sizeConstraint">
- <enum>QLayout::SetMinimumSize</enum>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="advancedContainer" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="advancedBar" native="true">
+ <property name="enabled">
+ <bool>true</bool>
</property>
- <item>
- <widget class="QSlider" name="sliderWordCount">
- <property name="minimum">
- <number>1</number>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QToolButton" name="checkBoxUpperAdv">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Upper-case letters</string>
+ </property>
+ <property name="accessibleName">
+ <string>Upper-case letters</string>
+ </property>
+ <property name="text">
+ <string>A-Z</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="checkBoxLowerAdv">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Lower-case letters</string>
+ </property>
+ <property name="accessibleName">
+ <string>Lower-case letters</string>
+ </property>
+ <property name="text">
+ <string>a-z</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QToolButton" name="checkBoxNumbersAdv">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Numbers</string>
+ </property>
+ <property name="text">
+ <string>0-9</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="checkBoxBraces">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Braces</string>
+ </property>
+ <property name="accessibleName">
+ <string>Braces</string>
+ </property>
+ <property name="text">
+ <string>{[(</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QToolButton" name="checkBoxPunctuation">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Punctuation</string>
+ </property>
+ <property name="accessibleName">
+ <string>Punctuation</string>
+ </property>
+ <property name="text">
+ <string>.,:;</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="checkBoxQuotes">
+ <property name="minimumSize">
+ <size>
+ <width>40</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Quotes</string>
+ </property>
+ <property name="text">
+ <string>&quot; '</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QToolButton" name="checkBoxMath">
+ <property name="minimumSize">
+ <size>
+ <width>60</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Math Symbols</string>
+ </property>
+ <property name="accessibleName">
+ <string>Math Symbols</string>
+ </property>
+ <property name="text">
+ <string>&lt;*+!?=</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="checkBoxDashes">
+ <property name="minimumSize">
+ <size>
+ <width>60</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Dashes and Slashes</string>
+ </property>
+ <property name="accessibleName">
+ <string>Dashes and Slashes</string>
+ </property>
+ <property name="text">
+ <string>\_|-/</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QToolButton" name="checkBoxLogograms">
+ <property name="minimumSize">
+ <size>
+ <width>105</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Logograms</string>
+ </property>
+ <property name="accessibleName">
+ <string>Logograms</string>
+ </property>
+ <property name="text">
+ <string>#$%&amp;&amp;@^`~</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="checkBoxExtASCIIAdv">
+ <property name="minimumSize">
+ <size>
+ <width>105</width>
+ <height>25</height>
+ </size>
+ </property>
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
+ </property>
+ <property name="toolTip">
+ <string>Extended ASCII</string>
+ </property>
+ <property name="text">
+ <string>ExtendedASCII</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Also choose from:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="editAdditionalChars">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
</property>
- <property name="maximum">
- <number>40</number>
+ <property name="toolTip">
+ <string>Additional characters to use for the generated password</string>
</property>
- <property name="value">
- <number>6</number>
+ <property name="accessibleName">
+ <string>Additional characters</string>
</property>
- <property name="sliderPosition">
- <number>6</number>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="editExcludedChars">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <property name="minimumSize">
+ <size>
+ <width>200</width>
+ <height>0</height>
+ </size>
</property>
- <property name="tickPosition">
- <enum>QSlider::TicksBelow</enum>
+ <property name="toolTip">
+ <string>Character set to exclude from generated password</string>
</property>
- <property name="tickInterval">
- <number>8</number>
+ <property name="accessibleName">
+ <string>Excluded characters</string>
</property>
</widget>
</item>
- <item>
- <widget class="QSpinBox" name="spinBoxWordCount">
- <property name="minimum">
- <number>1</number>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelExcludedChars">
+ <property name="text">
+ <string>Do not include:</string>
</property>
- <property name="maximum">
- <number>100</number>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QPushButton" name="buttonAddHex">
+ <property name="focusPolicy">
+ <enum>Qt::TabFocus</enum>
</property>
- <property name="value">
- <number>6</number>
+ <property name="toolTip">
+ <string>Add non-hex letters to &quot;do not include&quot; list</string>
+ </property>
+ <property name="accessibleName">
+ <string>Hex Passwords</string>
+ </property>
+ <property name="text">
+ <string>Hex</string>
</property>
</widget>
</item>
</layout>
</item>
- <item row="1" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="labelWordCount">
- <property name="text">
- <string>Word Co&amp;unt:</string>
+ <item>
+ <widget class="QCheckBox" name="checkBoxExcludeAlike">
+ <property name="toolTip">
+ <string>Excluded characters: &quot;0&quot;, &quot;1&quot;, &quot;l&quot;, &quot;I&quot;, &quot;O&quot;, &quot;|&quot;, &quot;ï¹’&quot;</string>
</property>
- <property name="buddy">
- <cstring>spinBoxLength</cstring>
+ <property name="text">
+ <string>Exclude look-alike characters</string>
</property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="2" column="1">
- <widget class="QLineEdit" name="editWordSeparator">
+ <item>
+ <widget class="QCheckBox" name="checkBoxEnsureEvery">
<property name="text">
- <string/>
+ <string>Pick characters from every group</string>
</property>
+ <attribute name="buttonGroup">
+ <string notr="true">optionButtons</string>
+ </attribute>
</widget>
</item>
- <item row="3" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QComboBox" name="wordCaseComboBox"/>
- </item>
- <item>
- <spacer name="horizontalSpacer_4">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="excludedChars" native="true">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="dicewareWidget">
+ <attribute name="title">
+ <string>Passphrase</string>
+ </attribute>
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_3">
+ <item row="3" column="0" alignment="Qt::AlignRight">
+ <widget class="QLabel" name="wordCaseLabel">
+ <property name="text">
+ <string>Word Case:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="comboBoxWordList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0" alignment="Qt::AlignRight">
+ <widget class="QLabel" name="labelWordSeparator">
+ <property name="text">
+ <string>Word Separator:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" alignment="Qt::AlignRight">
+ <widget class="QLabel" name="labelWordList">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Wordlist:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="1" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <widget class="QSlider" name="sliderWordCount">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>40</number>
+ </property>
+ <property name="value">
+ <number>6</number>
+ </property>
+ <property name="sliderPosition">
+ <number>6</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="tickPosition">
+ <enum>QSlider::TicksBelow</enum>
+ </property>
+ <property name="tickInterval">
+ <number>8</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="spinBoxWordCount">
+ <property name="minimum">
+ <number>1</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>6</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="0" alignment="Qt::AlignRight">
+ <widget class="QLabel" name="labelWordCount">
+ <property name="text">
+ <string>Word Count:</string>
+ </property>
+ <property name="buddy">
+ <cstring>spinBoxLength</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="editWordSeparator">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <item>
+ <widget class="QComboBox" name="wordCaseComboBox"/>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
- </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="horizontalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="buttonClose">
+ <property name="text">
+ <string>Close</string>
+ </property>
+ <property name="shortcut">
+ <string>Esc</string>
+ </property>
</widget>
</item>
<item>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="buttonGenerate">
- <property name="toolTip">
- <string>Regenerate password</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string>Regenerate</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="buttonCopy">
- <property name="toolTip">
- <string>Copy password</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string>Copy</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="buttonApply">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="toolTip">
- <string>Accept password</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- <property name="text">
- <string>Accept</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QPushButton" name="buttonApply">
+ <property name="text">
+ <string>Apply Password</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+S</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
</item>
</layout>
</item>
<item>
- <spacer name="verticalSpacer_4">
+ <spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
@@ -1208,6 +1183,22 @@ QProgressBar::chunk {
</spacer>
</item>
</layout>
+ <action name="clearInclude">
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ <property name="toolTip">
+ <string>Clear</string>
+ </property>
+ </action>
+ <action name="clearExclude">
+ <property name="text">
+ <string>Clear</string>
+ </property>
+ <property name="toolTip">
+ <string>Clear</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
@@ -1219,16 +1210,18 @@ QProgressBar::chunk {
</customwidgets>
<tabstops>
<tabstop>editNewPassword</tabstop>
- <tabstop>togglePasswordButton</tabstop>
+ <tabstop>buttonGenerate</tabstop>
+ <tabstop>buttonCopy</tabstop>
<tabstop>tabWidget</tabstop>
<tabstop>sliderLength</tabstop>
<tabstop>spinBoxLength</tabstop>
+ <tabstop>buttonAdvancedMode</tabstop>
+ <tabstop>groupBox</tabstop>
<tabstop>checkBoxUpper</tabstop>
<tabstop>checkBoxLower</tabstop>
<tabstop>checkBoxNumbers</tabstop>
<tabstop>checkBoxSpecialChars</tabstop>
<tabstop>checkBoxExtASCII</tabstop>
- <tabstop>buttonAdvancedMode</tabstop>
<tabstop>checkBoxUpperAdv</tabstop>
<tabstop>checkBoxNumbersAdv</tabstop>
<tabstop>checkBoxPunctuation</tabstop>
@@ -1239,17 +1232,17 @@ QProgressBar::chunk {
<tabstop>checkBoxQuotes</tabstop>
<tabstop>checkBoxDashes</tabstop>
<tabstop>checkBoxExtASCIIAdv</tabstop>
+ <tabstop>editAdditionalChars</tabstop>
<tabstop>editExcludedChars</tabstop>
<tabstop>buttonAddHex</tabstop>
<tabstop>checkBoxExcludeAlike</tabstop>
<tabstop>checkBoxEnsureEvery</tabstop>
- <tabstop>buttonSimpleMode</tabstop>
<tabstop>comboBoxWordList</tabstop>
<tabstop>sliderWordCount</tabstop>
<tabstop>spinBoxWordCount</tabstop>
<tabstop>editWordSeparator</tabstop>
- <tabstop>buttonGenerate</tabstop>
- <tabstop>buttonCopy</tabstop>
+ <tabstop>wordCaseComboBox</tabstop>
+ <tabstop>buttonClose</tabstop>
<tabstop>buttonApply</tabstop>
</tabstops>
<resources/>
diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui
index 45e0d0bc6..ebc62e991 100644
--- a/src/gui/SearchHelpWidget.ui
+++ b/src/gui/SearchHelpWidget.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>334</width>
- <height>249</height>
+ <width>397</width>
+ <height>264</height>
</rect>
</property>
<property name="windowTitle">
@@ -17,34 +17,13 @@
<bool>false</bool>
</property>
<property name="frameShape">
- <enum>QFrame::Box</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Plain</enum>
+ <enum>QFrame::StyledPanel</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>6</number>
- </property>
- <property name="sizeConstraint">
- <enum>QLayout::SetDefaultConstraint</enum>
- </property>
- <property name="leftMargin">
- <number>5</number>
- </property>
- <property name="topMargin">
- <number>5</number>
- </property>
- <property name="rightMargin">
- <number>5</number>
- </property>
- <property name="bottomMargin">
- <number>5</number>
- </property>
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -58,12 +37,15 @@
<property name="text">
<string>Search terms are as follows: [modifiers][field:][&quot;]term[&quot;]</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_24">
<property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@@ -77,6 +59,9 @@
<property name="text">
<string>Every search term must match (ie, logical AND)</string>
</property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
</widget>
</item>
<item>
@@ -93,24 +78,6 @@
<string>Modifiers</string>
</property>
<layout class="QFormLayout" name="formLayout">
- <property name="horizontalSpacing">
- <number>8</number>
- </property>
- <property name="verticalSpacing">
- <number>8</number>
- </property>
- <property name="leftMargin">
- <number>9</number>
- </property>
- <property name="topMargin">
- <number>10</number>
- </property>
- <property name="rightMargin">
- <number>9</number>
- </property>
- <property name="bottomMargin">
- <number>9</number>
- </property>
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="minimumSize">
@@ -219,21 +186,6 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
- <property name="leftMargin">
- <number>15</number>
- </property>
- <property name="topMargin">
- <number>10</number>
- </property>
- <property name="rightMargin">
- <number>15</number>
- </property>
- <property name="horizontalSpacing">
- <number>8</number>
- </property>
- <property name="verticalSpacing">
- <number>5</number>
- </property>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
@@ -283,6 +235,13 @@
</property>
</widget>
</item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="label_16">
+ <property name="text">
+ <string notr="true">group (g)</string>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>
@@ -296,12 +255,6 @@
<string>Term Wildcards</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
- <property name="horizontalSpacing">
- <number>8</number>
- </property>
- <property name="verticalSpacing">
- <number>8</number>
- </property>
<item row="0" column="0">
<widget class="QLabel" name="label_18">
<property name="minimumSize">
@@ -401,9 +354,6 @@
<string>Examples</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
- <property name="spacing">
- <number>8</number>
- </property>
<item>
<widget class="QLabel" name="label_9">
<property name="sizePolicy">
diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp
index 822d40ba1..1c7b683c1 100644
--- a/src/gui/SearchWidget.cpp
+++ b/src/gui/SearchWidget.cpp
@@ -25,7 +25,7 @@
#include <QToolButton>
#include "core/Config.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "gui/widgets/PopupHelpWidget.h"
SearchWidget::SearchWidget(QWidget* parent)
@@ -38,7 +38,6 @@ SearchWidget::SearchWidget(QWidget* parent)
setFocusProxy(m_ui->searchEdit);
m_helpWidget = new PopupHelpWidget(m_ui->searchEdit);
- m_helpWidget->setOffset(QPoint(0, 1));
Ui::SearchHelpWidget helpUi;
helpUi.setupUi(m_helpWidget);
@@ -68,15 +67,15 @@ SearchWidget::SearchWidget(QWidget* parent)
m_actionLimitGroup = m_searchMenu->addAction(tr("Limit search to selected group"), this, SLOT(updateLimitGroup()));
m_actionLimitGroup->setObjectName("actionSearchLimitGroup");
m_actionLimitGroup->setCheckable(true);
- m_actionLimitGroup->setChecked(config()->get("SearchLimitGroup", false).toBool());
+ m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
- m_ui->searchIcon->setIcon(filePath()->icon("actions", "system-search"));
+ m_ui->searchIcon->setIcon(resources()->icon("system-search"));
m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);
- m_ui->helpIcon->setIcon(filePath()->icon("actions", "system-help"));
+ m_ui->helpIcon->setIcon(resources()->icon("system-help"));
m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition);
- m_ui->clearIcon->setIcon(filePath()->icon("actions", "edit-clear-locationbar-rtl"));
+ m_ui->clearIcon->setIcon(resources()->icon("edit-clear-locationbar-rtl"));
m_ui->clearIcon->setVisible(false);
m_ui->searchEdit->addAction(m_ui->clearIcon, QLineEdit::TrailingPosition);
@@ -115,15 +114,16 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event)
return true;
}
}
- } else if (event->type() == QEvent::FocusOut) {
- if (config()->get("security/clearsearch").toBool()) {
- int timeout = config()->get("security/clearsearchtimeout").toInt();
+ } else if (event->type() == QEvent::FocusOut && !m_ui->searchEdit->text().isEmpty()) {
+ if (config()->get(Config::Security_ClearSearch).toBool()) {
+ int timeout = config()->get(Config::Security_ClearSearchTimeout).toInt();
if (timeout > 0) {
// Auto-clear search after set timeout (5 minutes by default)
m_clearSearchTimer->start(timeout * 60000); // 60 sec * 1000 ms
}
}
} else if (event->type() == QEvent::FocusIn) {
+ // Never clear the search if we are using it
m_clearSearchTimer->stop();
}
@@ -137,8 +137,11 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
- mx.connect(this, SIGNAL(downPressed()), SLOT(setFocus()));
+ mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
mx.connect(SIGNAL(clearSearch()), m_ui->searchEdit, SLOT(clear()));
+ mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
+ mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
+ mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(searchFocus()));
mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
}
@@ -147,8 +150,6 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
if (dbWidget != nullptr) {
// Set current search text from this database
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
- // Keyboard focus on search widget at database unlocking
- connect(dbWidget, SIGNAL(databaseUnlocked()), this, SLOT(searchFocus()));
// Enforce search policy
emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
emit limitGroupChanged(m_actionLimitGroup->isChecked());
@@ -177,6 +178,14 @@ void SearchWidget::startSearch()
search(m_ui->searchEdit->text());
}
+void SearchWidget::resetSearchClearTimer()
+{
+ // Restart the search clear timer if it is running
+ if (m_clearSearchTimer->isActive()) {
+ m_clearSearchTimer->start();
+ }
+}
+
void SearchWidget::updateCaseSensitive()
{
emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
@@ -190,7 +199,7 @@ void SearchWidget::setCaseSensitive(bool state)
void SearchWidget::updateLimitGroup()
{
- config()->set("SearchLimitGroup", m_actionLimitGroup->isChecked());
+ config()->set(Config::SearchLimitGroup, m_actionLimitGroup->isChecked());
emit limitGroupChanged(m_actionLimitGroup->isChecked());
}
diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h
index f3646c362..1a40aba95 100644
--- a/src/gui/SearchWidget.h
+++ b/src/gui/SearchWidget.h
@@ -70,6 +70,7 @@ private slots:
void searchFocus();
void toggleHelp();
void showSearchMenu();
+ void resetSearchClearTimer();
private:
const QScopedPointer<Ui::SearchWidget> m_ui;
diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui
index 93fbbdee5..74cc468cf 100644
--- a/src/gui/SearchWidget.ui
+++ b/src/gui/SearchWidget.ui
@@ -6,17 +6,11 @@
<rect>
<x>0</x>
<y>0</y>
- <width>630</width>
+ <width>465</width>
<height>34</height>
</rect>
</property>
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,4">
+ <layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>3</number>
</property>
@@ -30,38 +24,19 @@
<number>0</number>
</property>
<item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Minimum</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>30</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
<widget class="QLineEdit" name="searchEdit">
<property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
- <width>0</width>
+ <width>150</width>
<height>0</height>
</size>
</property>
- <property name="styleSheet">
- <string notr="true">padding:3px</string>
- </property>
<property name="placeholderText">
<string/>
</property>
diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp
index 639eb0ebd..871c2863c 100644
--- a/src/gui/TotpDialog.cpp
+++ b/src/gui/TotpDialog.cpp
@@ -65,10 +65,10 @@ TotpDialog::~TotpDialog()
void TotpDialog::copyToClipboard()
{
clipboard()->setText(m_entry->totp());
- if (config()->get("HideWindowOnCopy").toBool()) {
- if (config()->get("MinimizeOnCopy").toBool()) {
- getMainWindow()->showMinimized();
- } else if (config()->get("DropToBackgroundOnCopy").toBool()) {
+ if (config()->get(Config::HideWindowOnCopy).toBool()) {
+ if (config()->get(Config::MinimizeOnCopy).toBool()) {
+ getMainWindow()->minimizeOrHide();
+ } else if (config()->get(Config::DropToBackgroundOnCopy).toBool()) {
getMainWindow()->lower();
window()->lower();
}
diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp
index 178cd6d96..f73b9877a 100644
--- a/src/gui/TotpExportSettingsDialog.cpp
+++ b/src/gui/TotpExportSettingsDialog.cpp
@@ -103,10 +103,10 @@ TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry
void TotpExportSettingsDialog::copyToClipboard()
{
clipboard()->setText(m_totpUri);
- if (config()->get("HideWindowOnCopy").toBool()) {
- if (config()->get("MinimizeOnCopy").toBool()) {
- getMainWindow()->showMinimized();
- } else if (config()->get("DropToBackgroundOnCopy").toBool()) {
+ if (config()->get(Config::HideWindowOnCopy).toBool()) {
+ if (config()->get(Config::MinimizeOnCopy).toBool()) {
+ getMainWindow()->minimizeOrHide();
+ } else if (config()->get(Config::DropToBackgroundOnCopy).toBool()) {
getMainWindow()->lower();
window()->lower();
}
diff --git a/src/gui/TotpSetupDialog.ui b/src/gui/TotpSetupDialog.ui
index 84ba4cb54..c70dc00c7 100644
--- a/src/gui/TotpSetupDialog.ui
+++ b/src/gui/TotpSetupDialog.ui
@@ -216,7 +216,9 @@
<tabstop>radioDefault</tabstop>
<tabstop>radioSteam</tabstop>
<tabstop>radioCustom</tabstop>
+ <tabstop>algorithmComboBox</tabstop>
<tabstop>stepSpinBox</tabstop>
+ <tabstop>digitsSpinBox</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/src/gui/URLEdit.cpp b/src/gui/URLEdit.cpp
index 4dc2a55c2..428a918db 100644
--- a/src/gui/URLEdit.cpp
+++ b/src/gui/URLEdit.cpp
@@ -21,7 +21,7 @@
#include <QRegularExpression>
#include "core/Config.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "core/Tools.h"
#include "gui/Font.h"
@@ -30,7 +30,7 @@ const QColor URLEdit::ErrorColor = QColor(255, 125, 125);
URLEdit::URLEdit(QWidget* parent)
: QLineEdit(parent)
{
- const QIcon errorIcon = filePath()->icon("status", "dialog-error");
+ const QIcon errorIcon = resources()->icon("dialog-error");
m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition);
m_errorAction->setVisible(false);
m_errorAction->setToolTip(tr("Invalid URL"));
diff --git a/src/gui/UpdateCheckDialog.cpp b/src/gui/UpdateCheckDialog.cpp
index 2f6d1fc48..db817a74b 100644
--- a/src/gui/UpdateCheckDialog.cpp
+++ b/src/gui/UpdateCheckDialog.cpp
@@ -16,7 +16,7 @@
*/
#include "UpdateCheckDialog.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "ui_UpdateCheckDialog.h"
#include "updatecheck/UpdateChecker.h"
@@ -28,7 +28,7 @@ UpdateCheckDialog::UpdateCheckDialog(QWidget* parent)
setWindowFlags(Qt::Window);
setAttribute(Qt::WA_DeleteOnClose);
- m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48));
+ m_ui->iconLabel->setPixmap(resources()->applicationIcon().pixmap(48));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
connect(UpdateChecker::instance(),
diff --git a/src/gui/WelcomeWidget.cpp b/src/gui/WelcomeWidget.cpp
index 35cce553e..5e87a0883 100644
--- a/src/gui/WelcomeWidget.cpp
+++ b/src/gui/WelcomeWidget.cpp
@@ -22,7 +22,7 @@
#include "config-keepassx.h"
#include "core/Config.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
WelcomeWidget::WelcomeWidget(QWidget* parent)
: QWidget(parent)
@@ -36,15 +36,10 @@ WelcomeWidget::WelcomeWidget(QWidget* parent)
welcomeLabelFont.setPointSize(welcomeLabelFont.pointSize() + 4);
m_ui->welcomeLabel->setFont(welcomeLabelFont);
- m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(64));
+ m_ui->iconLabel->setPixmap(resources()->applicationIcon().pixmap(64));
refreshLastDatabases();
- bool recent_visibility = (m_ui->recentListWidget->count() > 0);
- m_ui->startLabel->setVisible(!recent_visibility);
- m_ui->recentListWidget->setVisible(recent_visibility);
- m_ui->recentLabel->setVisible(recent_visibility);
-
connect(m_ui->buttonNewDatabase, SIGNAL(clicked()), SIGNAL(newDatabase()));
connect(m_ui->buttonOpenDatabase, SIGNAL(clicked()), SIGNAL(openDatabase()));
connect(m_ui->buttonImportKeePass1, SIGNAL(clicked()), SIGNAL(importKeePass1Database()));
@@ -62,28 +57,57 @@ WelcomeWidget::~WelcomeWidget()
void WelcomeWidget::openDatabaseFromFile(QListWidgetItem* item)
{
- if (item->text().isEmpty()) {
+ if (!item || item->text().isEmpty()) {
return;
}
emit openDatabaseFile(item->text());
}
+void WelcomeWidget::removeFromLastDatabases(QListWidgetItem* item)
+{
+ if (!item || item->text().isEmpty()) {
+ return;
+ }
+
+ if (config()->get(Config::RememberLastDatabases).toBool()) {
+ QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
+ lastDatabases.removeOne(item->text());
+ config()->set(Config::LastDatabases, lastDatabases);
+ }
+ refreshLastDatabases();
+}
+
void WelcomeWidget::refreshLastDatabases()
{
m_ui->recentListWidget->clear();
- const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList();
+ const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
for (const QString& database : lastDatabases) {
QListWidgetItem* itm = new QListWidgetItem;
itm->setText(database);
m_ui->recentListWidget->addItem(itm);
}
+
+ bool recent_visibility = (m_ui->recentListWidget->count() > 0);
+ m_ui->startLabel->setVisible(!recent_visibility);
+ m_ui->recentListWidget->setVisible(recent_visibility);
+ m_ui->recentLabel->setVisible(recent_visibility);
}
void WelcomeWidget::keyPressEvent(QKeyEvent* event)
{
- if (m_ui->recentListWidget->hasFocus() && (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)) {
- openDatabaseFromFile(m_ui->recentListWidget->currentItem());
+ if (m_ui->recentListWidget->hasFocus()) {
+ if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
+ openDatabaseFromFile(m_ui->recentListWidget->currentItem());
+ } else if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
+ removeFromLastDatabases(m_ui->recentListWidget->currentItem());
+ }
}
QWidget::keyPressEvent(event);
}
+
+void WelcomeWidget::showEvent(QShowEvent* event)
+{
+ refreshLastDatabases();
+ QWidget::showEvent(event);
+}
diff --git a/src/gui/WelcomeWidget.h b/src/gui/WelcomeWidget.h
index de4956779..13850e85a 100644
--- a/src/gui/WelcomeWidget.h
+++ b/src/gui/WelcomeWidget.h
@@ -46,12 +46,14 @@ signals:
protected:
void keyPressEvent(QKeyEvent* event) override;
+ void showEvent(QShowEvent* event) override;
private slots:
void openDatabaseFromFile(QListWidgetItem* item);
private:
const QScopedPointer<Ui::WelcomeWidget> m_ui;
+ void removeFromLastDatabases(QListWidgetItem* item);
};
#endif // KEEPASSX_WELCOMEWIDGET_H
diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp
index 6e6c282b9..01fd5fc89 100644
--- a/src/gui/csvImport/CsvImportWidget.cpp
+++ b/src/gui/csvImport/CsvImportWidget.cpp
@@ -39,69 +39,26 @@ CsvImportWidget::CsvImportWidget(QWidget* parent)
, m_comboModel(new QStringListModel(this))
, m_columnHeader(QStringList() << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username")
<< QObject::tr("Password") << QObject::tr("URL") << QObject::tr("Notes")
- << QObject::tr("Last Modified") << QObject::tr("Created")
- /* << QObject::tr("Future field1") */)
-{
- m_ui->setupUi(this);
-
- m_ui->comboBoxCodec->addItems(QStringList() << "UTF-8"
- << "Windows-1252"
- << "UTF-16"
- << "UTF-16LE");
- m_ui->comboBoxFieldSeparator->addItems(QStringList() << ","
- << ";"
- << "-"
- << ":"
- << "."
- << "TAB (\\t)");
- m_fieldSeparatorList = QStringList() << ","
+ << QObject::tr("Last Modified") << QObject::tr("Created"))
+ , m_fieldSeparatorList(QStringList() << ","
<< ";"
<< "-"
<< ":"
<< "."
- << "\t";
- m_ui->comboBoxTextQualifier->addItems(QStringList() << "\""
- << "'"
- << ":"
- << "."
- << "|");
- m_ui->comboBoxComment->addItems(QStringList() << "#"
- << ";"
- << ":"
- << "@");
+ << "\t")
+{
+ m_ui->setupUi(this);
+
m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus);
m_ui->messageWidget->setHidden(true);
- for (int i = 0; i < m_columnHeader.count(); ++i) {
- QLabel* label = new QLabel(m_columnHeader.at(i), this);
- label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
- QFont font = label->font();
- font.setBold(false);
- label->setFont(font);
-
- QComboBox* combo = new QComboBox(this);
- font = combo->font();
- font.setBold(false);
- combo->setFont(font);
- m_combos.append(combo);
+ m_combos << m_ui->groupCombo << m_ui->titleCombo << m_ui->usernameCombo << m_ui->passwordCombo << m_ui->urlCombo
+ << m_ui->notesCombo << m_ui->lastModifiedCombo << m_ui->createdCombo;
+
+ for (auto combo : m_combos) {
combo->setModel(m_comboModel);
-#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
- connect(combo, QOverload<int>::of(&QComboBox::currentIndexChanged), [=] { comboChanged(combo, i); });
-#else
- connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [=] {
- comboChanged(combo, i);
- });
-#endif
-
- // layout labels and combo fields in column-first order
- int combo_rows = 1 + (m_columnHeader.count() - 1) / 2;
- int x = i % combo_rows;
- int y = 2 * (i / combo_rows);
- m_ui->gridLayout_combos->addWidget(label, x, y);
- m_ui->gridLayout_combos->addWidget(combo, x, y + 1);
- QSpacerItem* item = new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed);
- m_ui->gridLayout_combos->addItem(item, x, y + 2);
+ connect(combo, SIGNAL(currentIndexChanged(int)), SLOT(comboChanged(int)));
}
m_parserModel->setHeaderLabels(m_columnHeader);
@@ -119,12 +76,10 @@ CsvImportWidget::CsvImportWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
}
-void CsvImportWidget::comboChanged(QComboBox* currentSender, int comboId)
+void CsvImportWidget::comboChanged(int index)
{
- if (currentSender->currentIndex() != -1) {
- // this line is the one that actually updates GUI table
- m_parserModel->mapColumns(currentSender->currentIndex(), comboId);
- }
+ // this line is the one that actually updates GUI table
+ m_parserModel->mapColumns(index, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
updateTableview();
}
@@ -167,19 +122,17 @@ void CsvImportWidget::updatePreview()
m_ui->spinBoxSkip->setRange(minSkip, qMax(minSkip, m_parserModel->rowCount() - 1));
m_ui->spinBoxSkip->setValue(minSkip);
- int emptyId = 0;
- QString columnName;
- QStringList list(tr("Not present in CSV file"));
-
+ QStringList list(tr("Not Present"));
for (int i = 1; i < m_parserModel->getCsvCols(); ++i) {
if (m_ui->checkBoxFieldNames->isChecked()) {
- columnName = m_parserModel->getCsvTable().at(0).at(i);
+ auto columnName = m_parserModel->getCsvTable().at(0).at(i);
if (columnName.isEmpty()) {
- columnName = "<" + tr("Empty fieldname %1").arg(++emptyId) + ">";
+ list << QString(tr("Column %1").arg(i));
+ } else {
+ list << columnName;
}
- list << columnName;
} else {
- list << QString(tr("column %1").arg(i));
+ list << QString(tr("Column %1").arg(i));
}
}
m_comboModel->setStringList(list);
@@ -221,7 +174,6 @@ void CsvImportWidget::parse()
} else {
m_ui->messageWidget->setHidden(true);
}
- QWidget::adjustSize();
}
QString CsvImportWidget::formatStatusText() const
@@ -294,27 +246,30 @@ void CsvImportWidget::setRootGroup()
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
// use validity of second column as a GO/NOGO for all others fields
- if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid())
+ if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid()) {
continue;
+ }
groupLabel = m_parserModel->data(m_parserModel->index(r, 0)).toString();
// check if group name is either "root", "" (empty) or some other label
groupList = groupLabel.split("/", QString::SkipEmptyParts);
- if (groupList.isEmpty())
+ if (groupList.isEmpty()) {
is_empty = true;
- else if (not groupList.first().compare("Root", Qt::CaseSensitive))
+ } else if (not groupList.first().compare("Root", Qt::CaseSensitive)) {
is_root = true;
- else if (not groupLabel.compare(""))
+ } else if (not groupLabel.compare("")) {
is_empty = true;
- else
+ } else {
is_label = true;
+ }
groupList.clear();
}
- if ((is_empty and is_root) or (is_label and not is_empty and is_root))
+ if ((is_empty and is_root) or (is_label and not is_empty and is_root)) {
m_db->rootGroup()->setName("CSV IMPORTED");
- else
+ } else {
m_db->rootGroup()->setName("Root");
+ }
}
Group* CsvImportWidget::splitGroups(const QString& label)
diff --git a/src/gui/csvImport/CsvImportWidget.h b/src/gui/csvImport/CsvImportWidget.h
index a5807eefd..a81550aa1 100644
--- a/src/gui/csvImport/CsvImportWidget.h
+++ b/src/gui/csvImport/CsvImportWidget.h
@@ -49,7 +49,7 @@ signals:
private slots:
void parse();
- void comboChanged(QComboBox* currentSender, int comboId);
+ void comboChanged(int index);
void skippedChanged(int rows);
void writeDatabase();
void updatePreview();
diff --git a/src/gui/csvImport/CsvImportWidget.ui b/src/gui/csvImport/CsvImportWidget.ui
index beaa39386..1c268fd9d 100644
--- a/src/gui/csvImport/CsvImportWidget.ui
+++ b/src/gui/csvImport/CsvImportWidget.ui
@@ -6,444 +6,657 @@
<rect>
<x>0</x>
<y>0</y>
- <width>892</width>
- <height>525</height>
+ <width>788</width>
+ <height>530</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
- <layout class="QGridLayout" name="gridLayout_4">
- <item row="0" column="0" rowspan="2" colspan="2">
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="MessageWidget" name="messageWidget" native="true"/>
- </item>
- <item>
- <widget class="QLabel" name="labelHeadline">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <pointsize>11</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="text">
- <string>Import CSV fields</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="labelFilename">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>filename</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="labelSizeRowsCols">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>size, rows, columns</string>
- </property>
- </widget>
- </item>
- </layout>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
- <item row="1" column="1">
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
</property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
+ <property name="widgetResizable">
+ <bool>true</bool>
</property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>758</width>
- <height>24</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="2" column="0" colspan="2">
- <widget class="QGroupBox" name="Encoding">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="title">
- <string>Encoding</string>
- </property>
- <layout class="QGridLayout" name="gridLayout_2">
- <item row="0" column="0">
- <widget class="QLabel" name="labelCodec">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Codec</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="0" column="1" colspan="2">
- <widget class="QComboBox" name="comboBoxCodec">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>Codec</string>
- </property>
- <property name="editable">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item row="0" column="3">
- <widget class="QLabel" name="labelTextQualifier">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Text is qualified by</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="0" column="4">
- <widget class="QComboBox" name="comboBoxTextQualifier">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>Text qualification</string>
- </property>
- <property name="editable">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="labelFieldSeparator">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Fields are separated by</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="1" column="1" colspan="2">
- <widget class="QComboBox" name="comboBoxFieldSeparator">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>Field separation</string>
- </property>
- <property name="editable">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item row="1" column="3">
- <widget class="QLabel" name="labelComments">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Comments start with</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="1" column="4">
- <widget class="QComboBox" name="comboBoxComment">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>Comments start with</string>
- </property>
- <property name="editable">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item row="2" column="0" colspan="2">
- <widget class="QCheckBox" name="checkBoxFieldNames">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>First record has field names</string>
- </property>
- </widget>
- </item>
- <item row="2" column="2">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLabel" name="labelSkipRows">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Number of header lines to discard</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QSpinBox" name="spinBoxSkip">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>Number of header lines to discard</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="2" column="3">
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>122</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item row="2" column="4">
- <widget class="QCheckBox" name="checkBoxBackslash">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="text">
- <string>Consider '\' an escape character</string>
- </property>
- </widget>
- </item>
- <item row="2" column="5">
- <widget class="QLabel" name="labelWarnings">
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- <kerning>true</kerning>
- </font>
- </property>
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="scrollAreaWidgetContents">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>753</width>
+ <height>615</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QLabel" name="labelHeadline">
+ <property name="font">
+ <font>
+ <pointsize>11</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Import CSV fields</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="labelFilename">
+ <property name="text">
+ <string>filename</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="labelSizeRowsCols">
+ <property name="text">
+ <string>size, rows, columns</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBoxColumnAssociations">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="title">
+ <string>Column Association</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <layout class="QGridLayout" name="gridLayout_combos">
+ <item row="2" column="2">
+ <widget class="QLabel" name="lastModifiedLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Last Modified</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="passwordLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Password</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QComboBox" name="passwordCombo"/>
+ </item>
+ <item row="3" column="2">
+ <widget class="QLabel" name="createdLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Created</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QLabel" name="notesLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Notes</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="titleLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Title</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="groupCombo"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="groupLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Group</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QLabel" name="urlLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>URL</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="usernameLabel">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Username</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="usernameCombo"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="titleCombo"/>
+ </item>
+ <item row="0" column="3">
+ <widget class="QComboBox" name="urlCombo"/>
+ </item>
+ <item row="1" column="3">
+ <widget class="QComboBox" name="notesCombo"/>
+ </item>
+ <item row="2" column="3">
+ <widget class="QComboBox" name="lastModifiedCombo"/>
+ </item>
+ <item row="3" column="3">
+ <widget class="QComboBox" name="createdCombo"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="Encoding">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="title">
+ <string>Encoding</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelCodec">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Codec</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="comboBoxCodec">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>Codec</string>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="currentText">
+ <string notr="true">UTF-8</string>
+ </property>
+ <item>
+ <property name="text">
+ <string notr="true">UTF-8</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">Windows-1252</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">UTF-16</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">UTF-16LE</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="labelTextQualifier">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Text is qualified by</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="comboBoxTextQualifier">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>Text qualification</string>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="currentText">
+ <string notr="true">&quot;</string>
+ </property>
+ <item>
+ <property name="text">
+ <string notr="true">&quot;</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">'</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">:</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">.</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">|</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="labelFieldSeparator">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Fields are separated by</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="comboBoxFieldSeparator">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>Field separation</string>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="currentText">
+ <string notr="true">,</string>
+ </property>
+ <item>
+ <property name="text">
+ <string notr="true">,</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">;</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">-</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">:</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">.</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">TAB (\t)</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="labelComments">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Comments start with</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QComboBox" name="comboBoxComment">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>Comments start with</string>
+ </property>
+ <property name="editable">
+ <bool>false</bool>
+ </property>
+ <property name="currentText">
+ <string notr="true">#</string>
+ </property>
+ <item>
+ <property name="text">
+ <string notr="true">#</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">;</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">:</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string notr="true">@</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="labelSkipRows">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Header lines skipped</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QSpinBox" name="spinBoxSkip">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>Number of header lines to discard</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>122</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="5" column="1">
+ <widget class="QCheckBox" name="checkBoxFieldNames">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>First line has field names</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QCheckBox" name="checkBoxBackslash">
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string>Consider '\' an escape character</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBoxPreview">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="title">
+ <string>Preview</string>
+ </property>
+ <property name="checkable">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTableView" name="tableViewFields">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>300</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <weight>50</weight>
+ <bold>false</bold>
+ </font>
+ </property>
+ <property name="accessibleName">
+ <string>CSV import preview</string>
+ </property>
+ <property name="cornerButtonEnabled">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderVisible">
+ <bool>true</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</widget>
</item>
- <item row="4" column="0" colspan="2">
- <widget class="QGroupBox" name="groupBoxPreview">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>200</height>
- </size>
- </property>
- <property name="font">
- <font>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="title">
- <string>Preview</string>
- </property>
- <property name="checkable">
- <bool>false</bool>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <item row="0" column="0">
- <widget class="QTableView" name="tableViewFields">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <weight>50</weight>
- <bold>false</bold>
- </font>
- </property>
- <property name="accessibleName">
- <string>CSV import preview</string>
- </property>
- <attribute name="horizontalHeaderVisible">
- <bool>true</bool>
- </attribute>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item row="5" column="0" colspan="2">
+ <item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
@@ -453,40 +666,6 @@
</property>
</widget>
</item>
- <item row="3" column="0" colspan="2">
- <widget class="QGroupBox" name="groupBoxColumnAssociations">
- <property name="sizePolicy">
- <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="font">
- <font>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="title">
- <string>Column layout</string>
- </property>
- <layout class="QGridLayout" name="gridLayout_3">
- <item row="0" column="0">
- <layout class="QGridLayout" name="gridLayout_combos">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- </layout>
- </item>
- </layout>
- </widget>
- </item>
</layout>
</widget>
<customwidgets>
@@ -497,6 +676,25 @@
<container>1</container>
</customwidget>
</customwidgets>
+ <tabstops>
+ <tabstop>scrollArea</tabstop>
+ <tabstop>groupCombo</tabstop>
+ <tabstop>titleCombo</tabstop>
+ <tabstop>usernameCombo</tabstop>
+ <tabstop>passwordCombo</tabstop>
+ <tabstop>urlCombo</tabstop>
+ <tabstop>notesCombo</tabstop>
+ <tabstop>lastModifiedCombo</tabstop>
+ <tabstop>createdCombo</tabstop>
+ <tabstop>comboBoxCodec</tabstop>
+ <tabstop>comboBoxTextQualifier</tabstop>
+ <tabstop>comboBoxFieldSeparator</tabstop>
+ <tabstop>comboBoxComment</tabstop>
+ <tabstop>spinBoxSkip</tabstop>
+ <tabstop>checkBoxFieldNames</tabstop>
+ <tabstop>checkBoxBackslash</tabstop>
+ <tabstop>tableViewFields</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/csvImport/CsvParserModel.cpp b/src/gui/csvImport/CsvParserModel.cpp
index a6c24667d..d18db87c5 100644
--- a/src/gui/csvImport/CsvParserModel.cpp
+++ b/src/gui/csvImport/CsvParserModel.cpp
@@ -55,8 +55,9 @@ bool CsvParserModel::parse()
QFile csv(m_filename);
r = CsvParser::parse(&csv);
}
- for (int i = 0; i < columnCount(); ++i)
+ for (int i = 0; i < columnCount(); ++i) {
m_columnMap.insert(i, 0);
+ }
addEmptyColumn();
endResetModel();
return r;
@@ -73,13 +74,15 @@ void CsvParserModel::addEmptyColumn()
void CsvParserModel::mapColumns(int csvColumn, int dbColumn)
{
- if ((csvColumn < 0) || (dbColumn < 0))
+ if ((csvColumn < 0) || (dbColumn < 0)) {
return;
+ }
beginResetModel();
- if (csvColumn >= getCsvCols())
+ if (csvColumn >= getCsvCols()) {
m_columnMap[dbColumn] = 0; // map to the empty column
- else
+ } else {
m_columnMap[dbColumn] = csvColumn;
+ }
endResetModel();
}
@@ -99,15 +102,17 @@ void CsvParserModel::setHeaderLabels(const QStringList& labels)
int CsvParserModel::rowCount(const QModelIndex& parent) const
{
- if (parent.isValid())
+ if (parent.isValid()) {
return 0;
+ }
return getCsvRows();
}
int CsvParserModel::columnCount(const QModelIndex& parent) const
{
- if (parent.isValid())
+ if (parent.isValid()) {
return 0;
+ }
return m_columnHeader.size();
}
@@ -116,8 +121,9 @@ QVariant CsvParserModel::data(const QModelIndex& index, int role) const
if ((index.column() >= m_columnHeader.size()) || (index.row() + m_skipped >= rowCount()) || !index.isValid()) {
return QVariant();
}
- if (role == Qt::DisplayRole)
+ if (role == Qt::DisplayRole) {
return m_table.at(index.row() + m_skipped).at(m_columnMap[index.column()]);
+ }
return QVariant();
}
@@ -125,12 +131,14 @@ QVariant CsvParserModel::headerData(int section, Qt::Orientation orientation, in
{
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
- if ((section < 0) || (section >= m_columnHeader.size()))
+ if ((section < 0) || (section >= m_columnHeader.size())) {
return QVariant();
+ }
return m_columnHeader.at(section);
} else if (orientation == Qt::Vertical) {
- if (section + m_skipped >= rowCount())
+ if (section + m_skipped >= rowCount()) {
return QVariant();
+ }
return QString::number(section + 1);
}
}
diff --git a/src/gui/masterkey/KeyComponentWidget.cpp b/src/gui/databasekey/KeyComponentWidget.cpp
index 769cbab95..769cbab95 100644
--- a/src/gui/masterkey/KeyComponentWidget.cpp
+++ b/src/gui/databasekey/KeyComponentWidget.cpp
diff --git a/src/gui/masterkey/KeyComponentWidget.h b/src/gui/databasekey/KeyComponentWidget.h
index b73f881fc..b73f881fc 100644
--- a/src/gui/masterkey/KeyComponentWidget.h
+++ b/src/gui/databasekey/KeyComponentWidget.h
diff --git a/src/gui/masterkey/KeyComponentWidget.ui b/src/gui/databasekey/KeyComponentWidget.ui
index 5036655e0..5036655e0 100644
--- a/src/gui/masterkey/KeyComponentWidget.ui
+++ b/src/gui/databasekey/KeyComponentWidget.ui
diff --git a/src/gui/masterkey/KeyFileEditWidget.cpp b/src/gui/databasekey/KeyFileEditWidget.cpp
index e6b5bef49..e0486ae07 100644
--- a/src/gui/masterkey/KeyFileEditWidget.cpp
+++ b/src/gui/databasekey/KeyFileEditWidget.cpp
@@ -52,7 +52,7 @@ bool KeyFileEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> key)
tr("Legacy key file format"),
tr("You are using a legacy key file format which may become\n"
"unsupported in the future.\n\n"
- "Please go to the master key settings and generate a new key file."),
+ "Generate a new key file in the database security settings."),
QMessageBox::Ok);
}
diff --git a/src/gui/masterkey/KeyFileEditWidget.h b/src/gui/databasekey/KeyFileEditWidget.h
index dd414e133..dd414e133 100644
--- a/src/gui/masterkey/KeyFileEditWidget.h
+++ b/src/gui/databasekey/KeyFileEditWidget.h
diff --git a/src/gui/masterkey/KeyFileEditWidget.ui b/src/gui/databasekey/KeyFileEditWidget.ui
index 088995dc8..088995dc8 100644
--- a/src/gui/masterkey/KeyFileEditWidget.ui
+++ b/src/gui/databasekey/KeyFileEditWidget.ui
diff --git a/src/gui/masterkey/PasswordEditWidget.cpp b/src/gui/databasekey/PasswordEditWidget.cpp
index 9353cbe7a..2d355cc10 100644
--- a/src/gui/masterkey/PasswordEditWidget.cpp
+++ b/src/gui/databasekey/PasswordEditWidget.cpp
@@ -18,7 +18,7 @@
#include "PasswordEditWidget.h"
#include "ui_PasswordEditWidget.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "gui/PasswordGeneratorWidget.h"
#include "keys/CompositeKey.h"
#include "keys/PasswordKey.h"
@@ -53,7 +53,7 @@ bool PasswordEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> key)
*/
void PasswordEditWidget::setPasswordVisible(bool visible)
{
- m_compUi->togglePasswordButton->setChecked(visible);
+ m_compUi->enterPasswordEdit->setShowPassword(visible);
}
/**
@@ -61,7 +61,7 @@ void PasswordEditWidget::setPasswordVisible(bool visible)
*/
bool PasswordEditWidget::isPasswordVisible() const
{
- return m_compUi->togglePasswordButton->isChecked();
+ return m_compUi->enterPasswordEdit->isPasswordVisible();
}
bool PasswordEditWidget::isEmpty() const
@@ -73,16 +73,8 @@ QWidget* PasswordEditWidget::componentEditWidget()
{
m_compEditWidget = new QWidget();
m_compUi->setupUi(m_compEditWidget);
- m_compUi->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
- m_compUi->passwordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false));
- m_compUi->repeatPasswordEdit->enableVerifyMode(m_compUi->enterPasswordEdit);
-
- connect(m_compUi->togglePasswordButton,
- SIGNAL(toggled(bool)),
- m_compUi->enterPasswordEdit,
- SLOT(setShowPassword(bool)));
- connect(m_compUi->passwordGeneratorButton, SIGNAL(clicked(bool)), SLOT(showPasswordGenerator()));
-
+ m_compUi->enterPasswordEdit->enablePasswordGenerator();
+ m_compUi->enterPasswordEdit->setRepeatPartner(m_compUi->repeatPasswordEdit);
return m_compEditWidget;
}
@@ -113,26 +105,6 @@ bool PasswordEditWidget::validate(QString& errorMessage) const
return true;
}
-void PasswordEditWidget::showPasswordGenerator()
-{
- QDialog pwDialog;
- pwDialog.setWindowTitle(tr("Generate master password"));
-
- auto layout = new QVBoxLayout();
- pwDialog.setLayout(layout);
-
- auto pwGenerator = new PasswordGeneratorWidget(&pwDialog);
- layout->addWidget(pwGenerator);
-
- pwGenerator->setStandaloneMode(false);
- connect(pwGenerator, SIGNAL(appliedPassword(QString)), SLOT(setPassword(QString)));
- connect(pwGenerator, SIGNAL(dialogTerminated()), &pwDialog, SLOT(close()));
-
- pwGenerator->setPasswordVisible(isPasswordVisible());
-
- pwDialog.exec();
-}
-
void PasswordEditWidget::setPassword(const QString& password)
{
Q_ASSERT(m_compEditWidget);
diff --git a/src/gui/masterkey/PasswordEditWidget.h b/src/gui/databasekey/PasswordEditWidget.h
index 57c225c1f..802219451 100644
--- a/src/gui/masterkey/PasswordEditWidget.h
+++ b/src/gui/databasekey/PasswordEditWidget.h
@@ -47,7 +47,6 @@ protected:
void hideEvent(QHideEvent* event) override;
private slots:
- void showPasswordGenerator();
void setPassword(const QString& password);
private:
diff --git a/src/gui/masterkey/PasswordEditWidget.ui b/src/gui/databasekey/PasswordEditWidget.ui
index d0a85eb59..d8382ed94 100644
--- a/src/gui/masterkey/PasswordEditWidget.ui
+++ b/src/gui/databasekey/PasswordEditWidget.ui
@@ -31,49 +31,26 @@
</widget>
</item>
<item row="0" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="PasswordEdit" name="enterPasswordEdit">
- <property name="accessibleName">
- <string>Password field</string>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="togglePasswordButton">
- <property name="accessibleName">
- <string>Toggle password visibility</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="1" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="PasswordEdit" name="repeatPasswordEdit">
- <property name="accessibleName">
- <string>Repeat password field</string>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="passwordGeneratorButton">
- <property name="accessibleName">
- <string>Toggle password generator</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="PasswordEdit" name="enterPasswordEdit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>300</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="accessibleName">
+ <string>Password field</string>
+ </property>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="repeatPasswordLabel">
@@ -82,6 +59,28 @@
</property>
</widget>
</item>
+ <item row="1" column="1">
+ <widget class="PasswordEdit" name="repeatPasswordEdit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>300</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="accessibleName">
+ <string>Repeat password field</string>
+ </property>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
<customwidgets>
@@ -95,8 +94,6 @@
<tabstops>
<tabstop>enterPasswordEdit</tabstop>
<tabstop>repeatPasswordEdit</tabstop>
- <tabstop>togglePasswordButton</tabstop>
- <tabstop>passwordGeneratorButton</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/src/gui/masterkey/YubiKeyEditWidget.cpp b/src/gui/databasekey/YubiKeyEditWidget.cpp
index 9ecd918ef..855bd7099 100644
--- a/src/gui/masterkey/YubiKeyEditWidget.cpp
+++ b/src/gui/databasekey/YubiKeyEditWidget.cpp
@@ -19,13 +19,12 @@
#include "ui_YubiKeyEditWidget.h"
#include "config-keepassx.h"
+#include "core/AsyncTask.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "keys/CompositeKey.h"
#include "keys/YkChallengeResponseKey.h"
-#include <QtConcurrent>
-
YubiKeyEditWidget::YubiKeyEditWidget(QWidget* parent)
: KeyComponentWidget(parent)
, m_compUi(new Ui::YubiKeyEditWidget())
@@ -36,6 +35,8 @@ YubiKeyEditWidget::YubiKeyEditWidget(QWidget* parent)
"for additional security.</p><p>The YubiKey requires one of its slots to be programmed as "
"<a href=\"https://www.yubico.com/products/services-software/personalization-tools/challenge-response/\">"
"HMAC-SHA1 Challenge-Response</a>.</p>"));
+
+ connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
}
YubiKeyEditWidget::~YubiKeyEditWidget()
@@ -44,24 +45,31 @@ YubiKeyEditWidget::~YubiKeyEditWidget()
bool YubiKeyEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> key)
{
- QSharedPointer<YkChallengeResponseKey> keyPtr;
- if (!createCrKey(keyPtr, false)) {
+ if (!m_isDetected || !m_compEditWidget) {
return false;
}
- key->addChallengeResponseKey(keyPtr);
+ int selectionIndex = m_compUi->comboChallengeResponse->currentIndex();
+ auto slot = m_compUi->comboChallengeResponse->itemData(selectionIndex).value<YubiKeySlot>();
+ key->addChallengeResponseKey(QSharedPointer<YkChallengeResponseKey>::create(slot));
return true;
}
bool YubiKeyEditWidget::validate(QString& errorMessage) const
{
- QSharedPointer<YkChallengeResponseKey> keyPtr;
- if (!createCrKey(keyPtr)) {
- errorMessage = tr("No YubiKey detected, please ensure it's plugged in.");
+ if (!m_isDetected) {
+ errorMessage = tr("Could not find any hardware keys!");
return false;
}
- return true;
+ // Perform a test challenge response
+ int selectionIndex = m_compUi->comboChallengeResponse->currentIndex();
+ auto slot = m_compUi->comboChallengeResponse->itemData(selectionIndex).value<YubiKeySlot>();
+ bool valid = AsyncTask::runAndWaitForFuture([&slot] { return YubiKey::instance()->testChallenge(slot); });
+ if (!valid) {
+ errorMessage = tr("Selected hardware key slot does not support challenge-response!");
+ }
+ return valid;
}
QWidget* YubiKeyEditWidget::componentEditWidget()
@@ -76,13 +84,6 @@ QWidget* YubiKeyEditWidget::componentEditWidget()
#ifdef WITH_XC_YUBIKEY
connect(m_compUi->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
-
- // clang-format off
- connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection);
- connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection);
- connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection);
- // clang-format on
-
pollYubikey();
#endif
@@ -105,72 +106,36 @@ void YubiKeyEditWidget::pollYubikey()
m_isDetected = false;
m_compUi->comboChallengeResponse->clear();
+ m_compUi->comboChallengeResponse->addItem(tr("Detecting hardware keys…"));
m_compUi->buttonRedetectYubikey->setEnabled(false);
m_compUi->comboChallengeResponse->setEnabled(false);
m_compUi->yubikeyProgress->setVisible(true);
- // YubiKey init is slow, detect asynchronously to not block the UI
- QtConcurrent::run(YubiKey::instance(), &YubiKey::detect);
+ YubiKey::instance()->findValidKeys();
#endif
}
-void YubiKeyEditWidget::yubikeyDetected(int slot, bool blocking)
+void YubiKeyEditWidget::hardwareKeyResponse(bool found)
{
-#ifdef WITH_XC_YUBIKEY
if (!m_compEditWidget) {
return;
}
- YkChallengeResponseKey yk(slot, blocking);
- // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
- m_compUi->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1u) | blocking));
- m_isDetected = true;
-#else
- Q_UNUSED(slot);
- Q_UNUSED(blocking);
-#endif
-}
-
-void YubiKeyEditWidget::yubikeyDetectComplete()
-{
- m_compUi->comboChallengeResponse->setEnabled(true);
- m_compUi->buttonRedetectYubikey->setEnabled(true);
- m_compUi->yubikeyProgress->setVisible(false);
-}
-void YubiKeyEditWidget::noYubikeyFound()
-{
-#ifdef WITH_XC_YUBIKEY
- if (!m_compEditWidget) {
- return;
- }
m_compUi->comboChallengeResponse->clear();
- m_compUi->comboChallengeResponse->setEnabled(false);
- m_compUi->comboChallengeResponse->addItem(tr("No YubiKey inserted."));
m_compUi->buttonRedetectYubikey->setEnabled(true);
m_compUi->yubikeyProgress->setVisible(false);
- m_isDetected = false;
-#endif
-}
-bool YubiKeyEditWidget::createCrKey(QSharedPointer<YkChallengeResponseKey>& key, bool testChallenge) const
-{
- Q_ASSERT(m_compEditWidget);
- if (!m_isDetected || !m_compEditWidget) {
- return false;
+ if (!found) {
+ m_compUi->comboChallengeResponse->addItem(tr("No hardware keys detected"));
+ m_isDetected = false;
+ return;
}
- int selectionIndex = m_compUi->comboChallengeResponse->currentIndex();
- int comboPayload = m_compUi->comboChallengeResponse->itemData(selectionIndex).toInt();
-
- if (0 == comboPayload) {
- return false;
+ for (auto& slot : YubiKey::instance()->foundKeys()) {
+ // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
+ m_compUi->comboChallengeResponse->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot));
}
- auto blocking = static_cast<bool>(comboPayload & 1u);
- int slot = comboPayload >> 1u;
- key.reset(new YkChallengeResponseKey(slot, blocking));
- if (testChallenge) {
- return key->challenge(QByteArray("0000"));
- }
- return true;
+ m_isDetected = true;
+ m_compUi->comboChallengeResponse->setEnabled(true);
}
diff --git a/src/gui/masterkey/YubiKeyEditWidget.h b/src/gui/databasekey/YubiKeyEditWidget.h
index 549f7f44f..d5be0a683 100644
--- a/src/gui/masterkey/YubiKeyEditWidget.h
+++ b/src/gui/databasekey/YubiKeyEditWidget.h
@@ -45,14 +45,10 @@ protected:
void initComponentEditWidget(QWidget* widget) override;
private slots:
- void yubikeyDetected(int slot, bool blocking);
- void yubikeyDetectComplete();
- void noYubikeyFound();
+ void hardwareKeyResponse(bool found);
void pollYubikey();
private:
- bool createCrKey(QSharedPointer<YkChallengeResponseKey>& key, bool testChallenge = true) const;
-
const QScopedPointer<Ui::YubiKeyEditWidget> m_compUi;
QPointer<QWidget> m_compEditWidget;
bool m_isDetected = false;
diff --git a/src/gui/masterkey/YubiKeyEditWidget.ui b/src/gui/databasekey/YubiKeyEditWidget.ui
index fa150084b..fa150084b 100644
--- a/src/gui/masterkey/YubiKeyEditWidget.ui
+++ b/src/gui/databasekey/YubiKeyEditWidget.ui
diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp
index 33c4df2c4..32a9b74c2 100644
--- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp
@@ -19,10 +19,9 @@
#include "DatabaseSettingsDialog.h"
#include "ui_DatabaseSettingsDialog.h"
-#include "DatabaseSettingsPageStatistics.h"
+#include "DatabaseSettingsWidgetDatabaseKey.h"
#include "DatabaseSettingsWidgetEncryption.h"
#include "DatabaseSettingsWidgetGeneral.h"
-#include "DatabaseSettingsWidgetMasterKey.h"
#ifdef WITH_XC_BROWSER
#include "DatabaseSettingsWidgetBrowser.h"
#endif
@@ -35,8 +34,8 @@
#include "core/Config.h"
#include "core/Database.h"
-#include "core/FilePath.h"
#include "core/Global.h"
+#include "core/Resources.h"
#include "touchid/TouchID.h"
class DatabaseSettingsDialog::ExtraPage
@@ -66,7 +65,7 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
, m_ui(new Ui::DatabaseSettingsDialog())
, m_generalWidget(new DatabaseSettingsWidgetGeneral(this))
, m_securityTabWidget(new QTabWidget(this))
- , m_masterKeyWidget(new DatabaseSettingsWidgetMasterKey(this))
+ , m_databaseKeyWidget(new DatabaseSettingsWidgetDatabaseKey(this))
, m_encryptionWidget(new DatabaseSettingsWidgetEncryption(this))
#ifdef WITH_XC_BROWSER
, m_browserWidget(new DatabaseSettingsWidgetBrowser(this))
@@ -77,16 +76,14 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(save()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
- m_ui->categoryList->addCategory(tr("General"), FilePath::instance()->icon("categories", "preferences-other"));
- m_ui->categoryList->addCategory(tr("Security"), FilePath::instance()->icon("status", "security-high"));
+ m_ui->categoryList->addCategory(tr("General"), Resources::instance()->icon("preferences-other"));
+ m_ui->categoryList->addCategory(tr("Security"), Resources::instance()->icon("security-high"));
m_ui->stackedWidget->addWidget(m_generalWidget);
m_ui->stackedWidget->addWidget(m_securityTabWidget);
- m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key"));
+ m_securityTabWidget->addTab(m_databaseKeyWidget, tr("Database Credentials"));
m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings"));
- addSettingsPage(new DatabaseSettingsPageStatistics());
-
#if defined(WITH_XC_KEESHARE)
addSettingsPage(new DatabaseSettingsPageKeeShare());
#endif
@@ -103,8 +100,7 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent)
connect(m_ui->advancedSettingsToggle, SIGNAL(toggled(bool)), SLOT(toggleAdvancedMode(bool)));
#ifdef WITH_XC_BROWSER
- m_ui->categoryList->addCategory(tr("Browser Integration"),
- FilePath::instance()->icon("apps", "internet-web-browser"));
+ m_ui->categoryList->addCategory(tr("Browser Integration"), Resources::instance()->icon("internet-web-browser"));
m_ui->stackedWidget->addWidget(m_browserWidget);
#endif
@@ -119,7 +115,7 @@ void DatabaseSettingsDialog::load(const QSharedPointer<Database>& db)
{
m_ui->categoryList->setCurrentCategory(0);
m_generalWidget->load(db);
- m_masterKeyWidget->load(db);
+ m_databaseKeyWidget->load(db);
m_encryptionWidget->load(db);
#ifdef WITH_XC_BROWSER
m_browserWidget->load(db);
@@ -127,7 +123,7 @@ void DatabaseSettingsDialog::load(const QSharedPointer<Database>& db)
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(db);
}
- m_ui->advancedSettingsToggle->setChecked(config()->get("GUI/AdvancedSettings", false).toBool());
+ m_ui->advancedSettingsToggle->setChecked(config()->get(Config::GUI_AdvancedSettings).toBool());
m_db = db;
}
@@ -143,9 +139,9 @@ void DatabaseSettingsDialog::addSettingsPage(IDatabaseSettingsPage* page)
}
/**
- * Show page and tab with database master key settings.
+ * Show page and tab with database database key settings.
*/
-void DatabaseSettingsDialog::showMasterKeySettings()
+void DatabaseSettingsDialog::showDatabaseKeySettings()
{
m_ui->categoryList->setCurrentCategory(1);
m_securityTabWidget->setCurrentIndex(0);
@@ -157,7 +153,7 @@ void DatabaseSettingsDialog::save()
return;
}
- if (!m_masterKeyWidget->save()) {
+ if (!m_databaseKeyWidget->save()) {
return;
}
@@ -189,7 +185,7 @@ void DatabaseSettingsDialog::pageChanged()
if (Page::Security == pageIndex) {
int tabIndex = m_securityTabWidget->currentIndex();
- enabled = (tabIndex == 0 && m_masterKeyWidget->hasAdvancedMode());
+ enabled = (tabIndex == 0 && m_databaseKeyWidget->hasAdvancedMode());
enabled |= (tabIndex == 1 && m_encryptionWidget->hasAdvancedMode());
}
@@ -202,13 +198,13 @@ void DatabaseSettingsDialog::toggleAdvancedMode(bool advanced)
m_generalWidget->setAdvancedMode(advanced);
}
- if (m_masterKeyWidget->hasAdvancedMode()) {
- m_masterKeyWidget->setAdvancedMode(advanced);
+ if (m_databaseKeyWidget->hasAdvancedMode()) {
+ m_databaseKeyWidget->setAdvancedMode(advanced);
}
if (m_encryptionWidget->hasAdvancedMode()) {
m_encryptionWidget->setAdvancedMode(advanced);
}
- config()->set("GUI/AdvancedSettings", advanced);
+ config()->set(Config::GUI_AdvancedSettings, advanced);
}
diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.h b/src/gui/dbsettings/DatabaseSettingsDialog.h
index 855978f29..ba708c65b 100644
--- a/src/gui/dbsettings/DatabaseSettingsDialog.h
+++ b/src/gui/dbsettings/DatabaseSettingsDialog.h
@@ -28,7 +28,7 @@
class Database;
class DatabaseSettingsWidgetGeneral;
class DatabaseSettingsWidgetEncryption;
-class DatabaseSettingsWidgetMasterKey;
+class DatabaseSettingsWidgetDatabaseKey;
#ifdef WITH_XC_BROWSER
class DatabaseSettingsWidgetBrowser;
#endif
@@ -63,7 +63,7 @@ public:
void load(const QSharedPointer<Database>& db);
void addSettingsPage(IDatabaseSettingsPage* page);
- void showMasterKeySettings();
+ void showDatabaseKeySettings();
signals:
void editFinished(bool accepted);
@@ -85,7 +85,7 @@ private:
const QScopedPointer<Ui::DatabaseSettingsDialog> m_ui;
QPointer<DatabaseSettingsWidgetGeneral> m_generalWidget;
QPointer<QTabWidget> m_securityTabWidget;
- QPointer<DatabaseSettingsWidgetMasterKey> m_masterKeyWidget;
+ QPointer<DatabaseSettingsWidgetDatabaseKey> m_databaseKeyWidget;
QPointer<DatabaseSettingsWidgetEncryption> m_encryptionWidget;
#ifdef WITH_XC_BROWSER
QPointer<DatabaseSettingsWidgetBrowser> m_browserWidget;
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
index 4ea30c1f6..746e6a66e 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
@@ -34,7 +34,6 @@ DatabaseSettingsWidgetBrowser::DatabaseSettingsWidgetBrowser(QWidget* parent)
, m_ui(new Ui::DatabaseSettingsWidgetBrowser())
, m_customData(new CustomData(this))
, m_customDataModel(new QStandardItemModel(this))
- , m_browserService(nullptr)
{
m_ui->setupUi(this);
m_ui->removeCustomDataButton->setEnabled(false);
@@ -46,6 +45,8 @@ DatabaseSettingsWidgetBrowser::DatabaseSettingsWidgetBrowser(QWidget* parent)
connect(m_ui->customDataTable->selectionModel(),
SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
SLOT(toggleRemoveButton(QItemSelection)));
+ connect(m_ui->customDataTable, SIGNAL(doubleClicked(QModelIndex)), SLOT(editIndex(QModelIndex)));
+ connect(m_customDataModel, SIGNAL(itemChanged(QStandardItem*)), SLOT(editFinished(QStandardItem*)));
// clang-format on
connect(m_ui->removeCustomDataButton, SIGNAL(clicked()), SLOT(removeSelectedKey()));
@@ -54,6 +55,7 @@ DatabaseSettingsWidgetBrowser::DatabaseSettingsWidgetBrowser(QWidget* parent)
connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SLOT(removeSharedEncryptionKeys()));
connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SLOT(updateSharedKeyList()));
connect(m_ui->removeStoredPermissions, SIGNAL(clicked()), this, SLOT(removeStoredPermissions()));
+ connect(m_ui->refreshDatabaseID, SIGNAL(clicked()), this, SLOT(refreshDatabaseID()));
}
DatabaseSettingsWidgetBrowser::~DatabaseSettingsWidgetBrowser()
@@ -105,7 +107,7 @@ void DatabaseSettingsWidgetBrowser::removeSelectedKey()
if (itemSelectionModel) {
for (const QModelIndex& index : itemSelectionModel->selectedRows(0)) {
QString key = index.data().toString();
- key.insert(0, BrowserService::ASSOCIATE_KEY_PREFIX);
+ key.insert(0, CustomData::BrowserKeyPrefix);
customData()->remove(key);
}
updateModel();
@@ -120,14 +122,18 @@ void DatabaseSettingsWidgetBrowser::toggleRemoveButton(const QItemSelection& sel
void DatabaseSettingsWidgetBrowser::updateModel()
{
m_customDataModel->clear();
- m_customDataModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")});
+ m_customDataModel->setHorizontalHeaderLabels({tr("Key"), tr("Value"), tr("Created")});
for (const QString& key : customData()->keys()) {
- if (key.startsWith(BrowserService::ASSOCIATE_KEY_PREFIX)) {
+ if (key.startsWith(CustomData::BrowserKeyPrefix)) {
QString strippedKey = key;
- strippedKey.remove(BrowserService::ASSOCIATE_KEY_PREFIX);
- m_customDataModel->appendRow(QList<QStandardItem*>() << new QStandardItem(strippedKey)
- << new QStandardItem(customData()->value(key)));
+ strippedKey.remove(CustomData::BrowserKeyPrefix);
+ auto created = customData()->value(QString("%1_%2").arg(CustomData::Created, strippedKey));
+ auto createdItem = new QStandardItem(created);
+ createdItem->setEditable(false);
+ m_customDataModel->appendRow(QList<QStandardItem*>()
+ << new QStandardItem(strippedKey)
+ << new QStandardItem(customData()->value(key)) << createdItem);
}
}
@@ -168,7 +174,7 @@ void DatabaseSettingsWidgetBrowser::removeSharedEncryptionKeys()
QStringList keysToRemove;
for (const QString& key : m_db->metadata()->customData()->keys()) {
- if (key.startsWith(BrowserService::ASSOCIATE_KEY_PREFIX)) {
+ if (key.startsWith(CustomData::BrowserKeyPrefix)) {
keysToRemove << key;
}
}
@@ -251,7 +257,69 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData()
return;
}
- m_browserService.convertAttributesToCustomData(m_db);
+ BrowserService::convertAttributesToCustomData(m_db);
+}
+
+void DatabaseSettingsWidgetBrowser::refreshDatabaseID()
+{
+ if (MessageBox::Yes
+ != MessageBox::question(this,
+ tr("Refresh database ID"),
+ tr("Do you really want refresh the database ID?\n"
+ "This is only necessary if your database is a copy of another and the "
+ "browser extension cannot connect."),
+ MessageBox::Yes | MessageBox::Cancel,
+ MessageBox::Cancel)) {
+ return;
+ }
+
+ m_db->rootGroup()->setUuid(QUuid::createUuid());
+}
+
+void DatabaseSettingsWidgetBrowser::editIndex(const QModelIndex& index)
+{
+ Q_ASSERT(index.isValid());
+ if (!index.isValid()) {
+ return;
+ }
+
+ m_valueInEdit = index.data().toString();
+ m_ui->customDataTable->edit(index);
+}
+
+void DatabaseSettingsWidgetBrowser::editFinished(QStandardItem* item)
+{
+ const QItemSelectionModel* itemSelectionModel = m_ui->customDataTable->selectionModel();
+
+ if (itemSelectionModel) {
+ auto indexList = itemSelectionModel->selectedRows(item->column());
+ if (indexList.length() > 0) {
+ QString newValue = item->index().data().toString();
+
+ // The key is edited
+ if (item->column() == 0) {
+ // Get the old key/value pair, remove it and replace it
+ m_valueInEdit.insert(0, CustomData::BrowserKeyPrefix);
+ auto tempValue = customData()->value(m_valueInEdit);
+ newValue.insert(0, CustomData::BrowserKeyPrefix);
+
+ m_db->metadata()->customData()->remove(m_valueInEdit);
+ m_db->metadata()->customData()->set(newValue, tempValue);
+ } else {
+ // Replace just the value
+ for (const QString& key : m_db->metadata()->customData()->keys()) {
+ if (key.startsWith(CustomData::BrowserKeyPrefix)) {
+ if (m_valueInEdit == m_db->metadata()->customData()->value(key)) {
+ m_db->metadata()->customData()->set(key, newValue);
+ break;
+ }
+ }
+ }
+ }
+
+ updateModel();
+ }
+ }
}
// Updates the shared key list after the list is cleared
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h
index d9d9885ca..369c8640f 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.h
@@ -63,6 +63,9 @@ private slots:
void removeSharedEncryptionKeys();
void removeStoredPermissions();
void convertAttributesToCustomData();
+ void refreshDatabaseID();
+ void editIndex(const QModelIndex& index);
+ void editFinished(QStandardItem* item);
private:
void updateModel();
@@ -76,7 +79,7 @@ protected:
private:
QPointer<CustomData> m_customData;
QPointer<QStandardItemModel> m_customDataModel;
- BrowserService m_browserService;
+ QString m_valueInEdit;
};
#endif // KEEPASSXC_DATABASESETTINGSWIDGETBROWSER_H
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui
index 463f572d5..c67a5134b 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>453</width>
- <height>374</height>
+ <width>669</width>
+ <height>395</height>
</rect>
</property>
<property name="sizePolicy">
@@ -50,53 +50,58 @@
<property name="title">
<string>KeePassXC-Browser settings</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QPushButton" name="removeSharedEncryptionKeys">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>&amp;Disconnect all browsers</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="removeStoredPermissions">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Forg&amp;et all site-specific settings on entries</string>
- </property>
- </widget>
- </item>
- </layout>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0">
+ <widget class="QPushButton" name="convertToCustomData">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Move KeePassHTTP attributes to KeePassXC-Browser custom data</string>
+ </property>
+ </widget>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <widget class="QPushButton" name="convertToCustomData">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Move KeePassHTTP attributes to KeePassXC-Browser &amp;custom data</string>
- </property>
- </widget>
- </item>
- </layout>
+ <item row="2" column="1">
+ <widget class="QPushButton" name="refreshDatabaseID">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Refresh database root group ID</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QPushButton" name="removeSharedEncryptionKeys">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Disconnect all browsers</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QPushButton" name="removeStoredPermissions">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Forget all site-specific settings on entries</string>
+ </property>
+ </widget>
</item>
</layout>
</widget>
@@ -106,10 +111,10 @@
<property name="title">
<string>Stored keys</string>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QGroupBox" name="groupBox_3">
- <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QTableView" name="customDataTable">
<property name="accessibleName">
@@ -175,6 +180,14 @@
<container>1</container>
</customwidget>
</customwidgets>
+ <tabstops>
+ <tabstop>removeSharedEncryptionKeys</tabstop>
+ <tabstop>removeStoredPermissions</tabstop>
+ <tabstop>convertToCustomData</tabstop>
+ <tabstop>refreshDatabaseID</tabstop>
+ <tabstop>customDataTable</tabstop>
+ <tabstop>removeCustomDataButton</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp
index a58ee701f..2d733d06d 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp
@@ -15,13 +15,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include "DatabaseSettingsWidgetMasterKey.h"
+#include "DatabaseSettingsWidgetDatabaseKey.h"
#include "core/Database.h"
#include "gui/MessageBox.h"
-#include "gui/masterkey/KeyFileEditWidget.h"
-#include "gui/masterkey/PasswordEditWidget.h"
-#include "gui/masterkey/YubiKeyEditWidget.h"
+#include "gui/databasekey/KeyFileEditWidget.h"
+#include "gui/databasekey/PasswordEditWidget.h"
+#include "gui/databasekey/YubiKeyEditWidget.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#include "keys/YkChallengeResponseKey.h"
@@ -30,7 +30,7 @@
#include <QSpacerItem>
#include <QVBoxLayout>
-DatabaseSettingsWidgetMasterKey::DatabaseSettingsWidgetMasterKey(QWidget* parent)
+DatabaseSettingsWidgetDatabaseKey::DatabaseSettingsWidgetDatabaseKey(QWidget* parent)
: DatabaseSettingsWidget(parent)
, m_additionalKeyOptionsToggle(new QPushButton(tr("Add additional protection..."), this))
, m_additionalKeyOptions(new QWidget(this))
@@ -65,11 +65,11 @@ DatabaseSettingsWidgetMasterKey::DatabaseSettingsWidgetMasterKey(QWidget* parent
setLayout(vbox);
}
-DatabaseSettingsWidgetMasterKey::~DatabaseSettingsWidgetMasterKey()
+DatabaseSettingsWidgetDatabaseKey::~DatabaseSettingsWidgetDatabaseKey()
{
}
-void DatabaseSettingsWidgetMasterKey::load(QSharedPointer<Database> db)
+void DatabaseSettingsWidgetDatabaseKey::load(QSharedPointer<Database> db)
{
DatabaseSettingsWidget::load(db);
@@ -107,7 +107,7 @@ void DatabaseSettingsWidgetMasterKey::load(QSharedPointer<Database> db)
#endif
}
-void DatabaseSettingsWidgetMasterKey::initialize()
+void DatabaseSettingsWidgetDatabaseKey::initialize()
{
bool blocked = blockSignals(true);
m_passwordEditWidget->setComponentAdded(false);
@@ -118,11 +118,11 @@ void DatabaseSettingsWidgetMasterKey::initialize()
blockSignals(blocked);
}
-void DatabaseSettingsWidgetMasterKey::uninitialize()
+void DatabaseSettingsWidgetDatabaseKey::uninitialize()
{
}
-bool DatabaseSettingsWidgetMasterKey::save()
+bool DatabaseSettingsWidgetDatabaseKey::save()
{
m_isDirty |= (m_passwordEditWidget->visiblePage() == KeyComponentWidget::Page::Edit);
m_isDirty |= (m_keyFileEditWidget->visiblePage() == KeyComponentWidget::Page::Edit);
@@ -202,17 +202,17 @@ bool DatabaseSettingsWidgetMasterKey::save()
return true;
}
-void DatabaseSettingsWidgetMasterKey::discard()
+void DatabaseSettingsWidgetDatabaseKey::discard()
{
emit editFinished(false);
}
-void DatabaseSettingsWidgetMasterKey::showAdditionalKeyOptions()
+void DatabaseSettingsWidgetDatabaseKey::showAdditionalKeyOptions()
{
setAdditionalKeyOptionsVisible(true);
}
-void DatabaseSettingsWidgetMasterKey::setAdditionalKeyOptionsVisible(bool show)
+void DatabaseSettingsWidgetDatabaseKey::setAdditionalKeyOptionsVisible(bool show)
{
m_additionalKeyOptionsToggle->setVisible(!show);
m_additionalKeyOptions->setVisible(show);
@@ -220,14 +220,14 @@ void DatabaseSettingsWidgetMasterKey::setAdditionalKeyOptionsVisible(bool show)
emit sizeChanged();
}
-bool DatabaseSettingsWidgetMasterKey::addToCompositeKey(KeyComponentWidget* widget,
- QSharedPointer<CompositeKey>& newKey,
- QSharedPointer<Key>& oldKey)
+bool DatabaseSettingsWidgetDatabaseKey::addToCompositeKey(KeyComponentWidget* widget,
+ QSharedPointer<CompositeKey>& newKey,
+ QSharedPointer<Key>& oldKey)
{
if (widget->visiblePage() == KeyComponentWidget::Edit) {
QString error = tr("Unknown error");
if (!widget->validate(error) || !widget->addToCompositeKey(newKey)) {
- MessageBox::critical(this, tr("Failed to change master key"), error, MessageBox::Ok);
+ MessageBox::critical(this, tr("Failed to change database credentials"), error, MessageBox::Ok);
return false;
}
} else if (widget->visiblePage() == KeyComponentWidget::LeaveOrRemove) {
@@ -237,14 +237,14 @@ bool DatabaseSettingsWidgetMasterKey::addToCompositeKey(KeyComponentWidget* widg
return true;
}
-bool DatabaseSettingsWidgetMasterKey::addToCompositeKey(KeyComponentWidget* widget,
- QSharedPointer<CompositeKey>& newKey,
- QSharedPointer<ChallengeResponseKey>& oldKey)
+bool DatabaseSettingsWidgetDatabaseKey::addToCompositeKey(KeyComponentWidget* widget,
+ QSharedPointer<CompositeKey>& newKey,
+ QSharedPointer<ChallengeResponseKey>& oldKey)
{
if (widget->visiblePage() == KeyComponentWidget::Edit) {
QString error = tr("Unknown error");
if (!widget->validate(error) || !widget->addToCompositeKey(newKey)) {
- MessageBox::critical(this, tr("Failed to change master key"), error, MessageBox::Ok);
+ MessageBox::critical(this, tr("Failed to change database credentials"), error, MessageBox::Ok);
return false;
}
} else if (widget->visiblePage() == KeyComponentWidget::LeaveOrRemove) {
@@ -254,7 +254,7 @@ bool DatabaseSettingsWidgetMasterKey::addToCompositeKey(KeyComponentWidget* widg
return true;
}
-void DatabaseSettingsWidgetMasterKey::markDirty()
+void DatabaseSettingsWidgetDatabaseKey::markDirty()
{
m_isDirty = true;
}
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.h b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.h
index 1a3d6ed3f..a7d90e43e 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.h
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.h
@@ -15,8 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef KEEPASSXC_DATABASESETTINGSPAGECHANGEMASTERKEY_H
-#define KEEPASSXC_DATABASESETTINGSPAGECHANGEMASTERKEY_H
+#ifndef KEEPASSXC_DATABASESETTINGSPAGECHANGEDBKEY_H
+#define KEEPASSXC_DATABASESETTINGSPAGECHANGEDBKEY_H
#include "DatabaseSettingsWidget.h"
#include "config-keepassx.h"
@@ -33,14 +33,14 @@ class KeyFileEditWidget;
class YubiKeyEditWidget;
class QPushButton;
-class DatabaseSettingsWidgetMasterKey : public DatabaseSettingsWidget
+class DatabaseSettingsWidgetDatabaseKey : public DatabaseSettingsWidget
{
Q_OBJECT
public:
- explicit DatabaseSettingsWidgetMasterKey(QWidget* parent = nullptr);
- Q_DISABLE_COPY(DatabaseSettingsWidgetMasterKey);
- ~DatabaseSettingsWidgetMasterKey() override;
+ explicit DatabaseSettingsWidgetDatabaseKey(QWidget* parent = nullptr);
+ Q_DISABLE_COPY(DatabaseSettingsWidgetDatabaseKey);
+ ~DatabaseSettingsWidgetDatabaseKey() override;
void load(QSharedPointer<Database> db) override;
@@ -82,4 +82,4 @@ private:
#endif
};
-#endif // KEEPASSXC_DATABASESETTINGSPAGECHANGEMASTERKEY_H
+#endif // KEEPASSXC_DATABASESETTINGSPAGECHANGEDBKEY_H
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
index e2a8cdafe..cc57e453a 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
@@ -45,9 +45,17 @@ DatabaseSettingsWidgetEncryption::DatabaseSettingsWidgetEncryption(QWidget* pare
m_ui->compatibilitySelection->addItem(tr("KDBX 4.0 (recommended)"), KeePass2::KDF_ARGON2.toByteArray());
m_ui->compatibilitySelection->addItem(tr("KDBX 3.1"), KeePass2::KDF_AES_KDBX3.toByteArray());
- m_ui->decryptionTimeSlider->setValue(10);
+ m_ui->decryptionTimeSlider->setMinimum(Kdf::MIN_ENCRYPTION_TIME / 100);
+ m_ui->decryptionTimeSlider->setMaximum(Kdf::MAX_ENCRYPTION_TIME / 100);
+ m_ui->decryptionTimeSlider->setValue(Kdf::DEFAULT_ENCRYPTION_TIME / 100);
updateDecryptionTime(m_ui->decryptionTimeSlider->value());
+ m_ui->transformBenchmarkButton->setText(
+ QObject::tr("Benchmark %1 delay")
+ .arg(DatabaseSettingsWidgetEncryption::getTextualEncryptionTime(Kdf::DEFAULT_ENCRYPTION_TIME)));
+ m_ui->minTimeLabel->setText(DatabaseSettingsWidgetEncryption::getTextualEncryptionTime(Kdf::MIN_ENCRYPTION_TIME));
+ m_ui->maxTimeLabel->setText(DatabaseSettingsWidgetEncryption::getTextualEncryptionTime(Kdf::MAX_ENCRYPTION_TIME));
+
connect(m_ui->activateChangeDecryptionTimeButton, SIGNAL(clicked()), SLOT(activateChangeDecryptionTime()));
connect(m_ui->decryptionTimeSlider, SIGNAL(valueChanged(int)), SLOT(updateDecryptionTime(int)));
connect(m_ui->compatibilitySelection, SIGNAL(currentIndexChanged(int)), SLOT(updateFormatCompatibility(int)));
@@ -373,11 +381,7 @@ void DatabaseSettingsWidgetEncryption::setAdvancedMode(bool advanced)
void DatabaseSettingsWidgetEncryption::updateDecryptionTime(int value)
{
- if (value < 10) {
- m_ui->decryptionTimeValueLabel->setText(tr("%1 ms", "milliseconds", value * 100).arg(value * 100));
- } else {
- m_ui->decryptionTimeValueLabel->setText(tr("%1 s", "seconds", value / 10).arg(value / 10.0, 0, 'f', 1));
- }
+ m_ui->decryptionTimeValueLabel->setText(DatabaseSettingsWidgetEncryption::getTextualEncryptionTime(value * 100));
}
void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool retransform)
@@ -409,3 +413,12 @@ void DatabaseSettingsWidgetEncryption::updateFormatCompatibility(int index, bool
activateChangeDecryptionTime();
}
}
+
+QString DatabaseSettingsWidgetEncryption::getTextualEncryptionTime(int millisecs)
+{
+ if (millisecs < 1000) {
+ return QObject::tr("%1 ms", "milliseconds", millisecs).arg(millisecs);
+ } else {
+ return QObject::tr("%1 s", "seconds", millisecs / 1000).arg(millisecs / 1000.0, 0, 'f', 1);
+ }
+}
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
index 986a33b6a..69388da6b 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.h
@@ -20,6 +20,8 @@
#include "DatabaseSettingsWidget.h"
+#include "crypto/kdf/Kdf.h"
+
#include <QPointer>
#include <QScopedPointer>
@@ -49,11 +51,13 @@ public slots:
void uninitialize() override;
bool save() override;
+ static QString getTextualEncryptionTime(int millisecs);
+
protected:
void showEvent(QShowEvent* event) override;
private slots:
- void benchmarkTransformRounds(int millisecs = 1000);
+ void benchmarkTransformRounds(int millisecs = Kdf::DEFAULT_ENCRYPTION_TIME);
void changeKdf(int index);
void memoryChanged(int value);
void parallelismChanged(int value);
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
index f8ba579dc..97da37475 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.ui
@@ -95,12 +95,6 @@
<property name="accessibleName">
<string>Decryption time in seconds</string>
</property>
- <property name="minimum">
- <number>1</number>
- </property>
- <property name="maximum">
- <number>50</number>
- </property>
<property name="singleStep">
<number>1</number>
</property>
@@ -124,9 +118,9 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
- <widget class="QLabel" name="label_4">
+ <widget class="QLabel" name="minTimeLabel">
<property name="text">
- <string>100 ms</string>
+ <string>?? ms</string>
</property>
</widget>
</item>
@@ -144,9 +138,9 @@
</spacer>
</item>
<item>
- <widget class="QLabel" name="label_3">
+ <widget class="QLabel" name="maxTimeLabel">
<property name="text">
- <string>5 s</string>
+ <string>? s</string>
</property>
</widget>
</item>
@@ -326,9 +320,6 @@
<property name="focusPolicy">
<enum>Qt::WheelFocus</enum>
</property>
- <property name="text">
- <string>Benchmark 1-second delay</string>
- </property>
</widget>
</item>
<item>
@@ -429,6 +420,17 @@
</item>
</layout>
</widget>
+ <tabstops>
+ <tabstop>activateChangeDecryptionTimeButton</tabstop>
+ <tabstop>decryptionTimeSlider</tabstop>
+ <tabstop>compatibilitySelection</tabstop>
+ <tabstop>algorithmComboBox</tabstop>
+ <tabstop>kdfComboBox</tabstop>
+ <tabstop>transformRoundsSpinBox</tabstop>
+ <tabstop>transformBenchmarkButton</tabstop>
+ <tabstop>memorySpinBox</tabstop>
+ <tabstop>parallelismSpinBox</tabstop>
+ </tabstops>
<resources/>
<connections/>
</ui>
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui b/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui
index 02f07952b..29349282b 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui
@@ -183,7 +183,7 @@
<item>
<widget class="QCheckBox" name="compressionCheckbox">
<property name="text">
- <string>Enable &amp;compression (recommended)</string>
+ <string>Enable compression (recommended)</string>
</property>
<property name="checked">
<bool>true</bool>
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui b/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui
deleted file mode 100644
index ed9d6346e..000000000
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>DatabaseSettingsWidgetStatistics</class>
- <widget class="QWidget" name="DatabaseSettingsWidgetStatistics">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>327</width>
- <height>379</height>
- </rect>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout" stretch="0">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <item>
- <widget class="QGroupBox" name="statisticsGroupBox">
- <property name="title">
- <string>Statistics</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QTableView" name="statisticsTableView">
- <property name="editTriggers">
- <set>QAbstractItemView::NoEditTriggers</set>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>false</bool>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="textElideMode">
- <enum>Qt::ElideMiddle</enum>
- </property>
- <property name="sortingEnabled">
- <bool>false</bool>
- </property>
- <attribute name="horizontalHeaderVisible">
- <bool>false</bool>
- </attribute>
- <attribute name="horizontalHeaderStretchLastSection">
- <bool>true</bool>
- </attribute>
- <attribute name="verticalHeaderVisible">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="tipLabel">
- <property name="font">
- <font>
- <italic>true</italic>
- </font>
- </property>
- <property name="text">
- <string>Hover over lines with error icons for further information.</string>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </widget>
- <resources/>
- <connections/>
-</ui>
diff --git a/src/gui/entry/AutoTypeMatchModel.cpp b/src/gui/entry/AutoTypeMatchModel.cpp
index 4148ccc66..ac2d0f874 100644
--- a/src/gui/entry/AutoTypeMatchModel.cpp
+++ b/src/gui/entry/AutoTypeMatchModel.cpp
@@ -118,15 +118,11 @@ QVariant AutoTypeMatchModel::data(const QModelIndex& index, int role) const
switch (index.column()) {
case ParentGroup:
if (match.entry->group()) {
- return match.entry->group()->iconScaledPixmap();
+ return match.entry->group()->iconPixmap();
}
break;
case Title:
- if (match.entry->isExpired()) {
- return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex);
- } else {
- return match.entry->iconScaledPixmap();
- }
+ return match.entry->iconPixmap();
}
} else if (role == Qt::FontRole) {
QFont font;
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 02b9f3a59..cfe6fd5a9 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -41,8 +41,9 @@
#include "core/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
-#include "core/FilePath.h"
#include "core/Metadata.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
#include "core/TimeDelta.h"
#include "core/Tools.h"
#ifdef WITH_XC_SSHAGENT
@@ -106,12 +107,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
setupAutoType();
#ifdef WITH_XC_SSHAGENT
- if (config()->get("SSHAgent", false).toBool()) {
- setupSSHAgent();
- m_sshAgentEnabled = true;
- } else {
- m_sshAgentEnabled = false;
- }
+ setupSSHAgent();
#endif
#ifdef WITH_XC_BROWSER
@@ -122,6 +118,14 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
setupHistory();
setupEntryUpdate();
+ m_entryModifiedTimer.setSingleShot(true);
+ m_entryModifiedTimer.setInterval(0);
+ connect(&m_entryModifiedTimer, &QTimer::timeout, this, [this] {
+ if (isVisible() && m_entry) {
+ setForms(m_entry);
+ }
+ });
+
connect(this, SIGNAL(accepted()), SLOT(acceptEntry()));
connect(this, SIGNAL(rejected()), SLOT(cancel()));
connect(this, SIGNAL(apply()), SLOT(commitEntry()));
@@ -133,8 +137,6 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
connect(m_iconsWidget, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage()));
- m_mainUi->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0);
-
m_editWidgetProperties->setCustomData(m_customData.data());
}
@@ -145,7 +147,7 @@ EditEntryWidget::~EditEntryWidget()
void EditEntryWidget::setupMain()
{
m_mainUi->setupUi(m_mainWidget);
- addPage(tr("Entry"), FilePath::instance()->icon("actions", "document-edit"), m_mainWidget);
+ addPage(tr("Entry"), Resources::instance()->icon("document-edit"), m_mainWidget);
m_mainUi->usernameComboBox->setEditable(true);
m_usernameCompleter->setCompletionMode(QCompleter::InlineCompletion);
@@ -153,38 +155,35 @@ void EditEntryWidget::setupMain()
m_usernameCompleter->setModel(m_usernameCompleterModel);
m_mainUi->usernameComboBox->setCompleter(m_usernameCompleter);
- m_mainUi->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show"));
- m_mainUi->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator"));
#ifdef WITH_XC_NETWORKING
- m_mainUi->fetchFaviconButton->setIcon(filePath()->icon("actions", "favicon-download"));
+ m_mainUi->fetchFaviconButton->setIcon(resources()->icon("favicon-download"));
m_mainUi->fetchFaviconButton->setDisabled(true);
#else
m_mainUi->fetchFaviconButton->setVisible(false);
#endif
- connect(m_mainUi->togglePasswordButton, SIGNAL(toggled(bool)), m_mainUi->passwordEdit, SLOT(setShowPassword(bool)));
- connect(m_mainUi->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool)));
#ifdef WITH_XC_NETWORKING
connect(m_mainUi->fetchFaviconButton, SIGNAL(clicked()), m_iconsWidget, SLOT(downloadFavicon()));
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), m_iconsWidget, SLOT(setUrl(QString)));
m_mainUi->urlEdit->enableVerifyMode();
#endif
- connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool)));
+ connect(m_mainUi->expireCheck, &QCheckBox::toggled, [&](bool enabled) {
+ m_mainUi->expireDatePicker->setEnabled(enabled);
+ if (enabled) {
+ m_mainUi->expireDatePicker->setDateTime(Clock::currentDateTime());
+ }
+ });
+
connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool)));
- m_mainUi->passwordRepeatEdit->enableVerifyMode(m_mainUi->passwordEdit);
- connect(m_mainUi->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString)));
m_mainUi->expirePresets->setMenu(createPresetsMenu());
connect(m_mainUi->expirePresets->menu(), SIGNAL(triggered(QAction*)), this, SLOT(useExpiryPreset(QAction*)));
-
- m_mainUi->passwordGenerator->hide();
- m_mainUi->passwordGenerator->reset();
}
void EditEntryWidget::setupAdvanced()
{
m_advancedUi->setupUi(m_advancedWidget);
- addPage(tr("Advanced"), FilePath::instance()->icon("categories", "preferences-other"), m_advancedWidget);
+ addPage(tr("Advanced"), Resources::instance()->icon("preferences-other"), m_advancedWidget);
m_advancedUi->attachmentsWidget->setReadOnly(false);
m_advancedUi->attachmentsWidget->setButtonsVisible(true);
@@ -214,7 +213,7 @@ void EditEntryWidget::setupAdvanced()
void EditEntryWidget::setupIcon()
{
m_iconsWidget->setShowApplyIconToButton(false);
- addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_iconsWidget);
+ addPage(tr("Icon"), Resources::instance()->icon("preferences-desktop-icons"), m_iconsWidget);
connect(this, SIGNAL(accepted()), m_iconsWidget, SLOT(abortRequests()));
connect(this, SIGNAL(rejected()), m_iconsWidget, SLOT(abortRequests()));
}
@@ -227,9 +226,9 @@ void EditEntryWidget::openAutotypeHelp()
void EditEntryWidget::setupAutoType()
{
m_autoTypeUi->setupUi(m_autoTypeWidget);
- addPage(tr("Auto-Type"), FilePath::instance()->icon("actions", "key-enter"), m_autoTypeWidget);
+ addPage(tr("Auto-Type"), Resources::instance()->icon("key-enter"), m_autoTypeWidget);
- m_autoTypeUi->openHelpButton->setIcon(filePath()->icon("actions", "system-help"));
+ m_autoTypeUi->openHelpButton->setIcon(resources()->icon("system-help"));
m_autoTypeDefaultSequenceGroup->addButton(m_autoTypeUi->inheritSequenceButton);
m_autoTypeDefaultSequenceGroup->addButton(m_autoTypeUi->customSequenceButton);
@@ -267,8 +266,8 @@ void EditEntryWidget::setupBrowser()
{
m_browserUi->setupUi(m_browserWidget);
- if (config()->get("Browser/Enabled", false).toBool()) {
- addPage(tr("Browser Integration"), FilePath::instance()->icon("apps", "internet-web-browser"), m_browserWidget);
+ if (config()->get(Config::Browser_Enabled).toBool()) {
+ addPage(tr("Browser Integration"), Resources::instance()->icon("internet-web-browser"), m_browserWidget);
m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes);
m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel);
@@ -386,13 +385,13 @@ void EditEntryWidget::updateCurrentURL()
void EditEntryWidget::setupProperties()
{
- addPage(tr("Properties"), FilePath::instance()->icon("actions", "document-properties"), m_editWidgetProperties);
+ addPage(tr("Properties"), Resources::instance()->icon("document-properties"), m_editWidgetProperties);
}
void EditEntryWidget::setupHistory()
{
m_historyUi->setupUi(m_historyWidget);
- addPage(tr("History"), FilePath::instance()->icon("actions", "view-history"), m_historyWidget);
+ addPage(tr("History"), Resources::instance()->icon("view-history"), m_historyWidget);
m_sortModel->setSourceModel(m_historyModel);
m_sortModel->setDynamicSortFilter(true);
@@ -422,7 +421,6 @@ void EditEntryWidget::setupEntryUpdate()
connect(m_mainUi->titleEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
connect(m_mainUi->usernameComboBox->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(setModified()));
connect(m_mainUi->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
- connect(m_mainUi->passwordRepeatEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
#ifdef WITH_XC_NETWORKING
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFaviconButtonEnable(QString)));
@@ -434,6 +432,7 @@ void EditEntryWidget::setupEntryUpdate()
// Advanced tab
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->knownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
@@ -455,7 +454,7 @@ void EditEntryWidget::setupEntryUpdate()
#ifdef WITH_XC_SSHAGENT
// SSH Agent tab
- if (config()->get("SSHAgent", false).toBool()) {
+ if (sshAgent()->isEnabled()) {
connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setModified()));
@@ -470,7 +469,7 @@ void EditEntryWidget::setupEntryUpdate()
#endif
#ifdef WITH_XC_BROWSER
- if (config()->get("Browser/Enabled", false).toBool()) {
+ if (config()->get(Config::Browser_Enabled).toBool()) {
connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
connect(m_browserUi->onlyHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
@@ -538,34 +537,29 @@ void EditEntryWidget::setupSSHAgent()
SIGNAL(entryAttachmentsModified()),
SLOT(updateSSHAgentAttachments()));
- addPage(tr("SSH Agent"), FilePath::instance()->icon("apps", "utilities-terminal"), m_sshAgentWidget);
+ addPage(tr("SSH Agent"), Resources::instance()->icon("utilities-terminal"), m_sshAgentWidget);
}
-void EditEntryWidget::updateSSHAgent()
+void EditEntryWidget::setSSHAgentSettings()
{
- KeeAgentSettings settings;
- settings.fromXml(m_advancedUi->attachmentsWidget->getAttachment("KeeAgent.settings"));
-
- m_sshAgentUi->addKeyToAgentCheckBox->setChecked(settings.addAtDatabaseOpen());
- m_sshAgentUi->removeKeyFromAgentCheckBox->setChecked(settings.removeAtDatabaseClose());
- m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(settings.useConfirmConstraintWhenAdding());
- m_sshAgentUi->lifetimeCheckBox->setChecked(settings.useLifetimeConstraintWhenAdding());
- m_sshAgentUi->lifetimeSpinBox->setValue(settings.lifetimeConstraintDuration());
+ m_sshAgentUi->addKeyToAgentCheckBox->setChecked(m_sshAgentSettings.addAtDatabaseOpen());
+ m_sshAgentUi->removeKeyFromAgentCheckBox->setChecked(m_sshAgentSettings.removeAtDatabaseClose());
+ m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(m_sshAgentSettings.useConfirmConstraintWhenAdding());
+ m_sshAgentUi->lifetimeCheckBox->setChecked(m_sshAgentSettings.useLifetimeConstraintWhenAdding());
+ m_sshAgentUi->lifetimeSpinBox->setValue(m_sshAgentSettings.lifetimeConstraintDuration());
m_sshAgentUi->attachmentComboBox->clear();
m_sshAgentUi->addToAgentButton->setEnabled(false);
m_sshAgentUi->removeFromAgentButton->setEnabled(false);
m_sshAgentUi->copyToClipboardButton->setEnabled(false);
+}
- m_sshAgentSettings = settings;
- updateSSHAgentAttachments();
-
- if (settings.selectedType() == "attachment") {
- m_sshAgentUi->attachmentRadioButton->setChecked(true);
- } else {
- m_sshAgentUi->externalFileRadioButton->setChecked(true);
- }
+void EditEntryWidget::updateSSHAgent()
+{
+ m_sshAgentSettings.reset();
+ m_sshAgentSettings.fromEntry(m_entry);
+ setSSHAgentSettings();
- updateSSHAgentKeyInfo();
+ updateSSHAgentAttachments();
}
void EditEntryWidget::updateSSHAgentAttachment()
@@ -576,6 +570,13 @@ void EditEntryWidget::updateSSHAgentAttachment()
void EditEntryWidget::updateSSHAgentAttachments()
{
+ // detect if KeeAgent.settings was removed by hand and reset settings
+ if (m_entry && KeeAgentSettings::inEntryAttachments(m_entry->attachments())
+ && !KeeAgentSettings::inEntryAttachments(m_advancedUi->attachmentsWidget->entryAttachments())) {
+ m_sshAgentSettings.reset();
+ setSSHAgentSettings();
+ }
+
m_sshAgentUi->attachmentComboBox->clear();
m_sshAgentUi->attachmentComboBox->addItem("");
@@ -590,6 +591,14 @@ void EditEntryWidget::updateSSHAgentAttachments()
m_sshAgentUi->attachmentComboBox->setCurrentText(m_sshAgentSettings.attachmentName());
m_sshAgentUi->externalFileEdit->setText(m_sshAgentSettings.fileName());
+
+ if (m_sshAgentSettings.selectedType() == "attachment") {
+ m_sshAgentUi->attachmentRadioButton->setChecked(true);
+ } else {
+ m_sshAgentUi->externalFileRadioButton->setChecked(true);
+ }
+
+ updateSSHAgentKeyInfo();
}
void EditEntryWidget::updateSSHAgentKeyInfo()
@@ -631,18 +640,16 @@ void EditEntryWidget::updateSSHAgentKeyInfo()
}
// enable agent buttons only if we have an agent running
- if (SSHAgent::instance()->isAgentRunning()) {
+ if (sshAgent()->isAgentRunning()) {
m_sshAgentUi->addToAgentButton->setEnabled(true);
m_sshAgentUi->removeFromAgentButton->setEnabled(true);
- SSHAgent::instance()->setAutoRemoveOnLock(key, m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked());
+ sshAgent()->setAutoRemoveOnLock(key, m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked());
}
}
-void EditEntryWidget::saveSSHAgentConfig()
+void EditEntryWidget::toKeeAgentSettings(KeeAgentSettings& settings) const
{
- KeeAgentSettings settings;
-
settings.setAddAtDatabaseOpen(m_sshAgentUi->addKeyToAgentCheckBox->isChecked());
settings.setRemoveAtDatabaseClose(m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked());
settings.setUseConfirmConstraintWhenAdding(m_sshAgentUi->requireUserConfirmationCheckBox->isChecked());
@@ -662,14 +669,6 @@ void EditEntryWidget::saveSSHAgentConfig()
// we don't use this either but we don't want it to dirty flag the config
settings.setSaveAttachmentToTempFile(m_sshAgentSettings.saveAttachmentToTempFile());
-
- if (settings.isDefault()) {
- m_advancedUi->attachmentsWidget->removeAttachment("KeeAgent.settings");
- } else if (settings != m_sshAgentSettings) {
- m_advancedUi->attachmentsWidget->setAttachment("KeeAgent.settings", settings.toXml());
- }
-
- m_sshAgentSettings = settings;
}
void EditEntryWidget::browsePrivateKey()
@@ -684,58 +683,22 @@ void EditEntryWidget::browsePrivateKey()
bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt)
{
- QString fileName;
- QByteArray privateKeyData;
-
- if (m_sshAgentUi->attachmentRadioButton->isChecked()) {
- fileName = m_sshAgentUi->attachmentComboBox->currentText();
- privateKeyData = m_advancedUi->attachmentsWidget->getAttachment(fileName);
- } else {
- QFile localFile(m_sshAgentUi->externalFileEdit->text());
- QFileInfo localFileInfo(localFile);
- fileName = localFileInfo.fileName();
-
- if (localFile.fileName().isEmpty()) {
- return false;
- }
-
- if (localFile.size() > 1024 * 1024) {
- showMessage(tr("File too large to be a private key"), MessageWidget::Error);
- return false;
- }
-
- if (!localFile.open(QIODevice::ReadOnly)) {
- showMessage(tr("Failed to open private key"), MessageWidget::Error);
- return false;
- }
-
- privateKeyData = localFile.readAll();
- }
+ KeeAgentSettings settings;
+ toKeeAgentSettings(settings);
- if (privateKeyData.isEmpty()) {
+ if (!m_entry || !settings.keyConfigured()) {
return false;
}
- if (!key.parsePKCS1PEM(privateKeyData)) {
- showMessage(key.errorString(), MessageWidget::Error);
+ if (!settings.toOpenSSHKey(m_mainUi->usernameComboBox->lineEdit()->text(),
+ m_mainUi->passwordEdit->text(),
+ m_advancedUi->attachmentsWidget->entryAttachments(),
+ key,
+ decrypt)) {
+ showMessage(settings.errorString(), MessageWidget::Error);
return false;
}
- if (key.encrypted() && (decrypt || key.publicKey().isEmpty())) {
- if (!key.openKey(m_entry->password())) {
- showMessage(key.errorString(), MessageWidget::Error);
- return false;
- }
- }
-
- if (key.comment().isEmpty()) {
- key.setComment(m_entry->username());
- }
-
- if (key.comment().isEmpty()) {
- key.setComment(fileName);
- }
-
return true;
}
@@ -751,14 +714,10 @@ void EditEntryWidget::addKeyToAgent()
m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey());
KeeAgentSettings settings;
+ toKeeAgentSettings(settings);
- settings.setRemoveAtDatabaseClose(m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked());
- settings.setUseConfirmConstraintWhenAdding(m_sshAgentUi->requireUserConfirmationCheckBox->isChecked());
- settings.setUseLifetimeConstraintWhenAdding(m_sshAgentUi->lifetimeCheckBox->isChecked());
- settings.setLifetimeConstraintDuration(m_sshAgentUi->lifetimeSpinBox->value());
-
- if (!SSHAgent::instance()->addIdentity(key, settings)) {
- showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error);
+ if (!sshAgent()->addIdentity(key, settings, m_db->uuid())) {
+ showMessage(sshAgent()->errorString(), MessageWidget::Error);
return;
}
}
@@ -771,8 +730,8 @@ void EditEntryWidget::removeKeyFromAgent()
return;
}
- if (!SSHAgent::instance()->removeIdentity(key)) {
- showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error);
+ if (!sshAgent()->removeIdentity(key)) {
+ showMessage(sshAgent()->errorString(), MessageWidget::Error);
return;
}
}
@@ -834,13 +793,15 @@ void EditEntryWidget::loadEntry(Entry* entry,
m_create = create;
m_history = history;
+ connect(m_entry, &Entry::entryModified, this, [this] { m_entryModifiedTimer.start(); });
+
if (history) {
- setHeadline(QString("%1 > %2").arg(parentName, tr("Entry history")));
+ setHeadline(QString("%1 \u2B29 %2").arg(parentName, tr("Entry history")));
} else {
if (create) {
- setHeadline(QString("%1 > %2").arg(parentName, tr("Add entry")));
+ setHeadline(QString("%1 \u2B29 %2").arg(parentName, tr("Add entry")));
} else {
- setHeadline(QString("%1 > %2 > %3").arg(parentName, entry->title(), tr("Edit entry")));
+ setHeadline(QString("%1 \u2B29 %2 \u2B29 %3").arg(parentName, entry->title(), tr("Edit entry")));
}
}
@@ -849,6 +810,9 @@ void EditEntryWidget::loadEntry(Entry* entry,
setCurrentPage(0);
setPageHidden(m_historyWidget, m_history || m_entry->historyItems().count() < 1);
+#ifdef WITH_XC_SSHAGENT
+ setPageHidden(m_sshAgentWidget, !sshAgent()->isEnabled());
+#endif
// Force the user to Save/Discard new entries
showApplyButton(!m_create);
@@ -864,21 +828,17 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_mainUi->usernameComboBox->lineEdit()->setReadOnly(m_history);
m_mainUi->urlEdit->setReadOnly(m_history);
m_mainUi->passwordEdit->setReadOnly(m_history);
- m_mainUi->passwordRepeatEdit->setReadOnly(m_history);
m_mainUi->expireCheck->setEnabled(!m_history);
m_mainUi->expireDatePicker->setReadOnly(m_history);
- m_mainUi->notesEnabled->setChecked(!config()->get("security/hidenotes").toBool());
+ m_mainUi->notesEnabled->setChecked(!config()->get(Config::Security_HideNotes).toBool());
m_mainUi->notesEdit->setReadOnly(m_history);
- m_mainUi->notesEdit->setVisible(!config()->get("security/hidenotes").toBool());
- m_mainUi->notesHint->setVisible(config()->get("security/hidenotes").toBool());
- if (config()->get("GUI/MonospaceNotes", false).toBool()) {
+ m_mainUi->notesEdit->setVisible(!config()->get(Config::Security_HideNotes).toBool());
+ m_mainUi->notesHint->setVisible(config()->get(Config::Security_HideNotes).toBool());
+ if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
m_mainUi->notesEdit->setFont(Font::fixedFont());
} else {
m_mainUi->notesEdit->setFont(Font::defaultFont());
}
- m_mainUi->togglePasswordGeneratorButton->setChecked(false);
- m_mainUi->togglePasswordGeneratorButton->setDisabled(m_history);
- m_mainUi->passwordGenerator->reset(entry->password().length());
m_advancedUi->attachmentsWidget->setReadOnly(m_history);
m_advancedUi->addAttributeButton->setEnabled(!m_history);
@@ -892,6 +852,9 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
editTriggers = QAbstractItemView::DoubleClicked;
}
m_advancedUi->attributesView->setEditTriggers(editTriggers);
+ m_advancedUi->knownBadCheckBox->setChecked(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD)
+ == TRUE_STR);
setupColorButton(true, entry->foregroundColor());
setupColorButton(false, entry->backgroundColor());
m_iconsWidget->setEnabled(!m_history);
@@ -904,11 +867,13 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_mainUi->usernameComboBox->lineEdit()->setText(entry->username());
m_mainUi->urlEdit->setText(entry->url());
m_mainUi->passwordEdit->setText(entry->password());
- m_mainUi->passwordRepeatEdit->setText(entry->password());
+ m_mainUi->passwordEdit->setShowPassword(!config()->get(Config::Security_PasswordsHidden).toBool());
+ if (!m_history) {
+ m_mainUi->passwordEdit->enablePasswordGenerator();
+ }
m_mainUi->expireCheck->setChecked(entry->timeInfo().expires());
m_mainUi->expireDatePicker->setDateTime(entry->timeInfo().expiryTime().toLocalTime());
m_mainUi->expirePresets->setEnabled(!m_history);
- m_mainUi->togglePasswordButton->setChecked(config()->get("security/passwordscleartext").toBool());
QList<QString> commonUsernames = m_db->commonUsernames();
m_usernameCompleterModel->setStringList(commonUsernames);
@@ -960,7 +925,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
updateAutoTypeEnabled();
#ifdef WITH_XC_SSHAGENT
- if (m_sshAgentEnabled) {
+ if (sshAgent()->isEnabled()) {
updateSSHAgent();
}
#endif
@@ -1028,27 +993,6 @@ bool EditEntryWidget::commitEntry()
return true;
}
- if (!passwordsEqual()) {
- showMessage(tr("Different passwords supplied."), MessageWidget::Error);
- return false;
- }
-
- // Ask the user to apply the generator password, if open
- if (m_mainUi->togglePasswordGeneratorButton->isChecked()
- && m_mainUi->passwordGenerator->getGeneratedPassword() != m_mainUi->passwordEdit->text()) {
- auto answer = MessageBox::question(this,
- tr("Apply generated password?"),
- tr("Do you want to apply the generated password to this entry?"),
- MessageBox::Yes | MessageBox::No,
- MessageBox::Yes);
- if (answer == MessageBox::Yes) {
- m_mainUi->passwordGenerator->applyPassword();
- }
- }
-
- // Hide the password generator
- m_mainUi->togglePasswordGeneratorButton->setChecked(false);
-
if (m_advancedUi->attributesView->currentIndex().isValid() && m_advancedUi->attributesEdit->isEnabled()) {
QString key = m_attributesModel->keyByIndex(m_advancedUi->attributesView->currentIndex());
m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), m_entryAttributes->isProtected(key));
@@ -1064,13 +1008,11 @@ bool EditEntryWidget::commitEntry()
m_autoTypeAssoc->removeEmpty();
#ifdef WITH_XC_SSHAGENT
- if (m_sshAgentEnabled) {
- saveSSHAgentConfig();
- }
+ toKeeAgentSettings(m_sshAgentSettings);
#endif
#ifdef WITH_XC_BROWSER
- if (config()->get("Browser/Enabled", false).toBool()) {
+ if (config()->get(Config::Browser_Enabled).toBool()) {
updateBrowser();
}
#endif
@@ -1085,13 +1027,8 @@ bool EditEntryWidget::commitEntry()
m_entry->endUpdate();
}
-#ifdef WITH_XC_SSHAGENT
- if (m_sshAgentEnabled) {
- updateSSHAgent();
- }
-#endif
-
m_historyModel->setEntries(m_entry->historyItems());
+ m_advancedUi->attachmentsWidget->setEntryAttachments(m_entry->attachments());
showMessage(tr("Entry updated successfully."), MessageWidget::Positive);
setModified(false);
@@ -1122,16 +1059,23 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
entry->setNotes(m_mainUi->notesEdit->toPlainText());
+ const auto wasKnownBad = entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
+ const auto isKnownBad = m_advancedUi->knownBadCheckBox->isChecked();
+ if (isKnownBad != wasKnownBad) {
+ entry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+ }
+
if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {
- entry->setForegroundColor(QColor(m_advancedUi->fgColorButton->property("color").toString()));
+ entry->setForegroundColor(m_advancedUi->fgColorButton->property("color").toString());
} else {
- entry->setForegroundColor(QColor());
+ entry->setForegroundColor(QString());
}
if (m_advancedUi->bgColorCheckBox->isChecked() && m_advancedUi->bgColorButton->property("color").isValid()) {
- entry->setBackgroundColor(QColor(m_advancedUi->bgColorButton->property("color").toString()));
+ entry->setBackgroundColor(m_advancedUi->bgColorButton->property("color").toString());
} else {
- entry->setBackgroundColor(QColor());
+ entry->setBackgroundColor(QString());
}
IconStruct iconStruct = m_iconsWidget->state();
@@ -1152,6 +1096,12 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
}
entry->autoTypeAssociations()->copyDataFrom(m_autoTypeAssoc);
+
+#ifdef WITH_XC_SSHAGENT
+ if (sshAgent()->isEnabled()) {
+ m_sshAgentSettings.toEntry(entry);
+ }
+#endif
}
void EditEntryWidget::cancel()
@@ -1163,15 +1113,15 @@ void EditEntryWidget::cancel()
return;
}
- if (!m_entry->iconUuid().isNull() && !m_db->metadata()->containsCustomIcon(m_entry->iconUuid())) {
+ if (!m_entry->iconUuid().isNull() && !m_db->metadata()->hasCustomIcon(m_entry->iconUuid())) {
m_entry->setIcon(Entry::DefaultIconNumber);
}
bool accepted = false;
if (isModified()) {
auto result = MessageBox::question(this,
- QString(),
- tr("Entry has unsaved changes"),
+ tr("Unsaved Changes"),
+ tr("Would you like to save changes to this entry?"),
MessageBox::Cancel | MessageBox::Save | MessageBox::Discard,
MessageBox::Cancel);
if (result == MessageBox::Cancel) {
@@ -1190,12 +1140,15 @@ void EditEntryWidget::cancel()
void EditEntryWidget::clear()
{
+ if (m_entry) {
+ m_entry->disconnect(this);
+ }
+
m_entry = nullptr;
m_db.reset();
m_mainUi->titleEdit->setText("");
m_mainUi->passwordEdit->setText("");
- m_mainUi->passwordRepeatEdit->setText("");
m_mainUi->urlEdit->setText("");
m_mainUi->notesEdit->clear();
@@ -1207,27 +1160,6 @@ void EditEntryWidget::clear()
hideMessage();
}
-void EditEntryWidget::togglePasswordGeneratorButton(bool checked)
-{
- if (checked) {
- m_mainUi->passwordGenerator->regeneratePassword();
- }
- m_mainUi->passwordGenerator->setVisible(checked);
-}
-
-bool EditEntryWidget::passwordsEqual()
-{
- return m_mainUi->passwordEdit->text() == m_mainUi->passwordRepeatEdit->text();
-}
-
-void EditEntryWidget::setGeneratedPassword(const QString& password)
-{
- m_mainUi->passwordEdit->setText(password);
- m_mainUi->passwordRepeatEdit->setText(password);
-
- m_mainUi->togglePasswordGeneratorButton->setChecked(false);
-}
-
#ifdef WITH_XC_NETWORKING
void EditEntryWidget::updateFaviconButtonEnable(const QString& url)
{
@@ -1318,7 +1250,7 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
if (index.isValid()) {
QString key = m_attributesModel->keyByIndex(index);
if (showProtected) {
- m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press reveal to view or edit"));
+ m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press Reveal to view or edit"));
m_advancedUi->attributesEdit->setEnabled(false);
m_advancedUi->revealAttributeButton->setEnabled(true);
m_advancedUi->protectAttributeButton->setChecked(true);
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index 300220cd0..3d1835396 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -24,6 +24,7 @@
#include <QModelIndex>
#include <QPointer>
#include <QScopedPointer>
+#include <QTimer>
#include "config-keepassx.h"
#include "gui/EditWidget.h"
@@ -83,8 +84,6 @@ private slots:
void acceptEntry();
bool commitEntry();
void cancel();
- void togglePasswordGeneratorButton(bool checked);
- void setGeneratedPassword(const QString& password);
#ifdef WITH_XC_NETWORKING
void updateFaviconButtonEnable(const QString& url);
#endif
@@ -112,6 +111,8 @@ private slots:
void toggleHideNotes(bool visible);
void pickColor();
#ifdef WITH_XC_SSHAGENT
+ void toKeeAgentSettings(KeeAgentSettings& settings) const;
+ void setSSHAgentSettings();
void updateSSHAgent();
void updateSSHAgentAttachment();
void updateSSHAgentAttachments();
@@ -153,7 +154,6 @@ private:
void updateEntryData(Entry* entry) const;
#ifdef WITH_XC_SSHAGENT
bool getOpenSSHKey(OpenSSHKey& key, bool decrypt = false);
- void saveSSHAgentConfig();
#endif
void displayAttribute(QModelIndex index, bool showProtected);
@@ -164,7 +164,6 @@ private:
bool m_create;
bool m_history;
#ifdef WITH_XC_SSHAGENT
- bool m_sshAgentEnabled;
KeeAgentSettings m_sshAgentSettings;
#endif
const QScopedPointer<Ui::EditEntryWidgetMain> m_mainUi;
@@ -200,6 +199,7 @@ private:
QButtonGroup* const m_autoTypeWindowSequenceGroup;
QCompleter* const m_usernameCompleter;
QStringListModel* const m_usernameCompleterModel;
+ QTimer m_entryModifiedTimer;
Q_DISABLE_COPY(EditEntryWidget)
};
diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui
index 7b079b676..80841eb64 100644
--- a/src/gui/entry/EditEntryWidgetAdvanced.ui
+++ b/src/gui/entry/EditEntryWidgetAdvanced.ui
@@ -7,10 +7,22 @@
<x>0</x>
<y>0</y>
<width>532</width>
- <height>374</height>
+ <height>469</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<widget class="QGroupBox" name="attributesBox">
<property name="title">
@@ -175,8 +187,30 @@
</widget>
</item>
<item>
+ <widget class="QCheckBox" name="knownBadCheckBox">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements (e. g. password entropy or re-use). You can set the check mark if the password is beyond your control (e. g. if it needs to be a four-digit PIN) to prevent it from cluttering the reports.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ <property name="text">
+ <string>Exclude from database reports</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QWidget" name="colorsBox" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
<item>
<widget class="QCheckBox" name="fgColorCheckBox">
<property name="text">
@@ -293,6 +327,7 @@
<tabstop>editAttributeButton</tabstop>
<tabstop>protectAttributeButton</tabstop>
<tabstop>revealAttributeButton</tabstop>
+ <tabstop>knownBadCheckBox</tabstop>
<tabstop>fgColorCheckBox</tabstop>
<tabstop>fgColorButton</tabstop>
<tabstop>bgColorCheckBox</tabstop>
diff --git a/src/gui/entry/EditEntryWidgetAutoType.ui b/src/gui/entry/EditEntryWidgetAutoType.ui
index d987e8047..f82240d7d 100644
--- a/src/gui/entry/EditEntryWidgetAutoType.ui
+++ b/src/gui/entry/EditEntryWidgetAutoType.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>567</width>
- <height>348</height>
+ <width>577</width>
+ <height>434</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@@ -49,14 +49,14 @@
<item>
<widget class="QRadioButton" name="inheritSequenceButton">
<property name="text">
- <string>Inherit default Auto-Type sequence from the &amp;group</string>
+ <string>Inherit default Auto-Type sequence from the group</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="customSequenceButton">
<property name="text">
- <string>&amp;Use custom Auto-Type sequence:</string>
+ <string>Use custom Auto-Type sequence:</string>
</property>
</widget>
</item>
@@ -107,6 +107,22 @@
</layout>
</item>
<item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
<widget class="QGroupBox" name="windowsBox">
<property name="title">
<string>Window Associations</string>
diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui
index 54140fcd9..f96481a3f 100644
--- a/src/gui/entry/EditEntryWidgetMain.ui
+++ b/src/gui/entry/EditEntryWidgetMain.ui
@@ -6,11 +6,14 @@
<rect>
<x>0</x>
<y>0</y>
- <width>329</width>
- <height>381</height>
+ <width>496</width>
+ <height>420</height>
</rect>
</property>
- <layout class="QGridLayout" name="gridLayout_3">
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
<property name="topMargin">
<number>0</number>
</property>
@@ -20,127 +23,90 @@
<property name="bottomMargin">
<number>0</number>
</property>
- <item row="5" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="urlLabel">
- <property name="text">
- <string>URL:</string>
- </property>
- </widget>
- </item>
- <item row="5" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>8</number>
+ </property>
+ <item row="6" column="1">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="URLEdit" name="urlEdit">
+ <widget class="QPlainTextEdit" name="notesEdit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
<property name="accessibleName">
- <string>Url field</string>
+ <string>Notes field</string>
</property>
</widget>
</item>
<item>
- <widget class="QToolButton" name="fetchFaviconButton">
- <property name="toolTip">
- <string>Download favicon for URL</string>
+ <widget class="QLabel" name="notesHint">
+ <property name="visible">
+ <bool>true</bool>
</property>
- <property name="accessibleName">
- <string>Download favicon for URL</string>
+ <property name="text">
+ <string>Toggle the checkbox to reveal the notes section.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</item>
- <item row="4" column="1">
- <widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/>
- </item>
- <item row="2" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="passwordLabel">
- <property name="text">
- <string>Password:</string>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="usernameComboBox">
+ <property name="accessibleName">
+ <string>Username field</string>
</property>
</widget>
</item>
- <item row="3" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item row="6" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="PasswordEdit" name="passwordRepeatEdit">
- <property name="accessibleName">
- <string>Repeat password field</string>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="togglePasswordGeneratorButton">
+ <widget class="QCheckBox" name="notesEnabled">
<property name="toolTip">
- <string>Toggle password generator</string>
- </property>
- <property name="accessibleName">
- <string>Toggle password generator</string>
- </property>
- <property name="checkable">
- <bool>true</bool>
+ <string>Toggle notes visible</string>
</property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="2" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="PasswordEdit" name="passwordEdit">
<property name="accessibleName">
- <string>Password field</string>
+ <string>Toggle notes visible</string>
</property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
+ <property name="text">
+ <string>Notes:</string>
</property>
</widget>
</item>
<item>
- <widget class="QToolButton" name="togglePasswordButton">
- <property name="toolTip">
- <string>Toggle password visibility</string>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
</property>
- <property name="accessibleName">
- <string>Toggle password visibility</string>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
</property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
+ </spacer>
</item>
</layout>
</item>
- <item row="3" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="passwordRepeatLabel">
- <property name="text">
- <string>Repeat:</string>
- </property>
- </widget>
- </item>
- <item row="0" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="titleLabel">
- <property name="text">
- <string>Title:</string>
- </property>
- </widget>
- </item>
- <item row="9" column="0" alignment="Qt::AlignLeft|Qt::AlignTop">
- <widget class="QCheckBox" name="notesEnabled">
- <property name="toolTip">
- <string>Toggle notes visible</string>
- </property>
- <property name="accessibleName">
- <string>Toggle notes visible</string>
- </property>
- <property name="text">
- <string>Notes</string>
- </property>
- </widget>
- </item>
- <item row="7" column="1">
+ <item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="spacing">
+ <number>8</number>
+ </property>
<item>
<widget class="QDateTimeEdit" name="expireDatePicker">
<property name="enabled">
@@ -175,42 +141,60 @@
</item>
</layout>
</item>
- <item row="9" column="1">
- <widget class="QPlainTextEdit" name="notesEdit">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>1</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>100</height>
- </size>
+ <item row="2" column="0">
+ <widget class="QLabel" name="passwordLabel">
+ <property name="text">
+ <string>Password:</string>
</property>
- <property name="accessibleName">
- <string>Notes field</string>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="9" column="1">
- <widget class="QLabel" name="notesHint">
- <property name="visible">
- <bool>false</bool>
- </property>
+ <item row="3" column="0">
+ <widget class="QLabel" name="urlLabel">
<property name="text">
- <string>Toggle the checkbox to reveal the notes section.</string>
+ <string>URL:</string>
</property>
<property name="alignment">
- <set>Qt::AlignTop</set>
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="1" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="usernameLabel">
+ <item row="3" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="URLEdit" name="urlEdit">
+ <property name="accessibleName">
+ <string>Url field</string>
+ </property>
+ <property name="placeholderText">
+ <string>https://example.com</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="fetchFaviconButton">
+ <property name="toolTip">
+ <string>Download favicon for URL</string>
+ </property>
+ <property name="accessibleName">
+ <string>Download favicon for URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="titleLabel">
<property name="text">
- <string>Username:</string>
+ <string>Title:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
@@ -221,36 +205,50 @@
</property>
</widget>
</item>
- <item row="1" column="1">
- <widget class="QComboBox" name="usernameComboBox">
- <property name="accessibleName">
- <string>Username field</string>
+ <item row="1" column="0">
+ <widget class="QLabel" name="usernameLabel">
+ <property name="text">
+ <string>Username:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="7" column="0" alignment="Qt::AlignRight">
- <widget class="QCheckBox" name="expireCheck">
- <property name="toolTip">
- <string>Toggle expiration</string>
- </property>
+ <item row="2" column="1">
+ <widget class="PasswordEdit" name="passwordEdit">
<property name="accessibleName">
- <string>Toggle expiration</string>
+ <string>Password field</string>
</property>
- <property name="text">
- <string>Expires</string>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
+ <item row="5" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="expireCheck">
+ <property name="toolTip">
+ <string>Toggle expiration</string>
+ </property>
+ <property name="accessibleName">
+ <string>Toggle expiration</string>
+ </property>
+ <property name="text">
+ <string>Expires:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
</layout>
</widget>
<customwidgets>
<customwidget>
- <class>PasswordGeneratorWidget</class>
- <extends>QWidget</extends>
- <header>gui/PasswordGeneratorWidget.h</header>
- <container>1</container>
- </customwidget>
- <customwidget>
<class>PasswordEdit</class>
<extends>QLineEdit</extends>
<header>gui/PasswordEdit.h</header>
@@ -267,9 +265,6 @@
<tabstop>titleEdit</tabstop>
<tabstop>usernameComboBox</tabstop>
<tabstop>passwordEdit</tabstop>
- <tabstop>togglePasswordButton</tabstop>
- <tabstop>passwordRepeatEdit</tabstop>
- <tabstop>togglePasswordGeneratorButton</tabstop>
<tabstop>urlEdit</tabstop>
<tabstop>fetchFaviconButton</tabstop>
<tabstop>expireCheck</tabstop>
diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui
index 2e9d94b65..fae13dccb 100644
--- a/src/gui/entry/EditEntryWidgetSSHAgent.ui
+++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>498</width>
- <height>518</height>
+ <width>452</width>
+ <height>618</height>
</rect>
</property>
<property name="windowTitle">
@@ -26,53 +26,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
- <item row="3" column="1" colspan="4">
- <layout class="QHBoxLayout" name="removeKeyLayout">
- <item>
- <widget class="QCheckBox" name="lifetimeCheckBox">
- <property name="text">
- <string>Remove key from agent after</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QSpinBox" name="lifetimeSpinBox">
- <property name="accessibleName">
- <string>Remove key from agent after specified seconds</string>
- </property>
- <property name="suffix">
- <string> seconds</string>
- </property>
- <property name="maximum">
- <number>999999999</number>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="removeKeySpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- <item row="11" column="1">
- <widget class="QLabel" name="fingerprintLabel">
- <property name="text">
- <string>Fingerprint</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
<item row="1" column="1" colspan="4">
<widget class="QCheckBox" name="removeKeyFromAgentCheckBox">
<property name="text">
@@ -80,13 +33,13 @@
</property>
</widget>
</item>
- <item row="13" column="1">
- <widget class="QLabel" name="publicKeyLabel">
+ <item row="14" column="1">
+ <widget class="QLabel" name="commentLabel">
<property name="text">
- <string>Public key</string>
+ <string>Comment</string>
</property>
<property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
@@ -97,63 +50,69 @@
</property>
</widget>
</item>
- <item row="12" column="1">
- <widget class="QLabel" name="commentLabel">
- <property name="text">
- <string>Comment</string>
+ <item row="15" column="3" colspan="2">
+ <widget class="QPlainTextEdit" name="publicKeyEdit">
+ <property name="font">
+ <font>
+ <family>Monospace</family>
+ </font>
</property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ <property name="readOnly">
+ <bool>true</bool>
</property>
</widget>
</item>
- <item row="12" column="4">
+ <item row="14" column="4">
<widget class="QPushButton" name="decryptButton">
<property name="text">
<string>Decrypt</string>
</property>
</widget>
</item>
- <item row="11" column="3" colspan="2">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <widget class="QLabel" name="fingerprintTextLabel">
- <property name="font">
- <font>
- <family>Monospace</family>
- </font>
- </property>
- <property name="text">
- <string>n/a</string>
- </property>
- <property name="textInteractionFlags">
- <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <item row="4" column="1">
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="13" column="1">
+ <widget class="QLabel" name="fingerprintLabel">
+ <property name="text">
+ <string>Fingerprint</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
</item>
- <item row="14" column="3" colspan="2">
+ <item row="16" column="3" colspan="2">
<widget class="QPushButton" name="copyToClipboardButton">
<property name="text">
<string>Copy to clipboard</string>
</property>
</widget>
</item>
- <item row="4" column="1" colspan="4">
+ <item row="15" column="1">
+ <widget class="QLabel" name="publicKeyLabel">
+ <property name="text">
+ <string>Public key</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1" colspan="4">
<widget class="QGroupBox" name="privateKeyGroupBox">
<property name="title">
<string>Private key</string>
@@ -240,19 +199,7 @@
</property>
</widget>
</item>
- <item row="13" column="3" colspan="2">
- <widget class="QPlainTextEdit" name="publicKeyEdit">
- <property name="font">
- <font>
- <family>Monospace</family>
- </font>
- </property>
- <property name="readOnly">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="12" column="3">
+ <item row="14" column="3">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="commentTextLabel">
@@ -284,6 +231,91 @@
</item>
</layout>
</item>
+ <item row="3" column="1" colspan="4">
+ <layout class="QHBoxLayout" name="removeKeyLayout">
+ <item>
+ <widget class="QCheckBox" name="lifetimeCheckBox">
+ <property name="text">
+ <string>Remove key from agent after</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QSpinBox" name="lifetimeSpinBox">
+ <property name="accessibleName">
+ <string>Remove key from agent after specified seconds</string>
+ </property>
+ <property name="suffix">
+ <string> seconds</string>
+ </property>
+ <property name="maximum">
+ <number>999999999</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="removeKeySpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="13" column="3" colspan="2">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="fingerprintTextLabel">
+ <property name="font">
+ <font>
+ <family>Monospace</family>
+ </font>
+ </property>
+ <property name="text">
+ <string>n/a</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="12" column="3">
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
</layout>
</widget>
<tabstops>
diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp
index 9610cc05b..a614c4f35 100644
--- a/src/gui/entry/EntryAttachmentsWidget.cpp
+++ b/src/gui/entry/EntryAttachmentsWidget.cpp
@@ -135,21 +135,23 @@ void EntryAttachmentsWidget::insertAttachments()
return;
}
- QString defaultDirPath = config()->get("LastAttachmentDir").toString();
+ QString defaultDirPath = config()->get(Config::LastAttachmentDir).toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first();
}
- const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
+ const auto filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
if (filenames.isEmpty()) {
return;
}
-
- config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath());
-
+ const auto confirmedFileNames = confirmLargeAttachments(filenames);
+ if (confirmedFileNames.isEmpty()) {
+ return;
+ }
+ config()->set(Config::LastAttachmentDir, QFileInfo(filenames.first()).absolutePath());
QString errorMessage;
- if (!insertAttachments(filenames, errorMessage)) {
+ if (!insertAttachments(confirmedFileNames, errorMessage)) {
errorOccurred(errorMessage);
}
emit widgetUpdated();
@@ -190,7 +192,7 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
return;
}
- QString defaultDirPath = config()->get("LastAttachmentDir").toString();
+ QString defaultDirPath = config()->get(Config::LastAttachmentDir).toString();
const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists();
if (!dirExists) {
defaultDirPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
@@ -208,7 +210,7 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
return;
}
}
- config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath());
+ config()->set(Config::LastAttachmentDir, QFileInfo(saveDir.absolutePath()).absolutePath());
QStringList errors;
for (const QModelIndex& index : indexes) {
@@ -353,6 +355,33 @@ bool EntryAttachmentsWidget::openAttachment(const QModelIndex& index, QString& e
return true;
}
+QStringList EntryAttachmentsWidget::confirmLargeAttachments(const QStringList& filenames)
+{
+ const QString confirmation(tr("%1 is a big file (%2 MB).\nYour database may get very large and reduce "
+ "performance.\n\nAre you sure to add this file?"));
+ QStringList confirmedFileNames;
+ for (const auto& file : filenames) {
+ QFileInfo fileInfo(file);
+ double size = fileInfo.size() / (1024.0 * 1024.0);
+ // Ask for confirmation before adding files over 5 MB in size
+ if (size > 5.0) {
+ auto fileName = fileInfo.fileName();
+ auto result = MessageBox::question(this,
+ tr("Confirm Attachment"),
+ confirmation.arg(fileName, QString::number(size, 'f', 1)),
+ MessageBox::Yes | MessageBox::No,
+ MessageBox::No);
+ if (result == MessageBox::Yes) {
+ confirmedFileNames << file;
+ }
+ } else {
+ confirmedFileNames << file;
+ }
+ }
+
+ return confirmedFileNames;
+}
+
bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e)
{
if (watched == m_ui->attachmentsView->viewport() && !isReadOnly()) {
diff --git a/src/gui/entry/EntryAttachmentsWidget.h b/src/gui/entry/EntryAttachmentsWidget.h
index 8859536dd..df69752ee 100644
--- a/src/gui/entry/EntryAttachmentsWidget.h
+++ b/src/gui/entry/EntryAttachmentsWidget.h
@@ -54,6 +54,8 @@ private:
bool insertAttachments(const QStringList& fileNames, QString& errorMessage);
bool openAttachment(const QModelIndex& index, QString& errorMessage);
+ QStringList confirmLargeAttachments(const QStringList& filenames);
+
bool eventFilter(QObject* watched, QEvent* event) override;
QScopedPointer<Ui::EntryAttachmentsWidget> m_ui;
diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp
index bf7eca0c7..0b766c854 100644
--- a/src/gui/entry/EntryModel.cpp
+++ b/src/gui/entry/EntryModel.cpp
@@ -19,7 +19,6 @@
#include <QDateTime>
#include <QFont>
-#include <QFontMetrics>
#include <QMimeData>
#include <QPainter>
#include <QPalette>
@@ -30,8 +29,9 @@
#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
+#include "core/Resources.h"
#ifdef Q_OS_MACOS
-#include "gui/macutils/MacUtils.h"
+#include "gui/osutils/macutils/MacUtils.h"
#endif
EntryModel::EntryModel(QObject* parent)
@@ -129,7 +129,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const
return 0;
}
- return 13;
+ return 14;
}
QVariant EntryModel::data(const QModelIndex& index, int role) const
@@ -174,7 +174,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
if (attr->isReference(EntryAttributes::PasswordKey)) {
result.prepend(tr("Ref: ", "Reference abbreviation"));
}
- if (entry->password().isEmpty() && config()->get("security/passwordemptynodots").toBool()) {
+ if (entry->password().isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) {
result = "";
}
return result;
@@ -185,10 +185,16 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
return result;
case Notes:
- // Display only first line of notes in simplified format
- result = entry->notes().section("\n", 0, 0).simplified();
- if (attr->isReference(EntryAttributes::NotesKey)) {
- result.prepend(tr("Ref: ", "Reference abbreviation"));
+ if (!entry->notes().isEmpty()) {
+ if (config()->get(Config::Security_HideNotes).toBool()) {
+ result = EntryModel::HiddenContentDisplay;
+ } else {
+ // Display only first line of notes in simplified format if not hidden
+ result = entry->notes().section("\n", 0, 0).simplified();
+ }
+ if (attr->isReference(EntryAttributes::NotesKey)) {
+ result.prepend(tr("Ref: ", "Reference abbreviation"));
+ }
}
return result;
case Expires:
@@ -218,10 +224,23 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
return result;
}
- case Totp:
- result = entry->hasTotp() ? tr("Yes") : "";
+ case Size: {
+ const int unitsSize = 4;
+ QString units[unitsSize] = {"B", "KiB", "MiB", "GiB"};
+ float resultInt = entry->size();
+
+ for (int i = 0; i < unitsSize; i++) {
+ if (resultInt < 1024 || i == unitsSize - 1) {
+ resultInt = qRound(resultInt * 100) / 100.0;
+ result = QStringLiteral("%1 %2").arg(QString::number(resultInt), units[i]);
+ break;
+ }
+ resultInt /= 1024.0;
+ }
+
return result;
}
+ }
} else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView()
switch (index.column()) {
case Username:
@@ -240,7 +259,11 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
case Paperclip:
// Display entries with attachments above those without when
// sorting ascendingly (and vice versa when sorting descendingly)
- return entry->attachments()->isEmpty() ? 1 : 0;
+ return !entry->attachments()->isEmpty();
+ case Totp:
+ return entry->hasTotp();
+ case Size:
+ return entry->size();
default:
// For all other columns, simply use data provided by Qt::Display-
// Role for sorting
@@ -250,17 +273,19 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
switch (index.column()) {
case ParentGroup:
if (entry->group()) {
- return entry->group()->iconScaledPixmap();
+ return entry->group()->iconPixmap();
}
break;
case Title:
- if (entry->isExpired()) {
- return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex);
- }
- return entry->iconScaledPixmap();
+ return entry->iconPixmap();
case Paperclip:
if (!entry->attachments()->isEmpty()) {
- return m_paperClipPixmap;
+ return resources()->icon("paperclip");
+ }
+ break;
+ case Totp:
+ if (entry->hasTotp()) {
+ return resources()->icon("chronometer");
}
break;
}
@@ -271,20 +296,23 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
}
return font;
} else if (role == Qt::ForegroundRole) {
+ QColor foregroundColor;
+ foregroundColor.setNamedColor(entry->foregroundColor());
if (entry->hasReferences()) {
QPalette p;
-#ifdef Q_OS_MACOS
- if (macUtils()->isDarkMode()) {
- return QVariant(p.color(QPalette::Inactive, QPalette::Dark));
- }
-#endif
- return QVariant(p.color(QPalette::Active, QPalette::Mid));
- } else if (entry->foregroundColor().isValid()) {
- return QVariant(entry->foregroundColor());
+ foregroundColor = p.color(QPalette::Current, QPalette::Text);
+ int lightness =
+ qMin(255, qMax(0, foregroundColor.lightness() + (foregroundColor.lightness() < 110 ? 85 : -51)));
+ foregroundColor.setHsl(foregroundColor.hue(), foregroundColor.saturation(), lightness);
+ return QVariant(foregroundColor);
+ } else if (foregroundColor.isValid()) {
+ return QVariant(foregroundColor);
}
} else if (role == Qt::BackgroundRole) {
- if (entry->backgroundColor().isValid()) {
- return QVariant(entry->backgroundColor());
+ QColor backgroundColor;
+ backgroundColor.setNamedColor(entry->backgroundColor());
+ if (backgroundColor.isValid()) {
+ return QVariant(backgroundColor);
}
} else if (role == Qt::TextAlignmentRole) {
if (index.column() == Paperclip) {
@@ -323,16 +351,51 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
return tr("Accessed");
case Attachments:
return tr("Attachments");
- case Totp:
- return tr("TOTP");
+ case Size:
+ return tr("Size");
}
+
} else if (role == Qt::DecorationRole) {
- if (section == Paperclip) {
- return m_paperClipPixmap;
+ switch (section) {
+ case Paperclip:
+ return resources()->icon("paperclip");
+ case Totp:
+ return resources()->icon("chronometer");
+ }
+ } else if (role == Qt::ToolTipRole) {
+ switch (section) {
+ case ParentGroup:
+ return tr("Group name");
+ case Title:
+ return tr("Entry title");
+ case Username:
+ return tr("Username");
+ case Password:
+ return tr("Password");
+ case Url:
+ return tr("URL");
+ case Notes:
+ return tr("Entry notes");
+ case Expires:
+ return tr("Entry expires at");
+ case Created:
+ return tr("Creation date");
+ case Modified:
+ return tr("Last modification date");
+ case Accessed:
+ return tr("Last access date");
+ case Attachments:
+ return tr("Attached files");
+ case Size:
+ return tr("Entry size");
+ case Paperclip:
+ return tr("Has attachments");
+ case Totp:
+ return tr("Has TOTP one-time password");
}
}
- return QVariant();
+ return {};
}
Qt::DropActions EntryModel::supportedDropActions() const
@@ -433,10 +496,41 @@ void EntryModel::entryRemoved()
if (m_group) {
m_entries = m_group->entries();
}
-
endRemoveRows();
}
+void EntryModel::entryAboutToMoveUp(int row)
+{
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1);
+ if (m_group) {
+ m_entries.move(row, row - 1);
+ }
+}
+
+void EntryModel::entryMovedUp()
+{
+ if (m_group) {
+ m_entries = m_group->entries();
+ }
+ endMoveRows();
+}
+
+void EntryModel::entryAboutToMoveDown(int row)
+{
+ beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2);
+ if (m_group) {
+ m_entries.move(row, row + 1);
+ }
+}
+
+void EntryModel::entryMovedDown()
+{
+ if (m_group) {
+ m_entries = m_group->entries();
+ }
+ endMoveRows();
+}
+
void EntryModel::entryDataChanged(Entry* entry)
{
int row = m_entries.indexOf(entry);
@@ -460,6 +554,10 @@ void EntryModel::makeConnections(const Group* group)
connect(group, SIGNAL(entryAdded(Entry*)), SLOT(entryAdded(Entry*)));
connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*)));
connect(group, SIGNAL(entryRemoved(Entry*)), SLOT(entryRemoved()));
+ connect(group, SIGNAL(entryAboutToMoveUp(int)), SLOT(entryAboutToMoveUp(int)));
+ connect(group, SIGNAL(entryMovedUp()), SLOT(entryMovedUp()));
+ connect(group, SIGNAL(entryAboutToMoveDown(int)), SLOT(entryAboutToMoveDown(int)));
+ connect(group, SIGNAL(entryMovedDown()), SLOT(entryMovedDown()));
connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*)));
}
@@ -498,8 +596,3 @@ void EntryModel::setPasswordsHidden(bool hide)
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
emit passwordsHiddenChanged();
}
-
-void EntryModel::setPaperClipPixmap(const QPixmap& paperclip)
-{
- m_paperClipPixmap = paperclip;
-}
diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h
index 5f405bd41..78da7194c 100644
--- a/src/gui/entry/EntryModel.h
+++ b/src/gui/entry/EntryModel.h
@@ -43,7 +43,8 @@ public:
Accessed = 9,
Paperclip = 10,
Attachments = 11,
- Totp = 12
+ Totp = 12,
+ Size = 13
};
explicit EntryModel(QObject* parent = nullptr);
@@ -68,8 +69,6 @@ public:
bool isPasswordsHidden() const;
void setPasswordsHidden(bool hide);
- void setPaperClipPixmap(const QPixmap& paperclip);
-
signals:
void usernamesHiddenChanged();
void passwordsHiddenChanged();
@@ -79,6 +78,10 @@ private slots:
void entryAdded(Entry* entry);
void entryAboutToRemove(Entry* entry);
void entryRemoved();
+ void entryAboutToMoveUp(int row);
+ void entryMovedUp();
+ void entryAboutToMoveDown(int row);
+ void entryMovedDown();
void entryDataChanged(Entry* entry);
private:
@@ -93,8 +96,6 @@ private:
bool m_hideUsernames;
bool m_hidePasswords;
- QPixmap m_paperClipPixmap;
-
const QString HiddenContentDisplay;
const Qt::DateFormat DateFormat;
};
diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp
index 3e6fb839c..7bf673a99 100644
--- a/src/gui/entry/EntryURLModel.cpp
+++ b/src/gui/entry/EntryURLModel.cpp
@@ -19,7 +19,7 @@
#include "EntryURLModel.h"
#include "core/Entry.h"
-#include "core/FilePath.h"
+#include "core/Resources.h"
#include "core/Tools.h"
#include <algorithm>
@@ -27,7 +27,7 @@
EntryURLModel::EntryURLModel(QObject* parent)
: QStandardItemModel(parent)
, m_entryAttributes(nullptr)
- , m_errorIcon(filePath()->icon("status", "dialog-error"))
+ , m_errorIcon(resources()->icon("dialog-error"))
{
}
diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp
index bff11e124..18a69687d 100644
--- a/src/gui/entry/EntryView.cpp
+++ b/src/gui/entry/EntryView.cpp
@@ -24,13 +24,14 @@
#include <QMenu>
#include <QShortcut>
-#include "core/FilePath.h"
#include "gui/SortFilterHideProxyModel.h"
EntryView::EntryView(QWidget* parent)
: QTreeView(parent)
, m_model(new EntryModel(this))
, m_sortModel(new SortFilterHideProxyModel(this))
+ , m_lastIndex(-1)
+ , m_lastOrder(Qt::AscendingOrder)
, m_inSearchMode(false)
{
m_sortModel->setSourceModel(m_model);
@@ -70,22 +71,31 @@ EntryView::EntryView(QWidget* parent)
m_hidePasswordsAction->setCheckable(true);
m_headerMenu->addSeparator();
+ resetViewToDefaults();
+
// Actions to toggle column visibility, each carrying the corresponding
- // colummn index as data
+ // column index as data
m_columnActions = new QActionGroup(this);
m_columnActions->setExclusive(false);
- for (int columnIndex = 1; columnIndex < header()->count(); ++columnIndex) {
- QString caption = m_model->headerData(columnIndex, Qt::Horizontal, Qt::DisplayRole).toString();
- if (columnIndex == EntryModel::Paperclip) {
- caption = tr("Attachments (icon)");
+ for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
+ int logicalIndex = header()->logicalIndex(visualIndex);
+ QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
+ if (logicalIndex == EntryModel::Paperclip) {
+ caption = tr("Has attachments", "Entry attachment icon toggle");
+ } else if (logicalIndex == EntryModel::Totp) {
+ caption = tr("Has TOTP", "Entry TOTP icon toggle");
}
QAction* action = m_headerMenu->addAction(caption);
action->setCheckable(true);
- action->setData(columnIndex);
+ action->setData(logicalIndex);
m_columnActions->addAction(action);
}
connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*)));
+ connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) {
+ Q_UNUSED(order)
+ header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp);
+ });
m_headerMenu->addSeparator();
m_headerMenu->addAction(tr("Fit to window"), this, SLOT(fitColumnsToWindow()));
@@ -112,24 +122,8 @@ EntryView::EntryView(QWidget* parent)
// clang-format on
// clang-format off
- connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SIGNAL(viewStateChanged()));
+ connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SLOT(sortIndicatorChanged(int,Qt::SortOrder)));
// clang-format on
-
- resetFixedColumns();
-
- // Configure default search view state and save for later use
- header()->showSection(EntryModel::ParentGroup);
- m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder);
- sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder);
- m_defaultSearchViewState = header()->saveState();
-
- // Configure default list view state and save for later use
- header()->hideSection(EntryModel::ParentGroup);
- m_sortModel->sort(EntryModel::Title, Qt::AscendingOrder);
- sortByColumn(EntryModel::Title, Qt::AscendingOrder);
- m_defaultListViewState = header()->saveState();
-
- m_model->setPaperClipPixmap(filePath()->icon("actions", "paperclip").pixmap(16));
}
void EntryView::contextMenuShortcutPressed()
@@ -140,6 +134,31 @@ void EntryView::contextMenuShortcutPressed()
}
}
+void EntryView::sortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
+{
+ int oldIndex = m_lastIndex;
+ m_lastIndex = logicalIndex;
+ Qt::SortOrder oldOrder = m_lastOrder;
+ m_lastOrder = order;
+
+ if (oldIndex == logicalIndex // same index
+ && oldOrder == Qt::DescendingOrder // old order is descending
+ && order == Qt::AscendingOrder) // new order is ascending
+ {
+ // a change from descending to ascending on the same column occurred
+ // this sets the header into no sort order
+ header()->setSortIndicator(-1, Qt::AscendingOrder);
+ // do not emit any signals, header()->setSortIndicator recursively calls this
+ // function and the signals are emitted in the else part
+ } else {
+ // call emitEntrySelectionChanged even though the selection did not really change
+ // this triggers the evaluation of the menu activation and anyway, the position
+ // of the selected entry within the widget did change
+ emitEntrySelectionChanged();
+ emit viewStateChanged();
+ }
+}
+
void EntryView::keyPressEvent(QKeyEvent* event)
{
if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) {
@@ -219,6 +238,11 @@ bool EntryView::inSearchMode()
return m_inSearchMode;
}
+bool EntryView::isSorted()
+{
+ return header()->sortIndicatorSection() != -1;
+}
+
void EntryView::emitEntryActivated(const QModelIndex& index)
{
Entry* entry = entryFromIndex(index);
@@ -266,6 +290,17 @@ Entry* EntryView::entryFromIndex(const QModelIndex& index)
}
}
+int EntryView::currentEntryIndex()
+{
+ QModelIndexList list = selectionModel()->selectedRows();
+ if (list.size() == 1) {
+ auto index = m_sortModel->mapToSource(list.first());
+ return index.row();
+ } else {
+ return -1;
+ }
+}
+
/**
* Get current state of 'Hide Usernames' setting (NOTE: just pass-through for
* m_model)
@@ -325,6 +360,7 @@ bool EntryView::setViewState(const QByteArray& state)
{
bool status = header()->restoreState(state);
resetFixedColumns();
+ m_columnsNeedRelayout = state.isEmpty();
return status;
}
@@ -397,9 +433,11 @@ void EntryView::toggleColumnVisibility(QAction* action)
*/
void EntryView::fitColumnsToWindow()
{
- header()->resizeSections(QHeaderView::Stretch);
+ header()->setSectionResizeMode(QHeaderView::Stretch);
+ resetFixedColumns();
+ QCoreApplication::processEvents();
+ header()->setSectionResizeMode(QHeaderView::Interactive);
resetFixedColumns();
- fillRemainingWidth(true);
emit viewStateChanged();
}
@@ -409,69 +447,89 @@ void EntryView::fitColumnsToWindow()
*/
void EntryView::fitColumnsToContents()
{
- // Resize columns to fit contents
- header()->resizeSections(QHeaderView::ResizeToContents);
+ header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ resetFixedColumns();
+ QCoreApplication::processEvents();
+ header()->setSectionResizeMode(QHeaderView::Interactive);
resetFixedColumns();
- fillRemainingWidth(false);
emit viewStateChanged();
}
/**
- * Reset view to defaults
+ * Mark icon-only columns as fixed and resize them to their minimum section size.
+ */
+void EntryView::resetFixedColumns()
+{
+ header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
+ header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
+
+ header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
+ header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
+}
+
+/**
+ * Reset item view to defaults.
*/
void EntryView::resetViewToDefaults()
{
m_model->setUsernamesHidden(false);
m_model->setPasswordsHidden(true);
+ // Reduce number of columns that are shown by default
if (m_inSearchMode) {
- header()->restoreState(m_defaultSearchViewState);
+ header()->showSection(EntryModel::ParentGroup);
} else {
- header()->restoreState(m_defaultListViewState);
+ header()->hideSection(EntryModel::ParentGroup);
+ }
+ header()->showSection(EntryModel::Title);
+ header()->showSection(EntryModel::Username);
+ header()->showSection(EntryModel::Url);
+ header()->showSection(EntryModel::Notes);
+ header()->showSection(EntryModel::Modified);
+ header()->showSection(EntryModel::Paperclip);
+ header()->showSection(EntryModel::Totp);
+
+ header()->hideSection(EntryModel::Password);
+ header()->hideSection(EntryModel::Expires);
+ header()->hideSection(EntryModel::Created);
+ header()->hideSection(EntryModel::Accessed);
+ header()->hideSection(EntryModel::Attachments);
+ header()->hideSection(EntryModel::Size);
+
+ // Reset column order to logical indices
+ for (int i = 0; i < header()->count(); ++i) {
+ header()->moveSection(header()->visualIndex(i), i);
}
- fitColumnsToWindow();
-}
+ // Reorder some columns
+ header()->moveSection(header()->visualIndex(EntryModel::Paperclip), 1);
+ header()->moveSection(header()->visualIndex(EntryModel::Totp), 2);
-void EntryView::fillRemainingWidth(bool lastColumnOnly)
-{
- // Determine total width of currently visible columns
- int width = 0;
- int lastColumnIndex = 0;
- for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) {
- if (!header()->isSectionHidden(columnIndex)) {
- width += header()->sectionSize(columnIndex);
- }
- if (header()->visualIndex(columnIndex) > lastColumnIndex) {
- lastColumnIndex = header()->visualIndex(columnIndex);
- }
- }
+ // Sort by title or group (depending on the mode)
+ m_sortModel->sort(EntryModel::Title, Qt::AscendingOrder);
+ sortByColumn(EntryModel::Title, Qt::AscendingOrder);
- int numColumns = header()->count() - header()->hiddenSectionCount();
- int availWidth = header()->width() - width;
- if ((numColumns <= 0) || (availWidth <= 0)) {
- return;
+ if (m_inSearchMode) {
+ m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder);
+ sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder);
}
- if (!lastColumnOnly) {
- // Equally distribute remaining width to visible columns
- int add = availWidth / numColumns;
- width = 0;
- for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) {
- if (!header()->isSectionHidden(columnIndex)) {
- header()->resizeSection(columnIndex, header()->sectionSize(columnIndex) + add);
- width += header()->sectionSize(columnIndex);
- }
- }
+ // The following call only relayouts reliably if the widget has been shown
+ // already, so only do it if the widget is visible and let showEvent() handle
+ // the initial default layout.
+ if (isVisible()) {
+ fitColumnsToWindow();
}
-
- // Add remaining width to last column
- header()->resizeSection(header()->logicalIndex(lastColumnIndex),
- header()->sectionSize(lastColumnIndex) + (header()->width() - width));
}
-void EntryView::resetFixedColumns()
+void EntryView::showEvent(QShowEvent* event)
{
- header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
- header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
+ QTreeView::showEvent(event);
+
+ // Check if header columns need to be resized to sensible defaults.
+ // This is only needed if no previous view state has been loaded.
+ if (m_columnsNeedRelayout) {
+ fitColumnsToWindow();
+ m_columnsNeedRelayout = false;
+ }
}
diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h
index 53de7aff5..e32aa4729 100644
--- a/src/gui/entry/EntryView.h
+++ b/src/gui/entry/EntryView.h
@@ -39,7 +39,9 @@ public:
Entry* currentEntry();
void setCurrentEntry(Entry* entry);
Entry* entryFromIndex(const QModelIndex& index);
+ int currentEntryIndex();
bool inSearchMode();
+ bool isSorted();
int numberOfSelectedEntries();
void setFirstEntryActive();
bool isUsernamesHidden() const;
@@ -63,6 +65,7 @@ protected:
void keyPressEvent(QKeyEvent* event) override;
void focusInEvent(QFocusEvent* event) override;
void focusOutEvent(QFocusEvent* event) override;
+ void showEvent(QShowEvent* event) override;
private slots:
void emitEntryActivated(const QModelIndex& index);
@@ -73,17 +76,17 @@ private slots:
void fitColumnsToContents();
void resetViewToDefaults();
void contextMenuShortcutPressed();
+ void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
private:
- void fillRemainingWidth(bool lastColumnOnly);
void resetFixedColumns();
EntryModel* const m_model;
SortFilterHideProxyModel* const m_sortModel;
+ int m_lastIndex;
+ Qt::SortOrder m_lastOrder;
bool m_inSearchMode;
-
- QByteArray m_defaultListViewState;
- QByteArray m_defaultSearchViewState;
+ bool m_columnsNeedRelayout = true;
QMenu* m_headerMenu;
QAction* m_hideUsernamesAction;
diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp
index 30d8fb913..b77e49864 100644
--- a/src/gui/group/EditGroupWidget.cpp
+++ b/src/gui/group/EditGroupWidget.cpp
@@ -20,8 +20,8 @@
#include "ui_EditGroupWidgetMain.h"
#include "core/Config.h"
-#include "core/FilePath.h"
#include "core/Metadata.h"
+#include "core/Resources.h"
#include "gui/EditWidgetIcons.h"
#include "gui/EditWidgetProperties.h"
#include "gui/MessageBox.h"
@@ -69,12 +69,12 @@ EditGroupWidget::EditGroupWidget(QWidget* parent)
{
m_mainUi->setupUi(m_editGroupWidgetMain);
- addPage(tr("Group"), FilePath::instance()->icon("actions", "document-edit"), m_editGroupWidgetMain);
- addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_editGroupWidgetIcons);
+ addPage(tr("Group"), Resources::instance()->icon("document-edit"), m_editGroupWidgetMain);
+ addPage(tr("Icon"), Resources::instance()->icon("preferences-desktop-icons"), m_editGroupWidgetIcons);
#if defined(WITH_XC_KEESHARE)
addEditPage(new EditGroupPageKeeShare(this));
#endif
- addPage(tr("Properties"), FilePath::instance()->icon("actions", "document-properties"), m_editWidgetProperties);
+ addPage(tr("Properties"), Resources::instance()->icon("document-properties"), m_editWidgetProperties);
connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool)));
connect(m_mainUi->autoTypeSequenceCustomRadio,
@@ -153,7 +153,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
}
m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence());
- if (config()->get("GUI/MonospaceNotes", false).toBool()) {
+ if (config()->get(Config::GUI_MonospaceNotes).toBool()) {
m_mainUi->editNotes->setFont(Font::fixedFont());
} else {
m_mainUi->editNotes->setFont(Font::defaultFont());
@@ -236,7 +236,7 @@ void EditGroupWidget::apply()
void EditGroupWidget::cancel()
{
- if (!m_group->iconUuid().isNull() && !m_db->metadata()->containsCustomIcon(m_group->iconUuid())) {
+ if (!m_group->iconUuid().isNull() && !m_db->metadata()->hasCustomIcon(m_group->iconUuid())) {
m_group->setIcon(Entry::DefaultIconNumber);
}
diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui
index 486e408b6..9531cc847 100644
--- a/src/gui/group/EditGroupWidgetMain.ui
+++ b/src/gui/group/EditGroupWidgetMain.ui
@@ -6,13 +6,13 @@
<rect>
<x>0</x>
<y>0</y>
- <width>579</width>
- <height>407</height>
+ <width>410</width>
+ <height>430</height>
</rect>
</property>
- <layout class="QGridLayout" name="gridLayout">
+ <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,1" rowminimumheight="0,0,0,0,0,0,0,0,0,1">
<property name="leftMargin">
- <number>10</number>
+ <number>0</number>
</property>
<property name="topMargin">
<number>0</number>
@@ -23,64 +23,30 @@
<property name="bottomMargin">
<number>0</number>
</property>
- <item row="0" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="labelName">
- <property name="text">
- <string>Name</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="editName">
- <property name="accessibleName">
- <string>Name field</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0" alignment="Qt::AlignRight|Qt::AlignTop">
- <widget class="QLabel" name="labelNotes">
- <property name="text">
- <string>Notes</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QPlainTextEdit" name="editNotes">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>120</height>
- </size>
- </property>
- <property name="accessibleName">
- <string>Notes field</string>
- </property>
- </widget>
- </item>
- <item row="2" column="0" alignment="Qt::AlignRight">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>8</number>
+ </property>
+ <item row="3" column="0">
<widget class="QCheckBox" name="expireCheck">
<property name="accessibleName">
<string>Toggle expiration</string>
</property>
<property name="text">
- <string>Expires</string>
+ <string>Expires:</string>
</property>
</widget>
</item>
- <item row="4" column="1">
- <widget class="QComboBox" name="autotypeComboBox">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="editName">
<property name="accessibleName">
- <string>Auto-Type toggle for this and sub groups</string>
+ <string>Name field</string>
</property>
</widget>
</item>
- <item row="2" column="1">
+ <item row="3" column="1">
<widget class="QDateTimeEdit" name="expireDatePicker">
<property name="enabled">
<bool>false</bool>
@@ -93,42 +59,68 @@
</property>
</widget>
</item>
- <item row="3" column="0" alignment="Qt::AlignRight">
- <widget class="QLabel" name="searchLabel">
+ <item row="6" column="1">
+ <widget class="QRadioButton" name="autoTypeSequenceInherit">
<property name="text">
- <string>Search</string>
+ <string>Use default Auto-Type sequence of parent group</string>
</property>
</widget>
</item>
- <item row="3" column="1">
- <widget class="QComboBox" name="searchComboBox">
- <property name="accessibleName">
- <string>Search toggle for this and sub groups</string>
- </property>
- </widget>
- </item>
- <item row="4" column="0" alignment="Qt::AlignRight">
+ <item row="5" column="0">
<widget class="QLabel" name="autotypeLabel">
<property name="text">
- <string>Auto-Type</string>
+ <string>Auto-Type:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="5" column="1">
- <widget class="QRadioButton" name="autoTypeSequenceInherit">
+ <item row="4" column="0">
+ <widget class="QLabel" name="searchLabel">
<property name="text">
- <string>&amp;Use default Auto-Type sequence of parent group</string>
+ <string>Search:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
- <item row="6" column="1">
- <widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
- <property name="text">
- <string>Set default Auto-Type se&amp;quence</string>
+ <item row="5" column="1">
+ <widget class="QComboBox" name="autotypeComboBox">
+ <property name="accessibleName">
+ <string>Auto-Type toggle for this and sub groups</string>
</property>
</widget>
</item>
- <item row="7" column="1">
+ <item row="1" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="labelNotes">
+ <property name="text">
+ <string>Notes:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_2">
@@ -140,8 +132,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>13</width>
- <height>1</height>
+ <width>30</width>
+ <height>0</height>
</size>
</property>
</spacer>
@@ -161,8 +153,51 @@
</item>
</layout>
</item>
- <item row="8" column="1">
- <spacer name="verticalSpacer">
+ <item row="1" column="1">
+ <widget class="QPlainTextEdit" name="editNotes">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <property name="accessibleName">
+ <string>Notes field</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelName">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
+ <property name="text">
+ <string>Set default Auto-Type sequence</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QComboBox" name="searchComboBox">
+ <property name="accessibleName">
+ <string>Search toggle for this and sub groups</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0">
+ <spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@@ -183,6 +218,9 @@
<tabstop>expireDatePicker</tabstop>
<tabstop>searchComboBox</tabstop>
<tabstop>autotypeComboBox</tabstop>
+ <tabstop>autoTypeSequenceInherit</tabstop>
+ <tabstop>autoTypeSequenceCustomRadio</tabstop>
+ <tabstop>autoTypeSequenceCustomEdit</tabstop>
</tabstops>
<resources/>
<connections/>
diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp
index d3f2f40f6..beba13774 100644
--- a/src/gui/group/GroupModel.cpp
+++ b/src/gui/group/GroupModel.cpp
@@ -130,18 +130,20 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const
#endif
return nameTemplate.arg(group->name());
} else if (role == Qt::DecorationRole) {
- QPixmap pixmap = group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex)
- : group->iconScaledPixmap();
-#if defined(WITH_XC_KEESHARE)
- pixmap = KeeShare::indicatorBadge(group, pixmap);
-#endif
- return pixmap;
+ return group->iconPixmap();
} else if (role == Qt::FontRole) {
QFont font;
if (group->isExpired()) {
font.setStrikeOut(true);
}
return font;
+ } else if (role == Qt::ToolTipRole) {
+ QString tooltip;
+ if (!group->parentGroup()) {
+ // only show a tooltip for the root group
+ tooltip = m_db->filePath();
+ }
+ return tooltip;
} else {
return QVariant();
}
@@ -297,7 +299,7 @@ bool GroupModel::dropMimeData(const QMimeData* data,
Database* targetDb = parentGroup->database();
QUuid customIcon = entry->iconUuid();
- if (sourceDb != targetDb && !customIcon.isNull() && !targetDb->metadata()->containsCustomIcon(customIcon)) {
+ if (sourceDb != targetDb && !customIcon.isNull() && !targetDb->metadata()->hasCustomIcon(customIcon)) {
targetDb->metadata()->addCustomIcon(customIcon, sourceDb->metadata()->customIcon(customIcon));
}
diff --git a/src/gui/group/GroupView.cpp b/src/gui/group/GroupView.cpp
index 33c591696..056015ca8 100644
--- a/src/gui/group/GroupView.cpp
+++ b/src/gui/group/GroupView.cpp
@@ -38,9 +38,10 @@ GroupView::GroupView(Database* db, QWidget* parent)
// clang-format off
connect(this, SIGNAL(expanded(QModelIndex)), SLOT(expandedChanged(QModelIndex)));
connect(this, SIGNAL(collapsed(QModelIndex)), SLOT(expandedChanged(QModelIndex)));
+ connect(this, SIGNAL(clicked(QModelIndex)), SIGNAL(groupSelectionChanged()));
connect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(syncExpandedState(QModelIndex,int,int)));
connect(m_model, SIGNAL(modelReset()), SLOT(modelReset()));
- connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(emitGroupChanged()));
+ connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SIGNAL(groupSelectionChanged()));
// clang-format on
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
@@ -85,7 +86,7 @@ void GroupView::dragMoveEvent(QDragMoveEvent* event)
void GroupView::focusInEvent(QFocusEvent* event)
{
- emitGroupChanged();
+ emit groupFocused();
QTreeView::focusInEvent(event);
}
@@ -140,11 +141,6 @@ void GroupView::setModel(QAbstractItemModel* model)
Q_ASSERT(false);
}
-void GroupView::emitGroupChanged()
-{
- emit groupSelectionChanged(currentGroup());
-}
-
void GroupView::syncExpandedState(const QModelIndex& parent, int start, int end)
{
for (int row = start; row <= end; row++) {
@@ -155,10 +151,11 @@ void GroupView::syncExpandedState(const QModelIndex& parent, int start, int end)
void GroupView::setCurrentGroup(Group* group)
{
- if (group == nullptr)
+ if (group == nullptr) {
setCurrentIndex(QModelIndex());
- else
+ } else {
setCurrentIndex(m_model->index(group));
+ }
}
void GroupView::modelReset()
diff --git a/src/gui/group/GroupView.h b/src/gui/group/GroupView.h
index 00b5a28c0..aa4fd85de 100644
--- a/src/gui/group/GroupView.h
+++ b/src/gui/group/GroupView.h
@@ -38,11 +38,11 @@ public:
void sortGroups(bool reverse = false);
signals:
- void groupSelectionChanged(Group* group);
+ void groupSelectionChanged();
+ void groupFocused();
private slots:
void expandedChanged(const QModelIndex& index);
- void emitGroupChanged();
void syncExpandedState(const QModelIndex& parent, int start, int end);
void modelReset();
void contextMenuShortcutPressed();
diff --git a/src/gui/osutils/OSUtils.h b/src/gui/osutils/OSUtils.h
new file mode 100644
index 000000000..dd1bd8cd1
--- /dev/null
+++ b/src/gui/osutils/OSUtils.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_OSUTILS_H
+#define KEEPASSXC_OSUTILS_H
+
+#include "OSUtilsBase.h"
+#include <QtCore>
+
+#if defined(Q_OS_WIN)
+
+#include "winutils/WinUtils.h"
+#define osUtils static_cast<OSUtilsBase*>(winUtils())
+
+#elif defined(Q_OS_MACOS)
+
+#include "macutils/MacUtils.h"
+#define osUtils static_cast<OSUtilsBase*>(macUtils())
+
+#elif defined(Q_OS_UNIX)
+
+#include "nixutils/NixUtils.h"
+#define osUtils static_cast<OSUtilsBase*>(nixUtils())
+
+#endif
+
+#endif // KEEPASSXC_OSUTILS_H
diff --git a/src/gui/osutils/OSUtilsBase.cpp b/src/gui/osutils/OSUtilsBase.cpp
new file mode 100644
index 000000000..143cb72c1
--- /dev/null
+++ b/src/gui/osutils/OSUtilsBase.cpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "OSUtilsBase.h"
+
+OSUtilsBase::OSUtilsBase(QObject* parent)
+ : QObject(parent)
+{
+}
+
+OSUtilsBase::~OSUtilsBase()
+{
+}
diff --git a/src/gui/osutils/OSUtilsBase.h b/src/gui/osutils/OSUtilsBase.h
new file mode 100644
index 000000000..340e9bf7e
--- /dev/null
+++ b/src/gui/osutils/OSUtilsBase.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_OSUTILSBASE_H
+#define KEEPASSXC_OSUTILSBASE_H
+
+#include <QObject>
+#include <QPointer>
+
+/**
+ * Abstract base class for generic OS-specific functionality
+ * which can be reasonably expected to be available on all platforms.
+ */
+class OSUtilsBase : public QObject
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @return OS dark mode enabled.
+ */
+ virtual bool isDarkMode() const = 0;
+
+ /**
+ * @return KeePassXC set to launch at system startup (autostart).
+ */
+ virtual bool isLaunchAtStartupEnabled() const = 0;
+
+ /**
+ * @param enable Add or remove KeePassXC from system autostart.
+ */
+ virtual void setLaunchAtStartup(bool enable) = 0;
+
+ /**
+ * @return OS caps lock enabled.
+ */
+ virtual bool isCapslockEnabled() = 0;
+
+protected:
+ explicit OSUtilsBase(QObject* parent = nullptr);
+ virtual ~OSUtilsBase();
+};
+
+#endif // KEEPASSXC_OSUTILSBASE_H
diff --git a/src/gui/macutils/AppKit.h b/src/gui/osutils/macutils/AppKit.h
index 2bff8f5b3..a6f7b3a12 100644
--- a/src/gui/macutils/AppKit.h
+++ b/src/gui/osutils/macutils/AppKit.h
@@ -39,6 +39,7 @@ public:
bool isDarkMode();
bool enableAccessibility();
bool enableScreenRecording();
+ void toggleForegroundApp(bool foreground);
signals:
void lockDatabases();
diff --git a/src/gui/macutils/AppKitImpl.h b/src/gui/osutils/macutils/AppKitImpl.h
index 326879766..5dadc31dd 100644
--- a/src/gui/macutils/AppKitImpl.h
+++ b/src/gui/osutils/macutils/AppKitImpl.h
@@ -38,5 +38,6 @@
- (void) userSwitchHandler:(NSNotification*) notification;
- (bool) enableAccessibility;
- (bool) enableScreenRecording;
+- (void) toggleForegroundApp:(bool) foreground;
@end
diff --git a/src/gui/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm
index 4a93f963a..077dd71a6 100644
--- a/src/gui/macutils/AppKitImpl.mm
+++ b/src/gui/osutils/macutils/AppKitImpl.mm
@@ -154,6 +154,16 @@
return YES;
}
+- (void) toggleForegroundApp:(bool) foreground
+{
+ ProcessSerialNumber psn = {0, kCurrentProcess};
+ if (foreground) {
+ TransformProcessType(&psn, kProcessTransformToForegroundApplication);
+ } else {
+ TransformProcessType(&psn, kProcessTransformToUIElementApplication);
+ }
+}
+
@end
//
@@ -215,3 +225,8 @@ bool AppKit::enableScreenRecording()
{
return [static_cast<id>(self) enableScreenRecording];
}
+
+void AppKit::toggleForegroundApp(bool foreground)
+{
+ [static_cast<id>(self) toggleForegroundApp:foreground];
+}
diff --git a/src/gui/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp
index 211aaa7eb..d32a15612 100644
--- a/src/gui/macutils/MacUtils.cpp
+++ b/src/gui/osutils/macutils/MacUtils.cpp
@@ -18,11 +18,18 @@
#include "MacUtils.h"
#include <QApplication>
+#include <QDir>
+#include <QFile>
+#include <QSettings>
+#include <QStandardPaths>
-MacUtils* MacUtils::m_instance = nullptr;
+#include <CoreGraphics/CGEventSource.h>
+
+
+QPointer<MacUtils> MacUtils::m_instance = nullptr;
MacUtils::MacUtils(QObject* parent)
- : QObject(parent)
+ : OSUtilsBase(parent)
, m_appkit(new AppKit())
{
connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases()));
@@ -71,11 +78,6 @@ bool MacUtils::isHidden()
return m_appkit->isHidden(m_appkit->ownProcessId());
}
-bool MacUtils::isDarkMode()
-{
- return m_appkit->isDarkMode();
-}
-
bool MacUtils::enableAccessibility()
{
return m_appkit->enableAccessibility();
@@ -85,3 +87,50 @@ bool MacUtils::enableScreenRecording()
{
return m_appkit->enableScreenRecording();
}
+
+bool MacUtils::isDarkMode() const
+{
+ return m_appkit->isDarkMode();
+}
+
+QString MacUtils::getLaunchAgentFilename() const
+{
+ auto launchAgentDir = QDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/../LaunchAgents"));
+ return QFile(launchAgentDir.absoluteFilePath(
+ qApp->property("KPXC_QUALIFIED_APPNAME").toString().append(".plist"))).fileName();
+}
+
+bool MacUtils::isLaunchAtStartupEnabled() const
+{
+ return QFile::exists(getLaunchAgentFilename());
+}
+
+void MacUtils::setLaunchAtStartup(bool enable)
+{
+ if (enable) {
+ QSettings agent(getLaunchAgentFilename(), QSettings::NativeFormat);
+ agent.setValue("Label", qApp->property("KPXC_QUALIFIED_APPNAME").toString());
+ agent.setValue("ProgramArguments", QStringList() << QApplication::applicationFilePath());
+ agent.setValue("RunAtLoad", true);
+ agent.setValue("StandardErrorPath", "/dev/null");
+ agent.setValue("StandardOutPath", "/dev/null");
+ } else if (isLaunchAtStartupEnabled()) {
+ QFile::remove(getLaunchAgentFilename());
+ }
+}
+
+bool MacUtils::isCapslockEnabled()
+{
+ return (CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState) & kCGEventFlagMaskAlphaShift) != 0;
+}
+
+/**
+ * Toggle application state between foreground app and UIElement app.
+ * Foreground apps have dock icons, UIElement apps do not.
+ *
+ * @param foreground whether app is foreground app
+ */
+void MacUtils::toggleForegroundApp(bool foreground)
+{
+ m_appkit->toggleForegroundApp(foreground);
+}
diff --git a/src/gui/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h
index 3e35994b1..281c5438c 100644
--- a/src/gui/macutils/MacUtils.h
+++ b/src/gui/osutils/macutils/MacUtils.h
@@ -19,17 +19,24 @@
#ifndef KEEPASSXC_MACUTILS_H
#define KEEPASSXC_MACUTILS_H
+#include "gui/osutils/OSUtilsBase.h"
#include "AppKit.h"
-#include <QObject>
-#include <QWidget>
-class MacUtils : public QObject
+#include <QPointer>
+#include <QScopedPointer>
+#include <qwindowdefs.h>
+
+class MacUtils : public OSUtilsBase
{
Q_OBJECT
public:
static MacUtils* instance();
- static void createTestInstance();
+
+ bool isDarkMode() const override;
+ bool isLaunchAtStartupEnabled() const override;
+ void setLaunchAtStartup(bool enable) override;
+ bool isCapslockEnabled() override;
WId activeWindow();
bool raiseWindow(WId pid);
@@ -37,20 +44,22 @@ public:
bool raiseOwnWindow();
bool hideOwnWindow();
bool isHidden();
- bool isDarkMode();
bool enableAccessibility();
bool enableScreenRecording();
+ void toggleForegroundApp(bool foreground);
signals:
void lockDatabases();
-private:
+protected:
explicit MacUtils(QObject* parent = nullptr);
- ~MacUtils();
+ ~MacUtils() override;
private:
+ QString getLaunchAgentFilename() const;
+
QScopedPointer<AppKit> m_appkit;
- static MacUtils* m_instance;
+ static QPointer<MacUtils> m_instance;
Q_DISABLE_COPY(MacUtils)
};
diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp
new file mode 100644
index 000000000..b252458e5
--- /dev/null
+++ b/src/gui/osutils/nixutils/NixUtils.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "NixUtils.h"
+
+#include <QApplication>
+#include <QColor>
+#include <QDir>
+#include <QFile>
+#include <QGuiApplication>
+#include <QPalette>
+#include <QStandardPaths>
+#include <QStyle>
+#include <QTextStream>
+
+#include <qpa/qplatformnativeinterface.h>
+// namespace required to avoid name clashes with declarations in XKBlib.h
+namespace X11
+{
+#include <X11/XKBlib.h>
+}
+
+QPointer<NixUtils> NixUtils::m_instance = nullptr;
+
+NixUtils* NixUtils::instance()
+{
+ if (!m_instance) {
+ m_instance = new NixUtils(qApp);
+ }
+
+ return m_instance;
+}
+
+NixUtils::NixUtils(QObject* parent)
+ : OSUtilsBase(parent)
+{
+}
+
+NixUtils::~NixUtils()
+{
+}
+
+bool NixUtils::isDarkMode() const
+{
+ if (!qApp || !qApp->style()) {
+ return false;
+ }
+ return qApp->style()->standardPalette().color(QPalette::Window).toHsl().lightness() < 110;
+}
+
+QString NixUtils::getAutostartDesktopFilename(bool createDirs) const
+{
+ QDir autostartDir;
+ auto confHome = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
+ if (confHome.isEmpty()) {
+ return {};
+ }
+ autostartDir.setPath(confHome + QStringLiteral("/autostart"));
+ if (createDirs && !autostartDir.exists()) {
+ autostartDir.mkpath(".");
+ }
+
+ return QFile(autostartDir.absoluteFilePath(qApp->property("KPXC_QUALIFIED_APPNAME").toString().append(".desktop")))
+ .fileName();
+}
+
+bool NixUtils::isLaunchAtStartupEnabled() const
+{
+ return QFile::exists(getAutostartDesktopFilename());
+ ;
+}
+
+void NixUtils::setLaunchAtStartup(bool enable)
+{
+ if (enable) {
+ QFile desktopFile(getAutostartDesktopFilename(true));
+ if (!desktopFile.open(QIODevice::WriteOnly)) {
+ qWarning("Failed to create autostart desktop file.");
+ return;
+ }
+ QTextStream stream(&desktopFile);
+ stream.setCodec("UTF-8");
+ stream << QStringLiteral("[Desktop Entry]") << '\n'
+ << QStringLiteral("Name=") << QApplication::applicationDisplayName() << '\n'
+ << QStringLiteral("GenericName=") << tr("Password Manager") << '\n'
+ << QStringLiteral("Exec=") << QApplication::applicationFilePath() << '\n'
+ << QStringLiteral("TryExec=") << QApplication::applicationFilePath() << '\n'
+ << QStringLiteral("Icon=") << QApplication::applicationName().toLower() << '\n'
+ << QStringLiteral("StartupWMClass=keepassxc") << '\n'
+ << QStringLiteral("StartupNotify=true") << '\n'
+ << QStringLiteral("Terminal=false") << '\n'
+ << QStringLiteral("Type=Application") << '\n'
+ << QStringLiteral("Version=1.0") << "true" << '\n'
+ << QStringLiteral("Categories=Utility;Security;Qt;") << '\n'
+ << QStringLiteral("MimeType=application/x-keepass2;") << '\n'
+ << QStringLiteral("X-GNOME-Autostart-enabled=true") << endl;
+ desktopFile.close();
+ } else if (isLaunchAtStartupEnabled()) {
+ QFile::remove(getAutostartDesktopFilename());
+ }
+}
+
+bool NixUtils::isCapslockEnabled()
+{
+ QPlatformNativeInterface* native = QGuiApplication::platformNativeInterface();
+ auto* display = native->nativeResourceForWindow("display", nullptr);
+ if (!display) {
+ return false;
+ }
+
+ QString platform = QGuiApplication::platformName();
+ if (platform == "xcb") {
+ unsigned state = 0;
+ if (X11::XkbGetIndicatorState(reinterpret_cast<X11::Display*>(display), XkbUseCoreKbd, &state) == Success) {
+ return ((state & 1u) != 0);
+ }
+ }
+
+ // TODO: Wayland
+
+ return false;
+}
diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h
new file mode 100644
index 000000000..c91580796
--- /dev/null
+++ b/src/gui/osutils/nixutils/NixUtils.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_NIXUTILS_H
+#define KEEPASSXC_NIXUTILS_H
+
+#include "gui/osutils/OSUtilsBase.h"
+#include <QPointer>
+
+class NixUtils : public OSUtilsBase
+{
+ Q_OBJECT
+
+public:
+ static NixUtils* instance();
+
+ bool isDarkMode() const override;
+ bool isLaunchAtStartupEnabled() const override;
+ void setLaunchAtStartup(bool enable) override;
+ bool isCapslockEnabled() override;
+
+private:
+ explicit NixUtils(QObject* parent = nullptr);
+ ~NixUtils() override;
+
+private:
+ QString getAutostartDesktopFilename(bool createDirs = false) const;
+
+ static QPointer<NixUtils> m_instance;
+
+ Q_DISABLE_COPY(NixUtils)
+};
+
+inline NixUtils* nixUtils()
+{
+ return NixUtils::instance();
+}
+
+#endif // KEEPASSXC_NIXUTILS_H
diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp
new file mode 100644
index 000000000..385a9389a
--- /dev/null
+++ b/src/gui/osutils/winutils/WinUtils.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "WinUtils.h"
+#include <QAbstractNativeEventFilter>
+#include <QApplication>
+#include <QDir>
+#include <QSettings>
+
+#include <windows.h>
+
+QPointer<WinUtils> WinUtils::m_instance = nullptr;
+QScopedPointer<WinUtils::DWMEventFilter> WinUtils::m_eventFilter;
+
+WinUtils* WinUtils::instance()
+{
+ if (!m_instance) {
+ m_instance = new WinUtils(qApp);
+ }
+
+ return m_instance;
+}
+
+WinUtils::WinUtils(QObject* parent)
+ : OSUtilsBase(parent)
+{
+}
+
+WinUtils::~WinUtils()
+{
+}
+
+/**
+ * Register event filters to handle native platform events such as theme changes.
+ */
+void WinUtils::registerEventFilters()
+{
+ if (!m_eventFilter) {
+ m_eventFilter.reset(new DWMEventFilter);
+ qApp->installNativeEventFilter(m_eventFilter.data());
+ }
+}
+
+bool WinUtils::DWMEventFilter::nativeEventFilter(const QByteArray& eventType, void* message, long*)
+{
+ if (eventType != "windows_generic_MSG") {
+ return false;
+ }
+
+ auto* msg = static_cast<MSG*>(message);
+ if (!msg->hwnd) {
+ return false;
+ }
+ switch (msg->message) {
+ case WM_CREATE:
+ case WM_INITDIALOG: {
+ if (winUtils()->isDarkMode()) {
+ // TODO: indicate dark mode support for black title bar
+ }
+ break;
+ }
+ }
+
+ return false;
+}
+
+bool WinUtils::isDarkMode() const
+{
+ QSettings settings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)",
+ QSettings::NativeFormat);
+ return settings.value("AppsUseLightTheme", 1).toInt() == 0;
+}
+
+bool WinUtils::isLaunchAtStartupEnabled() const
+{
+ return QSettings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat)
+ .contains(qAppName());
+}
+
+void WinUtils::setLaunchAtStartup(bool enable)
+{
+ QSettings reg(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run)", QSettings::NativeFormat);
+ if (enable) {
+ reg.setValue(qAppName(), QString("\"%1\"").arg(QDir::toNativeSeparators(QApplication::applicationFilePath())));
+ } else {
+ reg.remove(qAppName());
+ }
+}
+
+bool WinUtils::isCapslockEnabled()
+{
+ return GetKeyState(VK_CAPITAL) == 1;
+}
diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h
new file mode 100644
index 000000000..bf49f2c7f
--- /dev/null
+++ b/src/gui/osutils/winutils/WinUtils.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_WINUTILS_H
+#define KEEPASSXC_WINUTILS_H
+
+#include "gui/osutils/OSUtilsBase.h"
+
+#include <QAbstractNativeEventFilter>
+#include <QPointer>
+#include <QScopedPointer>
+
+class WinUtils : public OSUtilsBase
+{
+ Q_OBJECT
+
+public:
+ static WinUtils* instance();
+ static void registerEventFilters();
+
+ bool isDarkMode() const override;
+ bool isLaunchAtStartupEnabled() const override;
+ void setLaunchAtStartup(bool enable) override;
+ bool isCapslockEnabled() override;
+
+protected:
+ explicit WinUtils(QObject* parent = nullptr);
+ ~WinUtils() override;
+
+private:
+ class DWMEventFilter : public QAbstractNativeEventFilter
+ {
+ public:
+ bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override;
+ };
+
+ static QPointer<WinUtils> m_instance;
+ static QScopedPointer<DWMEventFilter> m_eventFilter;
+
+ Q_DISABLE_COPY(WinUtils)
+};
+
+inline WinUtils* winUtils()
+{
+ return WinUtils::instance();
+}
+
+#endif // KEEPASSXC_WINUTILS_H
diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp
new file mode 100644
index 000000000..2fdc24389
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.cpp
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsDialog.h"
+#include "ui_ReportsDialog.h"
+
+#include "ReportsPageHealthcheck.h"
+#include "ReportsPageHibp.h"
+#include "ReportsPageStatistics.h"
+#include "ReportsWidgetHealthcheck.h"
+#include "ReportsWidgetHibp.h"
+
+#include "core/Global.h"
+#include "touchid/TouchID.h"
+#include <core/Entry.h>
+#include <core/Group.h>
+
+class ReportsDialog::ExtraPage
+{
+public:
+ ExtraPage(QSharedPointer<IReportsPage> p, QWidget* w)
+ : page(p)
+ , widget(w)
+ {
+ }
+ void loadSettings(QSharedPointer<Database> db) const
+ {
+ page->loadSettings(widget, db);
+ }
+ void saveSettings() const
+ {
+ page->saveSettings(widget);
+ }
+
+private:
+ QSharedPointer<IReportsPage> page;
+ QWidget* widget;
+};
+
+ReportsDialog::ReportsDialog(QWidget* parent)
+ : DialogyWidget(parent)
+ , m_ui(new Ui::ReportsDialog())
+ , m_healthPage(new ReportsPageHealthcheck())
+ , m_hibpPage(new ReportsPageHibp())
+ , m_statPage(new ReportsPageStatistics())
+ , m_editEntryWidget(new EditEntryWidget(this))
+{
+ m_ui->setupUi(this);
+
+ connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
+ addPage(m_healthPage);
+ addPage(m_hibpPage);
+ addPage(m_statPage);
+
+ m_ui->stackedWidget->setCurrentIndex(0);
+
+ m_editEntryWidget->setObjectName("editEntryWidget");
+ m_editEntryWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
+ m_ui->stackedWidget->addWidget(m_editEntryWidget);
+ adjustSize();
+
+ connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
+ connect(m_healthPage->m_healthWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
+ connect(m_hibpPage->m_hibpWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
+ connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
+}
+
+ReportsDialog::~ReportsDialog()
+{
+}
+
+void ReportsDialog::load(const QSharedPointer<Database>& db)
+{
+ m_ui->categoryList->setCurrentCategory(0);
+ for (const ExtraPage& page : asConst(m_extraPages)) {
+ page.loadSettings(db);
+ }
+ m_db = db;
+}
+
+void ReportsDialog::addPage(QSharedPointer<IReportsPage> page)
+{
+ const auto category = m_ui->categoryList->currentCategory();
+ const auto widget = page->createWidget();
+ widget->setParent(this);
+ m_extraPages.append(ExtraPage(page, widget));
+ m_ui->stackedWidget->addWidget(widget);
+ m_ui->categoryList->addCategory(page->name(), page->icon());
+ m_ui->categoryList->setCurrentCategory(category);
+}
+
+void ReportsDialog::reject()
+{
+ for (const ExtraPage& extraPage : asConst(m_extraPages)) {
+ extraPage.saveSettings();
+ }
+
+#ifdef WITH_XC_TOUCHID
+ TouchID::getInstance().reset(m_db ? m_db->filePath() : "");
+#endif
+
+ emit editFinished(true);
+}
+
+void ReportsDialog::entryActivationSignalReceived(Entry* entry)
+{
+ m_sender = static_cast<QWidget*>(sender());
+ m_editEntryWidget->loadEntry(entry, false, false, entry->group()->hierarchy().join(" > "), m_db);
+ m_ui->stackedWidget->setCurrentWidget(m_editEntryWidget);
+}
+
+void ReportsDialog::switchToMainView(bool previousDialogAccepted)
+{
+ // Sanity check
+ if (!m_sender) {
+ return;
+ }
+
+ // Return to the previous widget
+ m_ui->stackedWidget->setCurrentWidget(m_sender);
+
+ // If "OK" was clicked, and if we came from the Health Check pane,
+ // re-compute Health Check
+ if (previousDialogAccepted) {
+ if (m_sender == m_healthPage->m_healthWidget) {
+ m_healthPage->m_healthWidget->calculateHealth();
+ } else if (m_sender == m_hibpPage->m_hibpWidget) {
+ m_hibpPage->m_hibpWidget->refreshAfterEdit();
+ }
+ }
+
+ // Don't process the same sender twice
+ m_sender = nullptr;
+}
diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h
new file mode 100644
index 000000000..a82d7545e
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSX_REPORTSWIDGET_H
+#define KEEPASSX_REPORTSWIDGET_H
+
+#include "config-keepassx.h"
+#include "gui/DialogyWidget.h"
+#include "gui/entry/EditEntryWidget.h"
+
+#include <QPointer>
+#include <QScopedPointer>
+#include <QSharedPointer>
+
+class Database;
+class Entry;
+class Group;
+class QTabWidget;
+class ReportsPageHealthcheck;
+class ReportsPageHibp;
+class ReportsPageStatistics;
+
+namespace Ui
+{
+ class ReportsDialog;
+}
+
+class IReportsPage
+{
+public:
+ virtual ~IReportsPage()
+ {
+ }
+ virtual QString name() = 0;
+ virtual QIcon icon() = 0;
+ virtual QWidget* createWidget() = 0;
+ virtual void loadSettings(QWidget* widget, QSharedPointer<Database> db) = 0;
+ virtual void saveSettings(QWidget* widget) = 0;
+};
+
+class ReportsDialog : public DialogyWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ReportsDialog(QWidget* parent = nullptr);
+ ~ReportsDialog() override;
+ Q_DISABLE_COPY(ReportsDialog);
+
+ void load(const QSharedPointer<Database>& db);
+ void addPage(QSharedPointer<IReportsPage> page);
+
+signals:
+ void editFinished(bool accepted);
+
+private slots:
+ void reject();
+ void entryActivationSignalReceived(Entry* entry);
+ void switchToMainView(bool previousDialogAccepted);
+
+private:
+ QSharedPointer<Database> m_db;
+ const QScopedPointer<Ui::ReportsDialog> m_ui;
+ const QSharedPointer<ReportsPageHealthcheck> m_healthPage;
+ const QSharedPointer<ReportsPageHibp> m_hibpPage;
+ const QSharedPointer<ReportsPageStatistics> m_statPage;
+ QPointer<EditEntryWidget> m_editEntryWidget;
+ QWidget* m_sender = nullptr;
+
+ class ExtraPage;
+ QList<ExtraPage> m_extraPages;
+};
+
+#endif // KEEPASSX_REPORTSWIDGET_H
diff --git a/src/gui/reports/ReportsDialog.ui b/src/gui/reports/ReportsDialog.ui
new file mode 100644
index 000000000..773981a10
--- /dev/null
+++ b/src/gui/reports/ReportsDialog.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportsDialog</class>
+ <widget class="QWidget" name="ReportsDialog">
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
+ <item>
+ <widget class="CategoryListWidget" name="categoryList" native="true"/>
+ </item>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>CategoryListWidget</class>
+ <extends>QWidget</extends>
+ <header>gui/CategoryListWidget.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/reports/ReportsPageHealthcheck.cpp b/src/gui/reports/ReportsPageHealthcheck.cpp
new file mode 100644
index 000000000..1dfe793a6
--- /dev/null
+++ b/src/gui/reports/ReportsPageHealthcheck.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsPageHealthcheck.h"
+
+#include "ReportsWidgetHealthcheck.h"
+#include "core/Resources.h"
+
+#include <QApplication>
+
+ReportsPageHealthcheck::ReportsPageHealthcheck()
+ : m_healthWidget(new ReportsWidgetHealthcheck())
+{
+}
+
+QString ReportsPageHealthcheck::name()
+{
+ return QApplication::tr("Health Check");
+}
+
+QIcon ReportsPageHealthcheck::icon()
+{
+ return Resources::instance()->icon("health");
+}
+
+QWidget* ReportsPageHealthcheck::createWidget()
+{
+ return m_healthWidget;
+}
+
+void ReportsPageHealthcheck::loadSettings(QWidget* widget, QSharedPointer<Database> db)
+{
+ const auto settingsWidget = reinterpret_cast<ReportsWidgetHealthcheck*>(widget);
+ settingsWidget->loadSettings(db);
+}
+
+void ReportsPageHealthcheck::saveSettings(QWidget* widget)
+{
+ const auto settingsWidget = reinterpret_cast<ReportsWidgetHealthcheck*>(widget);
+ settingsWidget->saveSettings();
+}
diff --git a/src/gui/reports/ReportsPageHealthcheck.h b/src/gui/reports/ReportsPageHealthcheck.h
new file mode 100644
index 000000000..8a85b2d20
--- /dev/null
+++ b/src/gui/reports/ReportsPageHealthcheck.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
+#define KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
+
+#include <QWidget>
+
+#include "ReportsDialog.h"
+
+class ReportsWidgetHealthcheck;
+
+class ReportsPageHealthcheck : public IReportsPage
+{
+public:
+ ReportsWidgetHealthcheck* m_healthWidget;
+
+ ReportsPageHealthcheck();
+
+ QString name() override;
+ QIcon icon() override;
+ QWidget* createWidget() override;
+ void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
+ void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_REPORTSPAGEHEALTHCHECK_H
diff --git a/src/gui/reports/ReportsPageHibp.cpp b/src/gui/reports/ReportsPageHibp.cpp
new file mode 100644
index 000000000..8f5640a20
--- /dev/null
+++ b/src/gui/reports/ReportsPageHibp.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsPageHibp.h"
+
+#include "ReportsWidgetHibp.h"
+#include "core/Resources.h"
+
+#include <QApplication>
+
+ReportsPageHibp::ReportsPageHibp()
+ : m_hibpWidget(new ReportsWidgetHibp())
+{
+}
+
+QString ReportsPageHibp::name()
+{
+ return QApplication::tr("HIBP");
+}
+
+QIcon ReportsPageHibp::icon()
+{
+ return resources()->icon("hibp");
+}
+
+QWidget* ReportsPageHibp::createWidget()
+{
+ return m_hibpWidget;
+}
+
+void ReportsPageHibp::loadSettings(QWidget* widget, QSharedPointer<Database> db)
+{
+ const auto settingsWidget = reinterpret_cast<ReportsWidgetHibp*>(widget);
+ settingsWidget->loadSettings(db);
+}
+
+void ReportsPageHibp::saveSettings(QWidget* widget)
+{
+ const auto settingsWidget = reinterpret_cast<ReportsWidgetHibp*>(widget);
+ settingsWidget->saveSettings();
+}
diff --git a/src/gui/reports/ReportsPageHibp.h b/src/gui/reports/ReportsPageHibp.h
new file mode 100644
index 000000000..9d74347c9
--- /dev/null
+++ b/src/gui/reports/ReportsPageHibp.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_REPORTSPAGEHIBP_H
+#define KEEPASSXC_REPORTSPAGEHIBP_H
+
+#include <QWidget>
+
+#include "ReportsDialog.h"
+
+class ReportsWidgetHibp;
+
+class ReportsPageHibp : public IReportsPage
+{
+public:
+ ReportsWidgetHibp* m_hibpWidget;
+
+ ReportsPageHibp();
+
+ QString name() override;
+ QIcon icon() override;
+ QWidget* createWidget() override;
+ void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
+ void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_REPORTSPAGEHIBP_H
diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp b/src/gui/reports/ReportsPageStatistics.cpp
index 6fe24ff0f..90cd338df 100644
--- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp
+++ b/src/gui/reports/ReportsPageStatistics.cpp
@@ -15,38 +15,36 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include "DatabaseSettingsPageStatistics.h"
+#include "ReportsPageStatistics.h"
-#include "DatabaseSettingsWidgetStatistics.h"
-#include "core/Database.h"
-#include "core/FilePath.h"
-#include "core/Group.h"
+#include "ReportsWidgetStatistics.h"
+#include "core/Resources.h"
#include <QApplication>
-QString DatabaseSettingsPageStatistics::name()
+QString ReportsPageStatistics::name()
{
return QApplication::tr("Statistics");
}
-QIcon DatabaseSettingsPageStatistics::icon()
+QIcon ReportsPageStatistics::icon()
{
- return FilePath::instance()->icon("actions", "statistics");
+ return Resources::instance()->icon("statistics");
}
-QWidget* DatabaseSettingsPageStatistics::createWidget()
+QWidget* ReportsPageStatistics::createWidget()
{
- return new DatabaseSettingsWidgetStatistics();
+ return new ReportsWidgetStatistics();
}
-void DatabaseSettingsPageStatistics::loadSettings(QWidget* widget, QSharedPointer<Database> db)
+void ReportsPageStatistics::loadSettings(QWidget* widget, QSharedPointer<Database> db)
{
- DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
+ ReportsWidgetStatistics* settingsWidget = reinterpret_cast<ReportsWidgetStatistics*>(widget);
settingsWidget->loadSettings(db);
}
-void DatabaseSettingsPageStatistics::saveSettings(QWidget* widget)
+void ReportsPageStatistics::saveSettings(QWidget* widget)
{
- DatabaseSettingsWidgetStatistics* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetStatistics*>(widget);
+ ReportsWidgetStatistics* settingsWidget = reinterpret_cast<ReportsWidgetStatistics*>(widget);
settingsWidget->saveSettings();
}
diff --git a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h b/src/gui/reports/ReportsPageStatistics.h
index c890f3b81..00d611ee3 100644
--- a/src/gui/dbsettings/DatabaseSettingsPageStatistics.h
+++ b/src/gui/reports/ReportsPageStatistics.h
@@ -15,14 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
-#define KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
+#ifndef KEEPASSXC_REPORTSPAGESTATISTICS_H
+#define KEEPASSXC_REPORTSPAGESTATISTICS_H
#include <QWidget>
-#include "DatabaseSettingsDialog.h"
+#include "ReportsDialog.h"
-class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage
+class ReportsPageStatistics : public IReportsPage
{
public:
QString name() override;
@@ -32,4 +32,4 @@ public:
void saveSettings(QWidget* widget) override;
};
-#endif // KEEPASSXC_DATABASESETTINGSPAGESTATISTICS_H
+#endif // KEEPASSXC_REPORTSPAGESTATISTICS_H
diff --git a/src/gui/reports/ReportsWidget.cpp b/src/gui/reports/ReportsWidget.cpp
new file mode 100644
index 000000000..184434116
--- /dev/null
+++ b/src/gui/reports/ReportsWidget.cpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsWidget.h"
+
+ReportsWidget::ReportsWidget(QWidget* parent)
+ : SettingsWidget(parent)
+{
+}
+
+ReportsWidget::~ReportsWidget()
+{
+}
+
+/**
+ * Load the database to be configured by this page and initialize the page.
+ * The page will NOT take ownership of the database.
+ *
+ * @param db database object to be configured
+ */
+void ReportsWidget::load(QSharedPointer<Database> db)
+{
+ m_db = std::move(db);
+ initialize();
+}
+
+const QSharedPointer<Database> ReportsWidget::getDatabase() const
+{
+ return m_db;
+}
diff --git a/src/gui/reports/ReportsWidget.h b/src/gui/reports/ReportsWidget.h
new file mode 100644
index 000000000..631490405
--- /dev/null
+++ b/src/gui/reports/ReportsWidget.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGET_H
+#define KEEPASSXC_REPORTSWIDGET_H
+
+#include "gui/settings/SettingsWidget.h"
+
+#include <QSharedPointer>
+
+class Database;
+
+/**
+ * Pure-virtual base class for KeePassXC database settings widgets.
+ */
+class ReportsWidget : public SettingsWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ReportsWidget(QWidget* parent = nullptr);
+ Q_DISABLE_COPY(ReportsWidget);
+ ~ReportsWidget() override;
+
+ virtual void load(QSharedPointer<Database> db);
+
+ const QSharedPointer<Database> getDatabase() const;
+
+signals:
+ /**
+ * Can be emitted to indicate size changes and allow parents widgets to adjust properly.
+ */
+ void sizeChanged();
+
+protected:
+ QSharedPointer<Database> m_db;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGET_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
new file mode 100644
index 000000000..5f502b16b
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsWidgetHealthcheck.h"
+#include "ui_ReportsWidgetHealthcheck.h"
+
+#include "core/AsyncTask.h"
+#include "core/Database.h"
+#include "core/Global.h"
+#include "core/Group.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
+#include "gui/styles/StateColorPalette.h"
+
+#include <QMenu>
+#include <QSharedPointer>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+
+namespace
+{
+ class Health
+ {
+ public:
+ struct Item
+ {
+ QPointer<const Group> group;
+ QPointer<const Entry> entry;
+ QSharedPointer<PasswordHealth> health;
+ bool knownBad = false;
+
+ Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
+ : group(g)
+ , entry(e)
+ , health(h)
+ , knownBad(e->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && e->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR)
+ {
+ }
+
+ bool operator<(const Item& rhs) const
+ {
+ return health->score() < rhs.health->score();
+ }
+ };
+
+ explicit Health(QSharedPointer<Database>);
+
+ const QList<QSharedPointer<Item>>& items() const
+ {
+ return m_items;
+ }
+
+ bool anyKnownBad() const
+ {
+ return m_anyKnownBad;
+ }
+
+ private:
+ QSharedPointer<Database> m_db;
+ HealthChecker m_checker;
+ QList<QSharedPointer<Item>> m_items;
+ bool m_anyKnownBad = false;
+ };
+} // namespace
+
+Health::Health(QSharedPointer<Database> db)
+ : m_db(db)
+ , m_checker(db)
+{
+ for (const auto* group : db->rootGroup()->groupsRecursive(true)) {
+ // Skip recycle bin
+ if (group->isRecycled()) {
+ continue;
+ }
+
+ for (const auto* entry : group->entries()) {
+ if (entry->isRecycled()) {
+ continue;
+ }
+
+ // Skip entries with empty password
+ if (entry->password().isEmpty()) {
+ continue;
+ }
+
+ // Evaluate this entry
+ const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
+ if (item->knownBad) {
+ m_anyKnownBad = true;
+ }
+
+ // Add entry if its password isn't at least "good"
+ if (item->health->quality() < PasswordHealth::Quality::Good) {
+ m_items.append(item);
+ }
+ }
+ }
+
+ // Sort the result so that the worst passwords (least score)
+ // are at the top
+ std::sort(m_items.begin(), m_items.end(), [](QSharedPointer<Item> x, QSharedPointer<Item> y) { return *x < *y; });
+}
+
+ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
+ : QWidget(parent)
+ , m_ui(new Ui::ReportsWidgetHealthcheck())
+ , m_errorIcon(Resources::instance()->icon("dialog-error"))
+ , m_referencesModel(new QStandardItemModel(this))
+ , m_modelProxy(new QSortFilterProxyModel(this))
+{
+ m_ui->setupUi(this);
+
+ m_modelProxy->setSourceModel(m_referencesModel.data());
+ m_ui->healthcheckTableView->setModel(m_modelProxy.data());
+ m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection);
+ m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ m_ui->healthcheckTableView->setSortingEnabled(true);
+
+ connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
+ connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+ connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
+}
+
+ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
+{
+}
+
+void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health,
+ const Group* group,
+ const Entry* entry,
+ bool knownBad)
+{
+ QString descr, tip;
+ QColor qualityColor;
+ StateColorPalette statePalette;
+ const auto quality = health->quality();
+ switch (quality) {
+ case PasswordHealth::Quality::Bad:
+ descr = tr("Bad", "Password quality");
+ tip = tr("Bad — password must be changed");
+ qualityColor = statePalette.color(StateColorPalette::HealthCritical);
+ break;
+
+ case PasswordHealth::Quality::Poor:
+ descr = tr("Poor", "Password quality");
+ tip = tr("Poor — password should be changed");
+ qualityColor = statePalette.color(StateColorPalette::HealthBad);
+ break;
+
+ case PasswordHealth::Quality::Weak:
+ descr = tr("Weak", "Password quality");
+ tip = tr("Weak — consider changing the password");
+ qualityColor = statePalette.color(StateColorPalette::HealthWeak);
+ break;
+
+ case PasswordHealth::Quality::Good:
+ case PasswordHealth::Quality::Excellent:
+ qualityColor = statePalette.color(StateColorPalette::HealthOk);
+ break;
+ }
+
+ auto title = entry->title();
+ if (knownBad) {
+ title.append(tr(" (Excluded)"));
+ }
+
+ auto row = QList<QStandardItem*>();
+ row << new QStandardItem(descr);
+ row << new QStandardItem(entry->iconPixmap(), title);
+ row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"));
+ row << new QStandardItem(QString::number(health->score()));
+ row << new QStandardItem(health->scoreReason());
+
+ // Set background color of first column according to password quality.
+ // Set the same as foreground color so the description is usually
+ // invisible, it's just for screen readers etc.
+ QBrush brush(qualityColor);
+ row[0]->setForeground(brush);
+ row[0]->setBackground(brush);
+
+ // Set tooltips
+ row[0]->setToolTip(tip);
+ if (knownBad) {
+ row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ }
+ row[4]->setToolTip(health->scoreDetails());
+
+ // Store entry pointer per table row (used in double click handler)
+ m_referencesModel->appendRow(row);
+ m_rowToEntry.append({group, entry});
+}
+
+void ReportsWidgetHealthcheck::loadSettings(QSharedPointer<Database> db)
+{
+ m_db = std::move(db);
+ m_healthCalculated = false;
+ m_referencesModel->clear();
+ m_rowToEntry.clear();
+
+ auto row = QList<QStandardItem*>();
+ row << new QStandardItem(tr("Please wait, health data is being calculated..."));
+ m_referencesModel->appendRow(row);
+}
+
+void ReportsWidgetHealthcheck::showEvent(QShowEvent* event)
+{
+ QWidget::showEvent(event);
+
+ if (!m_healthCalculated) {
+ // Perform stats calculation on next event loop to allow widget to appear
+ m_healthCalculated = true;
+ QTimer::singleShot(0, this, SLOT(calculateHealth()));
+ }
+}
+
+void ReportsWidgetHealthcheck::calculateHealth()
+{
+ m_referencesModel->clear();
+
+ // Perform the health check
+ const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
+
+ // Display entries that are marked as "known bad"?
+ const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
+
+ // Display the entries
+ m_rowToEntry.clear();
+ for (const auto& item : health->items()) {
+ if (item->knownBad && !showKnownBad) {
+ // Exclude this entry from the report
+ continue;
+ }
+
+ // Show the entry in the report
+ addHealthRow(item->health, item->group, item->entry, item->knownBad);
+ }
+
+ // Set the table header
+ if (m_referencesModel->rowCount() == 0) {
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!"));
+ } else {
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score")
+ << tr("Reason"));
+ }
+
+ m_ui->healthcheckTableView->resizeRowsToContents();
+
+ // Show the "show known bad entries" checkbox if there's any known
+ // bad entry in the database.
+ if (health->anyKnownBad()) {
+ m_ui->showKnownBadCheckBox->show();
+ } else {
+ m_ui->showKnownBadCheckBox->hide();
+ }
+}
+
+void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ auto mappedIndex = m_modelProxy->mapToSource(index);
+ const auto row = m_rowToEntry[mappedIndex.row()];
+ const auto group = row.first;
+ const auto entry = row.second;
+ if (group && entry) {
+ emit entryActivated(const_cast<Entry*>(entry));
+ }
+}
+
+void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
+{
+
+ // Find which entry has been clicked
+ const auto index = m_ui->healthcheckTableView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+ auto mappedIndex = m_modelProxy->mapToSource(index);
+ m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[mappedIndex.row()].second);
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ // Create the context menu
+ const auto menu = new QMenu(this);
+
+ // Create the "edit entry" menu item
+ const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this);
+ menu->addAction(edit);
+ connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
+
+ // Create the "exclude from reports" menu item
+ const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this);
+ knownbad->setCheckable(true);
+ knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
+ menu->addAction(knownbad);
+ connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
+
+ // Show the context menu
+ menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
+}
+
+void ReportsWidgetHealthcheck::editFromContextmenu()
+{
+ if (m_contextmenuEntry) {
+ emit entryActivated(m_contextmenuEntry);
+ }
+}
+
+void ReportsWidgetHealthcheck::toggleKnownBad(bool isKnownBad)
+{
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+
+ calculateHealth();
+}
+
+void ReportsWidgetHealthcheck::saveSettings()
+{
+ // nothing to do - the tab is passive
+}
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h
new file mode 100644
index 000000000..58ac0a03b
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
+#define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
+
+#include "gui/entry/EntryModel.h"
+#include <QHash>
+#include <QIcon>
+#include <QPair>
+#include <QWidget>
+
+class Database;
+class Entry;
+class Group;
+class PasswordHealth;
+class QSortFilterProxyModel;
+class QStandardItemModel;
+
+namespace Ui
+{
+ class ReportsWidgetHealthcheck;
+}
+
+class ReportsWidgetHealthcheck : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
+ ~ReportsWidgetHealthcheck();
+
+ void loadSettings(QSharedPointer<Database> db);
+ void saveSettings();
+
+protected:
+ void showEvent(QShowEvent* event) override;
+
+signals:
+ void entryActivated(Entry*);
+
+public slots:
+ void calculateHealth();
+ void emitEntryActivated(const QModelIndex& index);
+ void customMenuRequested(QPoint);
+ void editFromContextmenu();
+ void toggleKnownBad(bool);
+
+private:
+ void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
+
+ QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
+
+ bool m_healthCalculated = false;
+ QIcon m_errorIcon;
+ QScopedPointer<QStandardItemModel> m_referencesModel;
+ QScopedPointer<QSortFilterProxyModel> m_modelProxy;
+ QSharedPointer<Database> m_db;
+ QList<QPair<const Group*, const Entry*>> m_rowToEntry;
+ Entry* m_contextmenuEntry = nullptr;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui
new file mode 100644
index 000000000..72e4fc328
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHealthcheck.ui
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportsWidgetHealthcheck</class>
+ <widget class="QWidget" name="ReportsWidgetHealthcheck">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>505</width>
+ <height>379</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTableView" name="healthcheckTableView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideMiddle</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showKnownBadCheckBox">
+ <property name="text">
+ <string>Also show entries that have been excluded from reports</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="tipLabel">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Hover over reason to show additional details. Double-click entries to edit.</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>healthcheckTableView</tabstop>
+ <tabstop>showKnownBadCheckBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
new file mode 100644
index 000000000..48e36518d
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ReportsWidgetHibp.h"
+#include "ui_ReportsWidgetHibp.h"
+
+#include "config-keepassx.h"
+#include "core/Database.h"
+#include "core/Global.h"
+#include "core/Group.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
+#include "gui/MessageBox.h"
+
+#include <QMenu>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+
+namespace
+{
+ /*
+ * Check if an entry has been marked as "known bad password".
+ * These entries are to be excluded from the HIBP report.
+ *
+ * Question to reviewer: Should this be a member function of Entry?
+ * It's duplicated in EditEntryWidget::setForms, EditEntryWidget::updateEntryData,
+ * ReportsWidgetHealthcheck::customMenuRequested, and Health::Item::Item.
+ */
+ bool isKnownBad(const Entry* entry)
+ {
+ return entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
+ }
+} // namespace
+
+ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
+ : QWidget(parent)
+ , m_ui(new Ui::ReportsWidgetHibp())
+ , m_referencesModel(new QStandardItemModel(this))
+ , m_modelProxy(new QSortFilterProxyModel(this))
+{
+ m_ui->setupUi(this);
+
+ m_modelProxy->setSourceModel(m_referencesModel.data());
+ m_ui->hibpTableView->setModel(m_modelProxy.data());
+ m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection);
+ m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ m_ui->hibpTableView->setSortingEnabled(true);
+
+ connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+ connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
+ connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(makeHibpTable()));
+#ifdef WITH_XC_NETWORKING
+ connect(&m_downloader, SIGNAL(hibpResult(QString, int)), SLOT(addHibpResult(QString, int)));
+ connect(&m_downloader, SIGNAL(fetchFailed(QString)), SLOT(fetchFailed(QString)));
+
+ connect(m_ui->validationButton, &QPushButton::pressed, [this] { startValidation(); });
+#endif
+}
+
+ReportsWidgetHibp::~ReportsWidgetHibp()
+{
+}
+
+void ReportsWidgetHibp::loadSettings(QSharedPointer<Database> db)
+{
+ // Re-initialize
+ m_db = std::move(db);
+ m_referencesModel->clear();
+ m_pwndPasswords.clear();
+ m_error.clear();
+ m_rowToEntry.clear();
+ m_editedEntry = nullptr;
+#ifdef WITH_XC_NETWORKING
+ m_ui->stackedWidget->setCurrentIndex(0);
+ m_ui->validationButton->setEnabled(true);
+ m_ui->progressBar->hide();
+#else
+ // Compiled without networking, can't do anything
+ m_ui->stackedWidget->setCurrentIndex(2);
+#endif
+}
+
+/*
+ * Fill the table will all entries that have passwords that we've
+ * found to have been pwned.
+ */
+void ReportsWidgetHibp::makeHibpTable()
+{
+ // Reset the table
+ m_referencesModel->clear();
+ m_rowToEntry.clear();
+
+ // If there were no findings, display a motivational message
+ if (m_pwndPasswords.isEmpty() && m_error.isEmpty()) {
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, no exposed passwords!"));
+ m_ui->stackedWidget->setCurrentIndex(1);
+ return;
+ }
+
+ // Standard header labels for found issues
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Password exposed…"));
+
+ // Search database for passwords that we've found so far
+ QList<QPair<const Entry*, int>> items;
+ for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
+ if (!entry->isRecycled()) {
+ const auto found = m_pwndPasswords.find(entry->password());
+ if (found != m_pwndPasswords.end()) {
+ items.append({entry, found.value()});
+ }
+ }
+ }
+
+ // Sort decending by the number the password has been exposed
+ qSort(items.begin(), items.end(), [](QPair<const Entry*, int>& lhs, QPair<const Entry*, int>& rhs) {
+ return lhs.second > rhs.second;
+ });
+
+ // Display entries that are marked as "known bad"?
+ const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
+
+ // The colors for table cells
+ const auto red = QBrush("red");
+
+ // Build the table
+ bool anyKnownBad = false;
+ for (const auto& item : items) {
+ const auto entry = item.first;
+ const auto group = entry->group();
+ const auto count = item.second;
+ auto title = entry->title();
+
+ // If the entry is marked as known bad, hide it unless the
+ // checkbox is set.
+ bool knownBad = isKnownBad(entry);
+ if (knownBad) {
+ anyKnownBad = true;
+ if (!showKnownBad) {
+ continue;
+ }
+
+ title.append(tr(" (Excluded)"));
+ }
+
+ auto row = QList<QStandardItem*>();
+ row << new QStandardItem(entry->iconPixmap(), title)
+ << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
+ << new QStandardItem(countToText(count));
+
+ if (knownBad) {
+ row[1]->setToolTip(tr("This entry is being excluded from reports"));
+ }
+
+ row[2]->setForeground(red);
+ m_referencesModel->appendRow(row);
+
+ // Store entry pointer per table row (used in double click handler)
+ m_rowToEntry.append(entry);
+ }
+
+ // If there was an error, append the error message to the table
+ if (!m_error.isEmpty()) {
+ auto row = QList<QStandardItem*>();
+ row << new QStandardItem(m_error);
+ m_referencesModel->appendRow(row);
+ row[0]->setForeground(QBrush(QColor("red")));
+ }
+
+ // If we're done and everything is good, display a motivational message
+#ifdef WITH_XC_NETWORKING
+ if (m_downloader.passwordsRemaining() == 0 && m_pwndPasswords.isEmpty() && m_error.isEmpty()) {
+ m_referencesModel->clear();
+ m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, no exposed passwords!"));
+ }
+#endif
+
+ // Show the "show known bad entries" checkbox if there's any known
+ // bad entry in the database.
+ if (anyKnownBad) {
+ m_ui->showKnownBadCheckBox->show();
+ } else {
+ m_ui->showKnownBadCheckBox->hide();
+ }
+
+ m_ui->hibpTableView->resizeRowsToContents();
+
+ m_ui->stackedWidget->setCurrentIndex(1);
+}
+
+/*
+ * Invoked when the downloader has finished checking one password.
+ */
+void ReportsWidgetHibp::addHibpResult(const QString& password, int count)
+{
+ // Add the password to the list of our findings if it has been pwned
+ if (count > 0) {
+ m_pwndPasswords[password] = count;
+ }
+
+#ifdef WITH_XC_NETWORKING
+ // Update the progress bar
+ int remaining = m_downloader.passwordsRemaining();
+ if (remaining > 0) {
+ m_ui->progressBar->setValue(m_ui->progressBar->maximum() - remaining);
+ } else {
+ // Finished, remove the progress bar and build the table
+ m_ui->progressBar->hide();
+ makeHibpTable();
+ }
+#endif
+}
+
+/*
+ * Invoked when a query to the HIBP server fails.
+ *
+ * Displays the table with the current findings.
+ */
+void ReportsWidgetHibp::fetchFailed(const QString& error)
+{
+ m_error = error;
+ m_ui->progressBar->hide();
+ makeHibpTable();
+}
+
+/*
+ * Add passwords to the downloader and start the actual online validation.
+ */
+void ReportsWidgetHibp::startValidation()
+{
+#ifdef WITH_XC_NETWORKING
+ // Collect all passwords in the database (unless recycled, and
+ // unless empty, and unless marked as "known bad") and submit them
+ // to the downloader.
+ for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
+ if (!entry->isRecycled() && !entry->password().isEmpty()) {
+ m_downloader.add(entry->password());
+ }
+ }
+
+ // Short circuit if we didn't actually add any passwords
+ if (m_downloader.passwordsToValidate() == 0) {
+ makeHibpTable();
+ return;
+ }
+
+ // Store the number of passwords we need to check for the progress bar
+ m_ui->progressBar->show();
+ m_ui->progressBar->setMaximum(m_downloader.passwordsToValidate());
+ m_ui->validationButton->setEnabled(false);
+
+ m_downloader.validate();
+#endif
+}
+
+/*
+ * Convert the number of times a password has been pwned into
+ * a display text for the third table column.
+ */
+QString ReportsWidgetHibp::countToText(int count)
+{
+ if (count == 1) {
+ return tr("once");
+ } else if (count <= 10) {
+ return tr("up to 10 times");
+ } else if (count <= 100) {
+ return tr("up to 100 times");
+ } else if (count <= 1000) {
+ return tr("up to 1000 times");
+ } else if (count <= 10000) {
+ return tr("up to 10,000 times");
+ } else if (count <= 100000) {
+ return tr("up to 100,000 times");
+ } else if (count <= 1000000) {
+ return tr("up to a million times");
+ }
+
+ return tr("millions of times");
+}
+
+/*
+ * Double-click handler
+ */
+void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
+{
+ if (!index.isValid()) {
+ return;
+ }
+
+ // Find which database entry was double-clicked
+ auto mappedIndex = m_modelProxy->mapToSource(index);
+ const auto entry = m_rowToEntry[mappedIndex.row()];
+ if (entry) {
+ // Found it, invoke entry editor
+ m_editedEntry = entry;
+ m_editedPassword = entry->password();
+ m_editedKnownBad = isKnownBad(entry);
+ emit entryActivated(const_cast<Entry*>(entry));
+ }
+}
+
+/*
+ * Invoked after "OK" was clicked in the entry editor.
+ * Re-validates the edited entry's new password.
+ */
+void ReportsWidgetHibp::refreshAfterEdit()
+{
+ // Sanity check
+ if (!m_editedEntry) {
+ return;
+ }
+
+ // No need to re-validate if there was no change that affects
+ // the HIBP result (i. e., change to the password or to the
+ // "known bad" flag)
+ if (m_editedEntry->password() == m_editedPassword && isKnownBad(m_editedEntry) == m_editedKnownBad) {
+ // Don't go through HIBP but still rebuild the table, the user might
+ // have edited the entry title.
+ makeHibpTable();
+ return;
+ }
+
+ // Remove the previous password from the list of findings
+ m_pwndPasswords.remove(m_editedPassword);
+
+ // Validate the new password against HIBP
+#ifdef WITH_XC_NETWORKING
+ m_downloader.add(m_editedEntry->password());
+ m_downloader.validate();
+#endif
+
+ m_editedEntry = nullptr;
+}
+
+void ReportsWidgetHibp::customMenuRequested(QPoint pos)
+{
+
+ // Find which entry has been clicked
+ const auto index = m_ui->hibpTableView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+ auto mappedIndex = m_modelProxy->mapToSource(index);
+ m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[mappedIndex.row()]);
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ // Create the context menu
+ const auto menu = new QMenu(this);
+
+ // Create the "edit entry" menu item
+ const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this);
+ menu->addAction(edit);
+ connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
+
+ // Create the "exclude from reports" menu item
+ const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this);
+ knownbad->setCheckable(true);
+ knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
+ menu->addAction(knownbad);
+ connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
+
+ // Show the context menu
+ menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
+}
+
+void ReportsWidgetHibp::editFromContextmenu()
+{
+ if (m_contextmenuEntry) {
+ emit entryActivated(m_contextmenuEntry);
+ }
+}
+
+void ReportsWidgetHibp::toggleKnownBad(bool isKnownBad)
+{
+ if (!m_contextmenuEntry) {
+ return;
+ }
+
+ m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
+
+ makeHibpTable();
+}
+
+void ReportsWidgetHibp::saveSettings()
+{
+ // nothing to do - the tab is passive
+}
diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h
new file mode 100644
index 000000000..0d76c07fe
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_REPORTSWIDGETHIBP_H
+#define KEEPASSXC_REPORTSWIDGETHIBP_H
+
+#include "config-keepassx.h"
+#include "gui/entry/EntryModel.h"
+
+#include <QMap>
+#include <QPair>
+#include <QPointer>
+#include <QWidget>
+
+#ifdef WITH_XC_NETWORKING
+#include "core/HibpDownloader.h"
+#endif
+
+class Database;
+class Entry;
+class Group;
+class QSortFilterProxyModel;
+class QStandardItemModel;
+
+namespace Ui
+{
+ class ReportsWidgetHibp;
+}
+
+class ReportsWidgetHibp : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ReportsWidgetHibp(QWidget* parent = nullptr);
+ ~ReportsWidgetHibp();
+
+ void loadSettings(QSharedPointer<Database> db);
+ void saveSettings();
+ void refreshAfterEdit();
+
+signals:
+ void entryActivated(Entry*);
+
+public slots:
+ void emitEntryActivated(const QModelIndex&);
+ void addHibpResult(const QString&, int);
+ void fetchFailed(const QString& error);
+ void makeHibpTable();
+ void customMenuRequested(QPoint);
+ void editFromContextmenu();
+ void toggleKnownBad(bool);
+
+private:
+ void startValidation();
+ static QString countToText(int count);
+
+ QScopedPointer<Ui::ReportsWidgetHibp> m_ui;
+ QScopedPointer<QStandardItemModel> m_referencesModel;
+ QScopedPointer<QSortFilterProxyModel> m_modelProxy;
+ QSharedPointer<Database> m_db;
+
+ QMap<QString, int> m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
+ QString m_error; // Error message if download failed, else empty
+ QList<const Entry*> m_rowToEntry; // List index is table row
+ QPointer<const Entry> m_editedEntry; // The entry we're currently editing
+ QString m_editedPassword; // The old password of the entry we're editing
+ bool m_editedKnownBad; // The old "known bad" flag of the entry we're editing
+ Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked
+
+#ifdef WITH_XC_NETWORKING
+ HibpDownloader m_downloader; // This performs the actual HIBP online query
+#endif
+};
+
+#endif // KEEPASSXC_REPORTSWIDGETHIBP_H
diff --git a/src/gui/reports/ReportsWidgetHibp.ui b/src/gui/reports/ReportsWidgetHibp.ui
new file mode 100644
index 000000000..94582e8cd
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetHibp.ui
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportsWidgetHibp</class>
+ <widget class="QWidget" name="ReportsWidgetHibp">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>545</width>
+ <height>379</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="currentIndex">
+ <number>1</number>
+ </property>
+ <widget class="QWidget" name="confirmation">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>15</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="maximumSize">
+ <size>
+ <width>450</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>CAUTION: This report requires sending information to the Have I Been Pwned online service (https://haveibeenpwned.com). If you proceed, your database passwords will be cryptographically hashed and the first five characters of those hashes will be sent securely to this service. Your database remains secure and cannot be reconstituted from this information. However, the number of passwords you send and your IP address will be exposed to this service.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="validationButton">
+ <property name="maximumSize">
+ <size>
+ <width>275</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Perform Online Analysis</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="resultsTable">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTableView" name="hibpTableView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideMiddle</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showKnownBadCheckBox">
+ <property name="text">
+ <string>Also show entries that have been excluded from reports</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="noNetwork">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="networkNoticeLabel">
+ <property name="maximumSize">
+ <size>
+ <width>450</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>This build of KeePassXC does not have network functions. Networking is required to check your passwords against Have I Been Pwned databases.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>hibpTableView</tabstop>
+ <tabstop>showKnownBadCheckBox</tabstop>
+ <tabstop>validationButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp
index b02741adb..400d82f29 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp
+++ b/src/gui/reports/ReportsWidgetStatistics.cpp
@@ -15,15 +15,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include "DatabaseSettingsWidgetStatistics.h"
-#include "ui_DatabaseSettingsWidgetStatistics.h"
+#include "ReportsWidgetStatistics.h"
+#include "ui_ReportsWidgetStatistics.h"
#include "core/AsyncTask.h"
#include "core/Database.h"
-#include "core/FilePath.h"
+#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
-#include "zxcvbn.h"
+#include "core/PasswordHealth.h"
+#include "core/Resources.h"
#include <QFileInfo>
#include <QHash>
@@ -43,11 +44,13 @@ namespace
int nPwdsShort = 0; // Number of passwords 8 characters or less in size
int nPwdsUnique = 0; // Number of unique passwords
int nPwdsReused = 0; // Number of non-unique passwords
+ int nKnownBad = 0; // Number of known bad entries
int pwdTotalLen = 0; // Total length of all passwords
// Ctor does all the work
explicit Stats(QSharedPointer<Database> db)
: modified(QFileInfo(db->filePath()).lastModified())
+ , m_db(db)
{
gatherStats(db->rootGroup()->groupsRecursive(true));
}
@@ -92,19 +95,27 @@ namespace
}
private:
+ QSharedPointer<Database> m_db;
QHash<QString, int> m_passwords;
void gatherStats(const QList<Group*>& groups)
{
+ auto checker = HealthChecker(m_db);
+
for (const auto* group : groups) {
// Don't count anything in the recycle bin
- if (group == group->database()->metadata()->recycleBin()) {
+ if (group->isRecycled()) {
continue;
}
++nGroups;
for (const auto* entry : group->entries()) {
+ // Don't count anything in the recycle bin
+ if (entry->isRecycled()) {
+ continue;
+ }
+
++nEntries;
if (entry->isExpired()) {
@@ -125,10 +136,15 @@ namespace
}
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
- if (pwd.size() < 25 && ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr) < 65) {
+ if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
++nPwdsWeak;
}
+ if (entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
+ && entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) {
+ ++nKnownBad;
+ }
+
pwdTotalLen += pwd.size();
m_passwords[pwd]++;
}
@@ -138,24 +154,25 @@ namespace
};
} // namespace
-DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent)
+ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent)
: QWidget(parent)
- , m_ui(new Ui::DatabaseSettingsWidgetStatistics())
- , m_errIcon(FilePath::instance()->icon("status", "dialog-error"))
+ , m_ui(new Ui::ReportsWidgetStatistics())
+ , m_errIcon(Resources::instance()->icon("dialog-error"))
{
m_ui->setupUi(this);
m_referencesModel.reset(new QStandardItemModel());
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Value"));
m_ui->statisticsTableView->setModel(m_referencesModel.data());
+ m_ui->statisticsTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->statisticsTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
}
-DatabaseSettingsWidgetStatistics::~DatabaseSettingsWidgetStatistics()
+ReportsWidgetStatistics::~ReportsWidgetStatistics()
{
}
-void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
+void ReportsWidgetStatistics::addStatsRow(QString name, QString value, bool bad, QString badMsg)
{
auto row = QList<QStandardItem*>();
row << new QStandardItem(name);
@@ -170,7 +187,7 @@ void DatabaseSettingsWidgetStatistics::addStatsRow(QString name, QString value,
}
};
-void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
+void ReportsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
{
m_db = std::move(db);
m_statsCalculated = false;
@@ -178,7 +195,7 @@ void DatabaseSettingsWidgetStatistics::loadSettings(QSharedPointer<Database> db)
addStatsRow(tr("Please wait, database statistics are being calculated..."), "");
}
-void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event)
+void ReportsWidgetStatistics::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
@@ -189,9 +206,9 @@ void DatabaseSettingsWidgetStatistics::showEvent(QShowEvent* event)
}
}
-void DatabaseSettingsWidgetStatistics::calculateStats()
+void ReportsWidgetStatistics::calculateStats()
{
- const auto stats = AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); });
+ const QScopedPointer<Stats> stats(AsyncTask::runAndWaitForFuture([this] { return new Stats(m_db); }));
m_referencesModel->clear();
addStatsRow(tr("Database name"), m_db->metadata()->name());
@@ -225,13 +242,18 @@ void DatabaseSettingsWidgetStatistics::calculateStats()
QString::number(stats->nPwdsWeak),
stats->nPwdsWeak > 0,
tr("Recommend using long, randomized passwords with a rating of 'good' or 'excellent'."));
+ addStatsRow(tr("Entries excluded from reports"),
+ QString::number(stats->nKnownBad),
+ stats->nKnownBad > 0,
+ tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't "
+ "necessarily a problem but you should keep an eye on them."));
addStatsRow(tr("Average password length"),
tr("%1 characters").arg(stats->averagePwdLength()),
stats->isAvgPwdTooShort(),
tr("Average password length is less than ten characters. Longer passwords provide more security."));
}
-void DatabaseSettingsWidgetStatistics::saveSettings()
+void ReportsWidgetStatistics::saveSettings()
{
// nothing to do - the tab is passive
}
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h b/src/gui/reports/ReportsWidgetStatistics.h
index 2bd42f13d..cc11a75f5 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h
+++ b/src/gui/reports/ReportsWidgetStatistics.h
@@ -15,8 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
-#define KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
+#ifndef KEEPASSXC_REPORTSWIDGETSTATISTICS_H
+#define KEEPASSXC_REPORTSWIDGETSTATISTICS_H
#include <QIcon>
#include <QWidget>
@@ -26,15 +26,15 @@ class QStandardItemModel;
namespace Ui
{
- class DatabaseSettingsWidgetStatistics;
+ class ReportsWidgetStatistics;
}
-class DatabaseSettingsWidgetStatistics : public QWidget
+class ReportsWidgetStatistics : public QWidget
{
Q_OBJECT
public:
- explicit DatabaseSettingsWidgetStatistics(QWidget* parent = nullptr);
- ~DatabaseSettingsWidgetStatistics();
+ explicit ReportsWidgetStatistics(QWidget* parent = nullptr);
+ ~ReportsWidgetStatistics();
void loadSettings(QSharedPointer<Database> db);
void saveSettings();
@@ -46,7 +46,7 @@ private slots:
void calculateStats();
private:
- QScopedPointer<Ui::DatabaseSettingsWidgetStatistics> m_ui;
+ QScopedPointer<Ui::ReportsWidgetStatistics> m_ui;
bool m_statsCalculated = false;
QIcon m_errIcon;
@@ -56,4 +56,4 @@ private:
void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = "");
};
-#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H
+#endif // KEEPASSXC_REPORTSWIDGETSTATISTICS_H
diff --git a/src/gui/reports/ReportsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui
new file mode 100644
index 000000000..047b55cf0
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetStatistics.ui
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportsWidgetStatistics</class>
+ <widget class="QWidget" name="ReportsWidgetStatistics">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>397</width>
+ <height>379</height>
+ </rect>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTableView" name="statisticsTableView">
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>false</bool>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="textElideMode">
+ <enum>Qt::ElideMiddle</enum>
+ </property>
+ <property name="sortingEnabled">
+ <bool>false</bool>
+ </property>
+ <attribute name="horizontalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <attribute name="verticalHeaderVisible">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="tipLabel">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Hover over lines with error icons for further information.</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>statisticsTableView</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/styles/StateColorPalette.cpp b/src/gui/styles/StateColorPalette.cpp
new file mode 100644
index 000000000..c729e3269
--- /dev/null
+++ b/src/gui/styles/StateColorPalette.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "StateColorPalette.h"
+
+#include "gui/Application.h"
+
+StateColorPalette::StateColorPalette()
+{
+ if (kpxcApp->isDarkTheme()) {
+ initDefaultPaletteDark();
+ } else {
+ initDefaultPaletteLight();
+ }
+}
+
+void StateColorPalette::initDefaultPaletteLight()
+{
+ setColor(ColorRole::Error, QStringLiteral("#FF7D7D"));
+ setColor(ColorRole::Warning, QStringLiteral("#FFD30F"));
+ setColor(ColorRole::Info, QStringLiteral("#84D0E1"));
+ setColor(ColorRole::Incomplete, QStringLiteral("#FFD30F"));
+
+ setColor(ColorRole::HealthCritical, QStringLiteral("#C43F31"));
+ setColor(ColorRole::HealthBad, QStringLiteral("#E07F16"));
+ setColor(ColorRole::HealthWeak, QStringLiteral("#FFD30F"));
+ setColor(ColorRole::HealthOk, QStringLiteral("#5EA10E"));
+ setColor(ColorRole::HealthExcellent, QStringLiteral("#118f17"));
+}
+
+void StateColorPalette::initDefaultPaletteDark()
+{
+ setColor(ColorRole::Error, QStringLiteral("#802D2D"));
+ setColor(ColorRole::Warning, QStringLiteral("#73682E"));
+ setColor(ColorRole::Info, QStringLiteral("#207183"));
+ setColor(ColorRole::Incomplete, QStringLiteral("#665124"));
+
+ setColor(ColorRole::HealthCritical, QStringLiteral("#C43F31"));
+ setColor(ColorRole::HealthBad, QStringLiteral("#DB9837"));
+ setColor(ColorRole::HealthWeak, QStringLiteral("#F0C400"));
+ setColor(ColorRole::HealthOk, QStringLiteral("#608A22"));
+ setColor(ColorRole::HealthExcellent, QStringLiteral("#1F8023"));
+}
diff --git a/src/gui/styles/StateColorPalette.h b/src/gui/styles/StateColorPalette.h
new file mode 100644
index 000000000..408fe032a
--- /dev/null
+++ b/src/gui/styles/StateColorPalette.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_STATECOLORPALETTE_H
+#define KEEPASSXC_STATECOLORPALETTE_H
+
+#include <QColor>
+#include <QHash>
+#include <QObject>
+
+/**
+ * Extended color palette for indicating custom widget states.
+ */
+class StateColorPalette
+{
+ Q_GADGET
+
+public:
+ StateColorPalette();
+
+ enum ColorRole
+ {
+ Error,
+ Warning,
+ Info,
+ Incomplete,
+ HealthCritical,
+ HealthBad,
+ HealthPoor,
+ HealthWeak,
+ HealthOk,
+ HealthExcellent
+ };
+
+ inline void setColor(ColorRole role, const QColor& color)
+ {
+ m_colorMap[role] = color;
+ }
+
+ inline QColor color(ColorRole role) const
+ {
+ return m_colorMap.value(role);
+ }
+
+private:
+ void initDefaultPaletteLight();
+ void initDefaultPaletteDark();
+
+ QHash<ColorRole, QColor> m_colorMap;
+};
+
+#endif // KEEPASSXC_STATECOLORPALETTE_H
diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp
new file mode 100644
index 000000000..aac9daf1c
--- /dev/null
+++ b/src/gui/styles/base/BaseStyle.cpp
@@ -0,0 +1,4808 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ * Copyright (C) 2019 Andrew Richards
+ *
+ * Derived from Phantomstyle and relicensed under the GPLv2 or v3.
+ * https://github.com/randrew/phantomstyle
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "BaseStyle.h"
+#include "phantomcolor.h"
+
+#include <QAbstractItemView>
+#include <QApplication>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QFile>
+#include <QHeaderView>
+#include <QListView>
+#include <QMainWindow>
+#include <QMenu>
+#include <QPainter>
+#include <QPainterPath>
+#include <QPoint>
+#include <QPolygon>
+#include <QPushButton>
+#include <QScrollBar>
+#include <QSharedData>
+#include <QSlider>
+#include <QSpinBox>
+#include <QSplitter>
+#include <QString>
+#include <QStyleOption>
+#include <QTableView>
+#include <QToolBar>
+#include <QToolButton>
+#include <QTreeView>
+#include <QWindow>
+#include <QWizard>
+#include <QtMath>
+#include <qdrawutil.h>
+
+#include <cmath>
+
+QT_BEGIN_NAMESPACE
+Q_GUI_EXPORT int qt_defaultDpiX();
+QT_END_NAMESPACE
+
+// Redefine Q_FALLTHROUGH for older Qt versions
+#ifndef Q_FALLTHROUGH
+#if (defined(Q_CC_GNU) && Q_CC_GNU >= 700) && !defined(Q_CC_INTEL)
+#define Q_FALLTHROUGH() __attribute__((fallthrough))
+#else
+#define Q_FALLTHROUGH() (void)0
+#endif
+#endif
+
+namespace Phantom
+{
+ namespace
+ {
+ constexpr qint16 DefaultFrameWidth = 6;
+ constexpr qint16 SplitterMaxLength = 25; // Length of splitter handle (not thickness)
+ constexpr qint16 MenuMinimumWidth = 20; // Smallest width that menu items can have
+ constexpr qint16 MenuBar_FrameWidth = 6;
+ constexpr qint16 SpinBox_ButtonWidth = 15;
+
+ // These two are currently not based on font, but could be
+ constexpr qint16 LineEdit_ContentsHPad = 5;
+ constexpr qint16 ComboBox_NonEditable_ContentsHPad = 7;
+ constexpr qint16 HeaderSortIndicator_HOffset = 1;
+ constexpr qint16 HeaderSortIndicator_VOffset = 2;
+ constexpr qint16 TabBar_InctiveVShift = 0;
+
+ constexpr qreal TabBarTab_Rounding = 1.0;
+ constexpr qreal SpinBox_Rounding = 1.0;
+ constexpr qreal LineEdit_Rounding = 1.0;
+ constexpr qreal FrameFocusRect_Rounding = 1.0;
+ constexpr qreal PushButton_Rounding = 1.0;
+ constexpr qreal ToolButton_Rounding = 1.0;
+ constexpr qreal ProgressBar_Rounding = 1.0;
+ constexpr qreal GroupBox_Rounding = 1.0;
+ constexpr qreal SliderGroove_Rounding = 1.0;
+ constexpr qreal SliderHandle_Rounding = 1.0;
+
+ constexpr qreal CheckMark_WidthOfHeightScale = 0.8;
+ constexpr qreal PushButton_HorizontalPaddingFontHeightRatio = 1.0;
+ constexpr qreal TabBar_HPaddingFontRatio = 1.25;
+ constexpr qreal TabBar_VPaddingFontRatio = 1.0 / 1.25;
+ constexpr qreal GroupBox_LabelBottomMarginFontRatio = 1.0 / 4.0;
+ constexpr qreal ComboBox_ArrowMarginRatio = 1.0 / 3.25;
+
+ constexpr qreal MenuBar_HorizontalPaddingFontRatio = 1.0 / 2.0;
+ constexpr qreal MenuBar_VerticalPaddingFontRatio = 1.0 / 3.0;
+
+ constexpr qreal MenuItem_LeftMarginFontRatio = 1.0 / 2.0;
+ constexpr qreal MenuItem_RightMarginForTextFontRatio = 1.0 / 1.5;
+ constexpr qreal MenuItem_RightMarginForArrowFontRatio = 1.0 / 4.0;
+ constexpr qreal MenuItem_VerticalMarginsFontRatio = 1.0 / 5.0;
+ // Number that's multiplied with a font's height to get the space between a
+ // menu item's checkbox (or other sign) and its text (or icon).
+ constexpr qreal MenuItem_CheckRightSpaceFontRatio = 1.0 / 4.0;
+ constexpr qreal MenuItem_TextMnemonicSpaceFontRatio = 1.5;
+ constexpr qreal MenuItem_SubMenuArrowSpaceFontRatio = 1.0 / 1.5;
+ constexpr qreal MenuItem_SubMenuArrowWidthFontRatio = 1.0 / 2.75;
+ constexpr qreal MenuItem_SeparatorHeightFontRatio = 1.0 / 1.5;
+ constexpr qreal MenuItem_CheckMarkVerticalInsetFontRatio = 1.0 / 5.0;
+ constexpr qreal MenuItem_IconRightSpaceFontRatio = 1.0 / 3.0;
+
+ constexpr bool BranchesOnEdge = false;
+ constexpr bool OverhangShadows = false;
+ constexpr bool IndicatorShadows = false;
+ constexpr bool MenuExtraBottomMargin = true;
+ constexpr bool MenuBarLeftMargin = false;
+ constexpr bool MenuBarDrawBorder = false;
+ constexpr bool AllowToolBarAutoRaise = true;
+ // Note that this only applies to the disclosure etc. decorators in tree views.
+ constexpr bool ShowItemViewDecorationSelected = false;
+ constexpr bool UseQMenuForComboBoxPopup = true;
+ constexpr bool ItemView_UseFontHeightForDecorationSize = true;
+
+ // Whether or not the non-raised tabs in a tab bar have shininess/highlights to
+ // them. Setting this to false adds an extra visual hint for distinguishing
+ // between the current and non-current tabs, but makes the non-current tabs
+ // appear less clickable. Other ways to increase the visual differences could
+ // be to increase the color contrast for the background fill color, or increase
+ // the vertical offset. However, increasing the vertical offset comes with some
+ // layout challenges, and increasing the color contrast further may visually
+ // imply an incorrect layout structure. Not sure what's best.
+ //
+ // This doesn't disable creating the color/brush resource, even though it's
+ // currently a compile-time-only option, because it may be changed to be part
+ // of some dynamic config system for Phantom in the future, or have a
+ // per-widget style hint associated with it.
+ const bool TabBar_InactiveTabsHaveSpecular = false;
+
+ struct Grad
+ {
+ Grad(const QColor& from, const QColor& to)
+ {
+ rgbA = Rgb::ofQColor(from);
+ rgbB = Rgb::ofQColor(to);
+ lA = rgbA.toHsl().l;
+ lB = rgbB.toHsl().l;
+ }
+ QColor sample(qreal alpha) const
+ {
+ Hsl hsl = Rgb::lerp(rgbA, rgbB, alpha).toHsl();
+ hsl.l = Phantom::lerp(lA, lB, alpha);
+ return hsl.toQColor();
+ }
+ Rgb rgbA, rgbB;
+ qreal lA, lB;
+ };
+
+ namespace DeriveColors
+ {
+ Q_NEVER_INLINE QColor adjustLightness(const QColor& qcolor, qreal ld)
+ {
+ Hsl hsl = Hsl::ofQColor(qcolor);
+ const qreal gamma = 3.0;
+ hsl.l = std::pow(Phantom::saturate(std::pow(hsl.l, 1.0 / gamma) + ld * 0.8), gamma);
+ return hsl.toQColor();
+ }
+ bool hack_isLightPalette(const QPalette& pal)
+ {
+ Hsl hsl0 = Hsl::ofQColor(pal.color(QPalette::WindowText));
+ Hsl hsl1 = Hsl::ofQColor(pal.color(QPalette::Window));
+ return hsl0.l < hsl1.l;
+ }
+ QColor buttonColor(const QPalette& pal)
+ {
+ // temp hack
+ if (pal.color(QPalette::Button) == pal.color(QPalette::Window))
+ return adjustLightness(pal.color(QPalette::Button), 0.01);
+ return pal.color(QPalette::Button);
+ }
+ QColor highlightedOutlineOf(const QPalette& pal)
+ {
+ return adjustLightness(pal.color(QPalette::Highlight), -0.08);
+ }
+ QColor dividerColor(const QColor& underlying)
+ {
+ return adjustLightness(underlying, -0.05);
+ }
+ QColor lightDividerColor(const QColor& underlying)
+ {
+ return adjustLightness(underlying, 0.02);
+ }
+ QColor outlineOf(const QPalette& pal)
+ {
+ return adjustLightness(pal.color(QPalette::Window), -0.1);
+ }
+ QColor gutterColorOf(const QPalette& pal)
+ {
+ return adjustLightness(pal.color(QPalette::Window), -0.05);
+ }
+ QColor darkGutterColorOf(const QPalette& pal)
+ {
+ return adjustLightness(pal.color(QPalette::Window), -0.08);
+ }
+ QColor lightShadeOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, 0.08);
+ }
+ QColor darkShadeOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, -0.08);
+ }
+ QColor overhangShadowOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, -0.05);
+ }
+ QColor sliderGutterShadowOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, -0.01);
+ }
+ QColor specularOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, 0.01);
+ }
+ QColor lightSpecularOf(const QColor& underlying)
+ {
+ return adjustLightness(underlying, 0.05);
+ }
+ QColor pressedOf(const QColor& color)
+ {
+ return adjustLightness(color, -0.05);
+ }
+ QColor darkPressedOf(const QColor& color)
+ {
+ return adjustLightness(color, -0.08);
+ }
+ QColor lightOnOf(const QColor& color)
+ {
+ return adjustLightness(color, -0.04);
+ }
+ QColor onOf(const QColor& color)
+ {
+ return adjustLightness(color, -0.08);
+ }
+ QColor indicatorColorOf(const QPalette& palette, QPalette::ColorGroup group = QPalette::Current)
+ {
+ if (hack_isLightPalette(palette)) {
+ qreal adjust = (palette.currentColorGroup() == QPalette::Disabled) ? 0.09 : 0.32;
+ return adjustLightness(palette.color(group, QPalette::WindowText), adjust);
+ }
+ return adjustLightness(palette.color(group, QPalette::WindowText), -0.05);
+ }
+ QColor inactiveTabFillColorOf(const QColor& underlying)
+ {
+ // used to be -0.01
+ return adjustLightness(underlying, -0.025);
+ }
+ QColor progressBarOutlineColorOf(const QPalette& pal)
+ {
+ // Pretty wasteful
+ Hsl hsl0 = Hsl::ofQColor(pal.color(QPalette::Window));
+ Hsl hsl1 = Hsl::ofQColor(pal.color(QPalette::Highlight));
+ hsl1.l = Phantom::saturate(qMin(hsl0.l - 0.1, hsl1.l - 0.2));
+ return hsl1.toQColor();
+ }
+ QColor itemViewMultiSelectionCurrentBorderOf(const QPalette& pal)
+ {
+ return adjustLightness(pal.color(QPalette::Highlight), -0.15);
+ }
+ QColor itemViewHeaderOnLineColorOf(const QPalette& pal)
+ {
+ return hack_isLightPalette(pal)
+ ? highlightedOutlineOf(pal)
+ : Grad(pal.color(QPalette::WindowText), pal.color(QPalette::Window)).sample(0.5);
+ }
+ } // namespace DeriveColors
+
+ namespace SwatchColors
+ {
+ enum SwatchColor
+ {
+ S_none = 0,
+ S_window,
+ S_button,
+ S_base,
+ S_text,
+ S_windowText,
+ S_highlight,
+ S_highlightedText,
+ S_scrollbarGutter,
+ S_scrollbarSlider,
+ S_window_outline,
+ S_window_specular,
+ S_window_divider,
+ S_window_lighter,
+ S_window_darker,
+ S_frame_outline,
+ S_button_specular,
+ S_button_pressed,
+ S_button_on,
+ S_button_pressed_specular,
+ S_sliderHandle,
+ S_sliderHandle_pressed,
+ S_sliderHandle_specular,
+ S_sliderHandle_pressed_specular,
+ S_base_shadow,
+ S_base_divider,
+ S_windowText_disabled,
+ S_highlight_outline,
+ S_highlight_specular,
+ S_progressBar_outline,
+ S_inactiveTabYesFrame,
+ S_inactiveTabNoFrame,
+ S_inactiveTabYesFrame_specular,
+ S_inactiveTabNoFrame_specular,
+ S_indicator_current,
+ S_indicator_disabled,
+ S_itemView_multiSelection_currentBorder,
+ S_itemView_headerOnLine,
+ S_scrollbarGutter_disabled,
+
+ // Aliases
+ S_progressBar = S_highlight,
+ S_progressBar_specular = S_highlight_specular,
+ S_tabFrame = S_window,
+ S_tabFrame_specular = S_window_specular,
+ };
+ }
+
+ using Swatchy = SwatchColors::SwatchColor;
+
+ enum
+ {
+ Num_SwatchColors = SwatchColors::S_scrollbarGutter_disabled + 1,
+ Num_ShadowSteps = 3,
+ };
+
+ struct PhSwatch : public QSharedData
+ {
+ // The pens store the brushes within them, so storing the brushes here as
+ // well is redundant. However, QPen::brush() does not return its brush by
+ // reference, so we'd end up doing a bunch of inc/dec work every time we use
+ // one. Also, it saves us the indirection of chasing two pointers (Swatch ->
+ // QPen -> QBrush) every time we want to get a QColor.
+ QBrush brushes[Num_SwatchColors];
+ QPen pens[Num_SwatchColors];
+ QColor scrollbarShadowColors[Num_ShadowSteps];
+
+ // Note: the casts to int in the assert macros are to suppress a false
+ // positive warning for tautological comparison in the clang linter.
+ inline const QColor& color(Swatchy swatchValue) const
+ {
+ Q_ASSERT(swatchValue >= 0 && static_cast<int>(swatchValue) < Num_SwatchColors);
+ return brushes[swatchValue].color();
+ }
+ inline const QBrush& brush(Swatchy swatchValue) const
+ {
+ Q_ASSERT(swatchValue >= 0 && static_cast<int>(swatchValue) < Num_SwatchColors);
+ return brushes[swatchValue];
+ }
+ inline const QPen& pen(Swatchy swatchValue) const
+ {
+ Q_ASSERT(swatchValue >= 0 && static_cast<int>(swatchValue) < Num_SwatchColors);
+ return pens[swatchValue];
+ }
+
+ void loadFromQPalette(const QPalette& pal);
+ };
+
+ using PhSwatchPtr = QExplicitlySharedDataPointer<PhSwatch>;
+ using PhCacheEntry = QPair<uint, PhSwatchPtr>;
+ enum : int
+ {
+ Num_ColorCacheEntries = 10,
+ };
+ using PhSwatchCache = QVarLengthArray<PhCacheEntry, Num_ColorCacheEntries>;
+ Q_NEVER_INLINE void PhSwatch::loadFromQPalette(const QPalette& pal)
+ {
+ using namespace SwatchColors;
+ namespace Dc = DeriveColors;
+ bool isLight = Dc::hack_isLightPalette(pal);
+ QColor colors[Num_SwatchColors];
+ colors[S_none] = QColor();
+
+ colors[S_window] = pal.color(QPalette::Window);
+ colors[S_button] = pal.color(QPalette::Button);
+ if (colors[S_button] == colors[S_window])
+ colors[S_button] = Dc::adjustLightness(colors[S_button], 0.01);
+ colors[S_base] = pal.color(QPalette::Base);
+ colors[S_text] = pal.color(QPalette::Text);
+ colors[S_windowText] = pal.color(QPalette::WindowText);
+ colors[S_highlight] = pal.color(QPalette::Highlight);
+ colors[S_highlightedText] = pal.color(QPalette::HighlightedText);
+ colors[S_scrollbarGutter] = isLight ? Dc::gutterColorOf(pal) : Dc::darkGutterColorOf(pal);
+ colors[S_scrollbarSlider] = isLight ? colors[S_button] : Dc::adjustLightness(colors[S_window], 0.2);
+
+ colors[S_window_outline] =
+ isLight ? Dc::adjustLightness(colors[S_window], -0.1) : Dc::adjustLightness(colors[S_window], 0.03);
+ colors[S_window_specular] = Dc::specularOf(colors[S_window]);
+ colors[S_window_divider] =
+ isLight ? Dc::dividerColor(colors[S_window]) : Dc::lightDividerColor(colors[S_window]);
+ colors[S_window_lighter] = Dc::lightShadeOf(colors[S_window]);
+ colors[S_window_darker] = Dc::darkShadeOf(colors[S_window]);
+ colors[S_frame_outline] = isLight ? colors[S_window_outline] : Dc::adjustLightness(colors[S_window], 0.08);
+ colors[S_button_specular] =
+ isLight ? Dc::specularOf(colors[S_button]) : Dc::lightSpecularOf(colors[S_button]);
+ colors[S_button_pressed] = isLight ? Dc::pressedOf(colors[S_button]) : Dc::darkPressedOf(colors[S_button]);
+ colors[S_button_on] = isLight ? Dc::lightOnOf(colors[S_button]) : Dc::onOf(colors[S_button]);
+ colors[S_button_pressed_specular] =
+ isLight ? Dc::specularOf(colors[S_button_pressed]) : Dc::lightSpecularOf(colors[S_button_pressed]);
+
+ colors[S_sliderHandle] = isLight ? colors[S_button] : Dc::adjustLightness(colors[S_button], -0.03);
+ colors[S_sliderHandle_specular] =
+ isLight ? Dc::specularOf(colors[S_sliderHandle]) : Dc::lightSpecularOf(colors[S_sliderHandle]);
+ colors[S_sliderHandle_pressed] =
+ isLight ? colors[S_button_pressed] : Dc::adjustLightness(colors[S_button_pressed], 0.03);
+ colors[S_sliderHandle_pressed_specular] = isLight ? Dc::specularOf(colors[S_sliderHandle_pressed])
+ : Dc::lightSpecularOf(colors[S_sliderHandle_pressed]);
+
+ colors[S_base_shadow] = Dc::overhangShadowOf(colors[S_base]);
+ colors[S_base_divider] = colors[S_window_divider];
+ colors[S_windowText_disabled] = pal.color(QPalette::Disabled, QPalette::WindowText);
+ colors[S_highlight_outline] = isLight ? Dc::adjustLightness(colors[S_highlight], -0.02)
+ : Dc::adjustLightness(colors[S_highlight], 0.05);
+ colors[S_highlight_specular] = Dc::specularOf(colors[S_highlight]);
+ colors[S_progressBar_outline] = Dc::progressBarOutlineColorOf(pal);
+ colors[S_inactiveTabYesFrame] = Dc::inactiveTabFillColorOf(colors[S_tabFrame]);
+ colors[S_inactiveTabNoFrame] = Dc::inactiveTabFillColorOf(colors[S_window]);
+ colors[S_inactiveTabYesFrame_specular] = Dc::specularOf(colors[S_inactiveTabYesFrame]);
+ colors[S_inactiveTabNoFrame_specular] = Dc::specularOf(colors[S_inactiveTabNoFrame]);
+ colors[S_indicator_current] = Dc::indicatorColorOf(pal, QPalette::Current);
+ colors[S_indicator_disabled] = Dc::indicatorColorOf(pal, QPalette::Disabled);
+ colors[S_itemView_multiSelection_currentBorder] = Dc::itemViewMultiSelectionCurrentBorderOf(pal);
+ colors[S_itemView_headerOnLine] = Dc::itemViewHeaderOnLineColorOf(pal);
+ colors[S_scrollbarGutter_disabled] = colors[S_window];
+
+ brushes[S_none] = Qt::NoBrush;
+ for (int i = S_none + 1; i < Num_SwatchColors; ++i) {
+ // todo try to reuse
+ brushes[i] = colors[i];
+ }
+ pens[S_none] = Qt::NoPen;
+ // QPen::setColor constructs a QBrush behind the scenes, so better to just
+ // re-use the ones we already made.
+ for (int i = S_none + 1; i < Num_SwatchColors; ++i) {
+ pens[i].setBrush(brushes[i]);
+ // Width is already 1, don't need to set it. Caps and joins already fine at
+ // their defaults, too.
+ }
+
+ Grad gutterGrad(Dc::sliderGutterShadowOf(colors[S_scrollbarGutter]), colors[S_scrollbarGutter]);
+ for (int i = 0; i < Num_ShadowSteps; ++i) {
+ scrollbarShadowColors[i] = gutterGrad.sample(i / static_cast<qreal>(Num_ShadowSteps));
+ }
+ }
+
+ // This is the "hash" (not really a hash) function we'll use on the happy fast
+ // path when looking up a PhSwatch for a given QPalette. It's fragile, because
+ // it uses QPalette::cacheKey(), so it may not match even when the contents
+ // (currentColorGroup + the RGB colors) of the QPalette are actually a match.
+ // But it's cheaper to calculate, so we'll store a single one of these "hashes"
+ // for the head (most recently used) cached PhSwatch, and check to see if it
+ // matches. This is the most common case, so we can usually save some work by
+ // doing this. (The second most common case is probably having a different
+ // ColorGroup but the rest of the contents are the same, but we don't have a
+ // special path for that.)
+ inline quint64 fastfragile_hash_qpalette(const QPalette& p)
+ {
+ union
+ {
+ qint64 i;
+ quint64 u;
+ } x;
+ x.i = p.cacheKey();
+ // QPalette::ColorGroup has range 0..5 (inclusive), so it only uses 3 bits.
+ // The high 32 bits in QPalette::cacheKey() are a global incrementing serial
+ // number for the QPalette creation. We don't store (2^29-1) things in our
+ // cache, and I doubt that many will ever be created in a real application
+ // while also retaining some of them across such a broad time range, so it's
+ // really unlikely that repurposing these top 3 bits to also include the
+ // QPalette::currentColorGroup() (which the cacheKey doesn't include for some
+ // reason...) will generate a collision.
+ //
+ // This may not be true in the future if the way the QPalette::cacheKey() is
+ // generated changes. If that happens, change to use the definition of
+ // `fastfragile_hash_qpalette` below, which is less likely to collide with an
+ // arbitrarily numbered key but also does more work.
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ x.u = x.u ^ (static_cast<quint64>(p.currentColorGroup()) << (64 - 3));
+ return x.u;
+#else
+ // Use this definition here if the contents/layout of QPalette::cacheKey()
+ // (as in, the C++ code in qpalette.cpp) are changed. We'll also put a Qt6
+ // guard for it, so that it will default to a more safe definition on the
+ // next guaranteed big breaking change for Qt. A warning will hopefully get
+ // someone to double-check it at some point in the future.
+#warning "Verify contents and layout of QPalette::cacheKey() have not changed"
+ QtPrivate::QHashCombine c;
+ uint h = qHash(p.currentColorGroup());
+ h = c(h, (uint)(x.u & 0xFFFFFFFFu));
+ h = c(h, (uint)((x.u >> 32) & 0xFFFFFFFFu));
+ return h;
+#endif
+ }
+
+ // This hash function is for when we want an actual accurate hash of a
+ // QPalette. QPalette's cacheKey() isn't very reliable -- it seems to change to
+ // a new random number whenever it's modified, with the exception of the
+ // currentColorGroup being changed. This kind of sucks for us, because it means
+ // two QPalette's can have the same contents but hash to different values. And
+ // this actually happens a lot! We'll do the hashing ourselves. Also, we're not
+ // interested in all of the colors, only some of them, and we ignore
+ // pens/brushes.
+ uint accurate_hash_qpalette(const QPalette& p)
+ {
+ // Probably shouldn't use this, could replace with our own guy. It's not a
+ // great hasher anyway.
+ QtPrivate::QHashCombine c;
+ uint h = qHash(p.currentColorGroup());
+ QPalette::ColorRole const roles[] = {QPalette::Window,
+ QPalette::Button,
+ QPalette::Base,
+ QPalette::Text,
+ QPalette::WindowText,
+ QPalette::Highlight,
+ QPalette::HighlightedText};
+ for (auto role : roles) {
+ h = c(h, p.color(role).rgb());
+ }
+ return h;
+ }
+
+ Q_NEVER_INLINE PhSwatchPtr
+ deep_getCachedSwatchOfQPalette(PhSwatchCache* cache,
+ int cacheCount, // Just saving a call to cache->count()
+ const QPalette& qpalette)
+ {
+ // Calculate our hash key from the QPalette's current ColorGroup and the
+ // actual RGBA values that we use. We have to mix the ColorGroup in
+ // ourselves, because QPalette does not account for it in the cache key.
+ uint key = accurate_hash_qpalette(qpalette);
+ int n = cacheCount;
+ int idx = -1;
+ for (int i = 0; i < n; ++i) {
+ const auto& x = cache->at(i);
+ if (x.first == key) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx == -1) {
+ PhSwatchPtr ptr;
+ if (n < Num_ColorCacheEntries) {
+ ptr = new PhSwatch;
+ } else {
+ // Remove the oldest guy from the cache. Remember that because we may
+ // re-enter QStyle functions multiple times when drawing or calculating
+ // something, we may have to load several swaitches derived from
+ // different QPalettes on different stack frames at the same time. But as
+ // an extra cost-savings measure, we'll check and see if something else
+ // has a reference to the removed guy. If there aren't any references to
+ // it, then we'll re-use it directly instead of allocating a new one. (We
+ // will only ever run into the case where we can't re-use it directly if
+ // some other stack frame has a reference to it.) This is nice because
+ // then the QPens and QBrushes don't all also have to reallocate their d
+ // ptr stuff.
+ ptr = cache->last().second;
+ cache->removeLast();
+ ptr.detach();
+ }
+ ptr->loadFromQPalette(qpalette);
+ cache->prepend(PhCacheEntry(key, ptr));
+ return ptr;
+ } else {
+ if (idx == 0) {
+ return cache->at(idx).second;
+ }
+ PhCacheEntry e = cache->at(idx);
+ // Using std::move from algorithm could be more efficient here, but I don't
+ // want to depend on algorithm or write this myself. Small N with a movable
+ // type means it doesn't really matter in this case.
+ cache->remove(idx);
+ cache->prepend(e);
+ return e.second;
+ }
+ }
+
+ Q_NEVER_INLINE PhSwatchPtr
+ getCachedSwatchOfQPalette(PhSwatchCache* cache,
+ quint64* headSwatchFastKey, // Optimistic fast-path quick hash key
+ const QPalette& qpalette)
+ {
+ quint64 ck = fastfragile_hash_qpalette(qpalette);
+ int cacheCount = cache->count();
+ // This hint is counter-productive if we're being called in a way that
+ // interleaves different QPalettes. But misses to this optimistic path were
+ // rare in my tests. (Probably not going to amount to any significant
+ // difference, anyway.)
+ if (Q_LIKELY(cacheCount > 0 && *headSwatchFastKey == ck)) {
+ return cache->at(0).second;
+ }
+ *headSwatchFastKey = ck;
+ return deep_getCachedSwatchOfQPalette(cache, cacheCount, qpalette);
+ }
+
+ } // namespace
+} // namespace Phantom
+
+class BaseStylePrivate
+{
+public:
+ BaseStylePrivate();
+
+ // A fast'n'easy hash of QPalette::cacheKey()+QPalette::currentColorGroup()
+ // of only the head element of swatchCache list. The most common thing that
+ // happens when deriving a PhSwatch from a QPalette is that we just end up
+ // re-using the last one that we used. For that case, we can potentially save
+ // calling `accurate_hash_qpalette()` and instead use the value returned by
+ // QPalette::cacheKey() (and QPalette::currentColorGroup()) and compare it to
+ // the last one that we used. If it matches, then we know we can just use the
+ // head of the cache list without having to do any further checks, which
+ // saves a few hundred (!) nanoseconds.
+ //
+ // However, the `QPalette::cacheKey()` value is fragile and may change even
+ // if none of the colors in the QPalette have changed. In other words, all of
+ // the colors in a QPalette may match another QPalette (or a derived
+ // PhSwatch) even if the `QPalette::cacheKey()` value is different.
+ //
+ // So if `QPalette::cacheKey()+currentColorGroup()` doesn't match, then we'll
+ // use our more accurate `accurate_hash_qpalette()` to get a more accurate
+ // comparison key, and then search through the cache list to find a matching
+ // cached PhSwatch. (The more accurate cache key is what we store alongside
+ // each PhSwatch element, as the `.first` in each QPair. The
+ // QPalette::cacheKey() that we associate with the PhSwatch in the head
+ // position, `headSwatchFastKey`, is only stored for our single head element,
+ // as a special fast case.) If we find it, we'll move it to the head of the
+ // cache list. If not, we'll make a new one, and put it at the head. Either
+ // way, the `headSwatchFastKey` will be updated to the
+ // `fastfragile_qpalette_hash()` of the QPalette that we needed to derive a
+ // PhSwatch from, so that if we get called with the same QPalette again next
+ // time (which is probably going to be the case), it'll match and we can take
+ // the fast path.
+ quint64 headSwatchFastKey;
+
+ Phantom::PhSwatchCache swatchCache;
+ QPen checkBox_pen_scratch;
+};
+
+namespace Phantom
+{
+ namespace
+ {
+
+ // Minimal QPainter save/restore just for pen, brush, and AA render hint. If
+ // you touch more than that, this won't help you. But if you're only touching
+ // those things, this will save you some typing from manually storing/saving
+ // those properties each time.
+ struct PSave final
+ {
+ Q_DISABLE_COPY(PSave)
+
+ explicit PSave(QPainter* painter_)
+ {
+ Q_ASSERT(painter_);
+ painter = painter_;
+ pen = painter_->pen();
+ brush = painter_->brush();
+ hintAA = painter_->testRenderHint(QPainter::Antialiasing);
+ }
+ Q_NEVER_INLINE void restore()
+ {
+ QPainter* p = painter;
+ if (!p)
+ return;
+ bool hintAA_ = hintAA;
+ // QPainter will check both pen and brush for equality when setting, so we
+ // should set it unconditionally here.
+ p->setPen(pen);
+ p->setBrush(brush);
+ // But it won't check the render hint to guard against doing extra work.
+ // We'll do that ourselves. (Though at least for the raster engine, this
+ // doesn't cause very much work to occur. But it still chases a few
+ // pointers.)
+ if (p->testRenderHint(QPainter::Antialiasing) != hintAA_) {
+ p->setRenderHint(QPainter::Antialiasing, hintAA_);
+ }
+ painter = nullptr;
+ pen = QPen();
+ brush = QBrush();
+ hintAA = false;
+ }
+ ~PSave()
+ {
+ restore();
+ }
+
+ private:
+ QPainter* painter;
+ QPen pen;
+ QBrush brush;
+ bool hintAA;
+ };
+
+ const qreal Pi = M_PI;
+
+ qreal dpiScaled(qreal value)
+ {
+#ifdef Q_OS_MAC
+ // On mac the DPI is always 72 so we should not scale it
+ return value;
+#else
+ const qreal scale = qt_defaultDpiX() / 96.0;
+ return value * scale;
+#endif
+ }
+
+ struct MenuItemMetrics
+ {
+ int fontHeight;
+ int frameThickness;
+ int leftMargin;
+ int rightMarginForText;
+ int rightMarginForArrow;
+ int topMargin;
+ int bottomMargin;
+ int checkWidth;
+ int checkRightSpace;
+ int iconRightSpace;
+ int mnemonicSpace;
+ int arrowSpace;
+ int arrowWidth;
+ int separatorHeight;
+ int totalHeight;
+
+ static MenuItemMetrics ofFontHeight(int fontHeight);
+
+ private:
+ MenuItemMetrics()
+ {
+ }
+ };
+
+ MenuItemMetrics MenuItemMetrics::ofFontHeight(int fontHeight)
+ {
+ MenuItemMetrics m;
+ m.fontHeight = fontHeight;
+ m.frameThickness = dpiScaled(1.0);
+ m.leftMargin = static_cast<int>(fontHeight * MenuItem_LeftMarginFontRatio);
+ m.rightMarginForText = static_cast<int>(fontHeight * MenuItem_RightMarginForTextFontRatio);
+ m.rightMarginForArrow = static_cast<int>(fontHeight * MenuItem_RightMarginForArrowFontRatio);
+ m.topMargin = static_cast<int>(fontHeight * MenuItem_VerticalMarginsFontRatio);
+ m.bottomMargin = static_cast<int>(fontHeight * MenuItem_VerticalMarginsFontRatio);
+ int checkVMargin = static_cast<int>(fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio);
+ int checkHeight = fontHeight - checkVMargin * 2;
+ if (checkHeight < 0)
+ checkHeight = 0;
+ m.checkWidth = static_cast<int>(checkHeight * CheckMark_WidthOfHeightScale);
+ m.checkRightSpace = static_cast<int>(fontHeight * MenuItem_CheckRightSpaceFontRatio);
+ m.iconRightSpace = static_cast<int>(fontHeight * MenuItem_IconRightSpaceFontRatio);
+ m.mnemonicSpace = static_cast<int>(fontHeight * MenuItem_TextMnemonicSpaceFontRatio);
+ m.arrowSpace = static_cast<int>(fontHeight * MenuItem_SubMenuArrowSpaceFontRatio);
+ m.arrowWidth = static_cast<int>(fontHeight * MenuItem_SubMenuArrowWidthFontRatio);
+ m.separatorHeight = static_cast<int>(fontHeight * MenuItem_SeparatorHeightFontRatio);
+ // Odd numbers only
+ m.separatorHeight = (m.separatorHeight / 2) * 2 + 1;
+ m.totalHeight = fontHeight + m.frameThickness * 2 + m.topMargin + m.bottomMargin;
+ return m;
+ }
+
+ QRect menuItemContentRect(const MenuItemMetrics& metrics, QRect itemRect, bool hasArrow)
+ {
+ QRect r = itemRect;
+ int ft = metrics.frameThickness;
+ int rm = hasArrow ? metrics.rightMarginForArrow : metrics.rightMarginForText;
+ r.adjust(ft + metrics.leftMargin, ft + metrics.topMargin, -(ft + rm), -(ft + metrics.bottomMargin));
+ return r.isValid() ? r : QRect();
+ }
+ QRect
+ menuItemCheckRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow)
+ {
+ QRect r = menuItemContentRect(metrics, itemRect, hasArrow);
+ int checkVMargin = static_cast<int>(metrics.fontHeight * MenuItem_CheckMarkVerticalInsetFontRatio);
+ if (checkVMargin < 0)
+ checkVMargin = 0;
+ r.setSize(QSize(metrics.checkWidth, metrics.fontHeight));
+ r.adjust(0, checkVMargin, 0, -checkVMargin);
+ return QStyle::visualRect(direction, itemRect, r) & itemRect;
+ }
+ QRect
+ menuItemIconRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect, bool hasArrow)
+ {
+ QRect r = menuItemContentRect(metrics, itemRect, hasArrow);
+ r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace);
+ r.setSize(QSize(metrics.fontHeight, metrics.fontHeight));
+ return QStyle::visualRect(direction, itemRect, r) & itemRect;
+ }
+ QRect menuItemTextRect(const MenuItemMetrics& metrics,
+ Qt::LayoutDirection direction,
+ QRect itemRect,
+ bool hasArrow,
+ bool hasIcon,
+ int tabWidth)
+ {
+ QRect r = menuItemContentRect(metrics, itemRect, hasArrow);
+ r.setX(r.x() + metrics.checkWidth + metrics.checkRightSpace);
+ if (hasIcon) {
+ r.setX(r.x() + metrics.fontHeight + metrics.iconRightSpace);
+ }
+ r.setWidth(r.width() - tabWidth);
+ r.setHeight(metrics.fontHeight);
+ r &= itemRect;
+ return QStyle::visualRect(direction, itemRect, r);
+ }
+ QRect menuItemMnemonicRect(const MenuItemMetrics& metrics,
+ Qt::LayoutDirection direction,
+ QRect itemRect,
+ bool hasArrow,
+ int tabWidth)
+ {
+ QRect r = menuItemContentRect(metrics, itemRect, hasArrow);
+ int x = r.x() + r.width() - tabWidth;
+ if (hasArrow)
+ x -= metrics.arrowSpace + metrics.arrowWidth;
+ r.setX(x);
+ r.setHeight(metrics.fontHeight);
+ r &= itemRect;
+ return QStyle::visualRect(direction, itemRect, r);
+ }
+ QRect menuItemArrowRect(const MenuItemMetrics& metrics, Qt::LayoutDirection direction, QRect itemRect)
+ {
+ QRect r = menuItemContentRect(metrics, itemRect, true);
+ int x = r.x() + r.width() - metrics.arrowWidth;
+ r.setX(x);
+ r &= itemRect;
+ return QStyle::visualRect(direction, itemRect, r);
+ }
+
+ Q_NEVER_INLINE
+ void progressBarFillRects(const QStyleOptionProgressBar* bar,
+ // The rect that represents the filled/completed region
+ QRect& outFilled,
+ // The rect that represents the incomplete region
+ QRect& outNonFilled,
+ // Whether or not the progress bar is indeterminate
+ bool& outIsIndeterminate)
+ {
+ QRect ra = bar->rect;
+ QRect rb = ra;
+ bool isHorizontal = bar->orientation != Qt::Vertical;
+ bool isInverted = bar->invertedAppearance;
+ bool isIndeterminate = bar->minimum == 0 && bar->maximum == 0;
+ bool isForward = !isHorizontal || bar->direction != Qt::RightToLeft;
+ if (isInverted)
+ isForward = !isForward;
+ int maxLen = isHorizontal ? ra.width() : ra.height();
+ const auto availSteps = qMax(Q_INT64_C(1), qint64(bar->maximum) - bar->minimum);
+ const auto progress = qMax(bar->progress, bar->minimum); // workaround for bug in QProgressBar
+ const auto progressSteps = qint64(progress) - bar->minimum;
+ const auto progressBarWidth = progressSteps * maxLen / availSteps;
+ int barLen = isIndeterminate ? maxLen : progressBarWidth;
+ if (isHorizontal) {
+ if (isForward) {
+ ra.setWidth(barLen);
+ rb.setX(barLen);
+ } else {
+ ra.setX(ra.x() + ra.width() - barLen);
+ rb.setWidth(rb.width() - barLen);
+ }
+ } else {
+ if (isForward) {
+ ra.setY(ra.y() + ra.height() - barLen);
+ rb.setHeight(rb.height() - barLen);
+ } else {
+ ra.setHeight(barLen);
+ rb.setY(barLen);
+ }
+ }
+ outFilled = ra;
+ outNonFilled = rb;
+ outIsIndeterminate = isIndeterminate;
+ }
+
+ int calcBigLineSize(int radius)
+ {
+ int bigLineSize = radius / 6;
+ if (bigLineSize < 4)
+ bigLineSize = 4;
+ if (bigLineSize > radius / 2)
+ bigLineSize = radius / 2;
+ return bigLineSize;
+ }
+ Q_NEVER_INLINE QPointF calcRadialPos(const QStyleOptionSlider* dial, qreal offset)
+ {
+ const int width = dial->rect.width();
+ const int height = dial->rect.height();
+ const int r = qMin(width, height) / 2;
+ const int currentSliderPosition =
+ dial->upsideDown ? dial->sliderPosition : (dial->maximum - dial->sliderPosition);
+ qreal a = 0;
+ if (dial->maximum == dial->minimum)
+ a = Pi / 2;
+ else if (dial->dialWrapping)
+ a = Pi * 3 / 2 - (currentSliderPosition - dial->minimum) * 2 * Pi / (dial->maximum - dial->minimum);
+ else
+ a = (Pi * 8 - (currentSliderPosition - dial->minimum) * 10 * Pi / (dial->maximum - dial->minimum)) / 6;
+ qreal xc = width / 2.0;
+ qreal yc = height / 2.0;
+ qreal len = r - calcBigLineSize(r) - 3;
+ qreal back = offset * len;
+ QPointF pos(QPointF(xc + back * qCos(a), yc - back * qSin(a)));
+ return pos;
+ }
+ Q_NEVER_INLINE QPolygonF calcLines(const QStyleOptionSlider* dial)
+ {
+ QPolygonF poly;
+ qreal width = dial->rect.width();
+ qreal height = dial->rect.height();
+ qreal r = qMin(width, height) / 2.0;
+ int bigLineSize = calcBigLineSize(r);
+
+ qreal xc = width / 2.0 + 0.5;
+ qreal yc = height / 2.0 + 0.5;
+ const int ns = dial->tickInterval;
+ if (!ns) // Invalid values may be set by Qt Designer.
+ return poly;
+ int notches = (dial->maximum + ns - 1 - dial->minimum) / ns;
+ if (notches <= 0)
+ return poly;
+ if (dial->maximum < dial->minimum || dial->maximum - dial->minimum > 1000) {
+ int maximum = dial->minimum + 1000;
+ notches = (maximum + ns - 1 - dial->minimum) / ns;
+ }
+ poly.resize(2 + 2 * notches);
+ int smallLineSize = bigLineSize / 2;
+ for (int i = 0; i <= notches; ++i) {
+ qreal angle =
+ dial->dialWrapping ? Pi * 3 / 2 - i * 2 * Pi / notches : (Pi * 8 - i * 10 * Pi / notches) / 6;
+ qreal s = qSin(angle);
+ qreal c = qCos(angle);
+ if (i == 0 || (((ns * i) % (dial->pageStep ? dial->pageStep : 1)) == 0)) {
+ poly[2 * i] = QPointF(xc + (r - bigLineSize) * c, yc - (r - bigLineSize) * s);
+ poly[2 * i + 1] = QPointF(xc + r * c, yc - r * s);
+ } else {
+ poly[2 * i] = QPointF(xc + (r - 1 - smallLineSize) * c, yc - (r - 1 - smallLineSize) * s);
+ poly[2 * i + 1] = QPointF(xc + (r - 1) * c, yc - (r - 1) * s);
+ }
+ }
+ return poly;
+ }
+ // This will draw a nice and shiny QDial for us. We don't want
+ // all the shinyness in QWindowsStyle, hence we place it here
+ Q_NEVER_INLINE void drawDial(const QStyleOptionSlider* option, QPainter* painter)
+ {
+ namespace Dc = Phantom::DeriveColors;
+ const QPalette& pal = option->palette;
+ QColor buttonColor = Dc::buttonColor(option->palette);
+ const int width = option->rect.width();
+ const int height = option->rect.height();
+ const bool enabled = option->state & QStyle::State_Enabled;
+ qreal r = qMin(width, height) / 2.0;
+ r -= r / 50.0;
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing);
+ // Draw notches
+ if (option->subControls & QStyle::SC_DialTickmarks) {
+ painter->setPen(pal.color(QPalette::Disabled, QPalette::Text));
+ painter->drawLines(calcLines(option));
+ }
+ const qreal d_ = r / 6;
+ const qreal dx = option->rect.x() + d_ + (width - 2 * r) / 2 + 1;
+ const qreal dy = option->rect.y() + d_ + (height - 2 * r) / 2 + 1;
+ QRectF br = QRectF(dx + 0.5, dy + 0.5, int(r * 2 - 2 * d_ - 2), int(r * 2 - 2 * d_ - 2));
+ if (enabled) {
+ painter->setBrush(buttonColor);
+ } else {
+ painter->setBrush(Qt::NoBrush);
+ }
+ painter->setPen(Dc::outlineOf(option->palette));
+ painter->drawEllipse(br);
+ painter->setBrush(Qt::NoBrush);
+ painter->setPen(Dc::specularOf(buttonColor));
+ painter->drawEllipse(br.adjusted(1, 1, -1, -1));
+ if (option->state & QStyle::State_HasFocus) {
+ QColor highlight = pal.highlight().color();
+ highlight.setHsv(highlight.hue(), qMin(160, highlight.saturation()), qMax(230, highlight.value()));
+ highlight.setAlpha(127);
+ painter->setPen(QPen(highlight, 2.0));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawEllipse(br.adjusted(-1, -1, 1, 1));
+ }
+ QPointF dp = calcRadialPos(option, 0.70);
+ const qreal ds = r / 7.0;
+ QRectF dialRect(dp.x() - ds, dp.y() - ds, 2 * ds, 2 * ds);
+ painter->setBrush(option->palette.color(QPalette::Window));
+ painter->setPen(Dc::outlineOf(option->palette));
+ painter->drawEllipse(dialRect.adjusted(-1, -1, 1, 1));
+ painter->restore();
+ }
+
+ int fontMetricsWidth(const QFontMetrics& fontMetrics, const QString& text)
+ {
+#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
+ return fontMetrics.width(text, text.size(), Qt::TextBypassShaping);
+#else
+ return fontMetrics.horizontalAdvance(text);
+#endif
+ }
+
+ // This always draws the arrow with the correct aspect ratio, even if the
+ // provided bounding rect is non-square. The base edge of the triangle is
+ // snapped to a whole pixel to avoid anti-aliasing making it look soft.
+ //
+ // Expected time (release): 5usecs for regular-sized arrows
+ Q_NEVER_INLINE void drawArrow(QPainter* p, QRect rect, Qt::ArrowType arrowDirection, const QBrush& brush)
+ {
+ const qreal ArrowBaseRatio = 0.70;
+ qreal irx, iry, irw, irh;
+ QRectF(rect).getRect(&irx, &iry, &irw, &irh);
+ if (irw < 1.0 || irh < 1.0)
+ return;
+ qreal dw, dh;
+ if (arrowDirection == Qt::LeftArrow || arrowDirection == Qt::RightArrow) {
+ dw = ArrowBaseRatio;
+ dh = 1.0;
+ } else {
+ dw = 1.0;
+ dh = ArrowBaseRatio;
+ }
+ QSizeF sz = QSizeF(dw, dh).scaled(irw, irh, Qt::KeepAspectRatio);
+ qreal aw = sz.width();
+ qreal ah = sz.height();
+ qreal ax, ay;
+ ax = irx + (irw - aw) / 2;
+ ay = iry + (irh - ah) / 2;
+ QRectF arrowRect(ax, ay, aw, ah);
+ QPointF points[3];
+ switch (arrowDirection) {
+ case Qt::DownArrow:
+ arrowRect.setTop(std::round(arrowRect.top()));
+ points[0] = arrowRect.topLeft();
+ points[1] = arrowRect.topRight();
+ points[2] = QPointF(arrowRect.center().x(), arrowRect.bottom());
+ break;
+ case Qt::RightArrow: {
+ arrowRect.setLeft(std::round(arrowRect.left()));
+ points[0] = arrowRect.topLeft();
+ points[1] = arrowRect.bottomLeft();
+ points[2] = QPointF(arrowRect.right(), arrowRect.center().y());
+ break;
+ }
+ case Qt::LeftArrow:
+ arrowRect.setRight(std::round(arrowRect.right()));
+ points[0] = arrowRect.topRight();
+ points[1] = arrowRect.bottomRight();
+ points[2] = QPointF(arrowRect.left(), arrowRect.center().y());
+ break;
+ case Qt::UpArrow:
+ default:
+ arrowRect.setBottom(std::round(arrowRect.bottom()));
+ points[0] = arrowRect.bottomLeft();
+ points[1] = arrowRect.bottomRight();
+ points[2] = QPointF(arrowRect.center().x(), arrowRect.top());
+ break;
+ }
+ auto oldPen = p->pen();
+ auto oldBrush = p->brush();
+ bool oldAA = p->testRenderHint(QPainter::Antialiasing);
+ p->setPen(Qt::NoPen);
+ p->setBrush(brush);
+ if (!oldAA) {
+ p->setRenderHint(QPainter::Antialiasing);
+ }
+ p->drawConvexPolygon(points, 3);
+ p->setPen(oldPen);
+ p->setBrush(oldBrush);
+ if (!oldAA) {
+ p->setRenderHint(QPainter::Antialiasing, false);
+ }
+ }
+
+ // Pass allowEnabled as false to always draw the arrow with the disabled color,
+ // even if the underlying palette's current color group is not disabled. Useful
+ // for parts of widgets which may want to be drawn as disabled even if the
+ // actual widget is not set as disabled, such as scrollbar step buttons when
+ // the scrollbar has no movable range.
+ Q_NEVER_INLINE void drawArrow(QPainter* painter,
+ QRect rect,
+ Qt::ArrowType type,
+ const PhSwatch& swatch,
+ bool allowEnabled = true,
+ qreal lightnessAdjustment = 0.0)
+ {
+ if (rect.isEmpty())
+ return;
+ using namespace SwatchColors;
+ auto brush = swatch.brush(allowEnabled ? S_indicator_current : S_indicator_disabled);
+ brush.setColor(DeriveColors::adjustLightness(brush.color(), lightnessAdjustment));
+ Phantom::drawArrow(painter, rect, type, brush);
+ }
+
+ // This draws exactly within the rect provided. If you provide a square rect,
+ // it will appear too wide -- you probably want to shrink the width of your
+ // square first by multiplying it with CheckMark_WidthOfHeightScale.
+ Q_NEVER_INLINE void
+ drawCheck(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color)
+ {
+ using namespace Phantom::SwatchColors;
+ qreal rx, ry, rw, rh;
+ QRectF(r).getRect(&rx, &ry, &rw, &rh);
+ qreal penWidth = 0.25 * qMin(rw, rh);
+ qreal dimx = rw - penWidth;
+ qreal dimy = rh - penWidth;
+ if (dimx < 0.5 || dimy < 0.5)
+ return;
+ qreal x = (rw - dimx) / 2 + rx;
+ qreal y = (rh - dimy) / 2 + ry;
+ QPointF points[3];
+ points[0] = QPointF(0.0, 0.55);
+ points[1] = QPointF(0.4, 1.0);
+ points[2] = QPointF(1.0, 0);
+ for (int i = 0; i < 3; ++i) {
+ QPointF pnt = points[i];
+ pnt.setX(pnt.x() * dimx + x);
+ pnt.setY(pnt.y() * dimy + y);
+ points[i] = pnt;
+ }
+ scratchPen.setBrush(swatch.brush(color));
+ scratchPen.setCapStyle(Qt::RoundCap);
+ scratchPen.setJoinStyle(Qt::RoundJoin);
+ scratchPen.setWidthF(penWidth);
+ Phantom::PSave save(painter);
+ if (!painter->testRenderHint(QPainter::Antialiasing))
+ painter->setRenderHint(QPainter::Antialiasing);
+ painter->setPen(scratchPen);
+ painter->setBrush(Qt::NoBrush);
+ painter->drawPolyline(points, 3);
+ }
+
+ Q_NEVER_INLINE void
+ drawHyphen(QPainter* painter, QPen& scratchPen, const QRectF& r, const PhSwatch& swatch, Swatchy color)
+ {
+ using namespace Phantom::SwatchColors;
+ qreal rx, ry, rw, rh;
+ QRectF(r).getRect(&rx, &ry, &rw, &rh);
+ qreal penWidth = 0.25 * qMin(rw, rh);
+ qreal dimx = rw - penWidth;
+ qreal dimy = rh - penWidth;
+ if (dimx < 0.5 || dimy < 0.5)
+ return;
+ qreal x = (rw - dimx) / 2 + rx;
+ qreal y = (rh - dimy) / 2 + ry;
+ QPointF p0(0.0 * dimx + x, 0.5 * dimy + y);
+ QPointF p1(1.0 * dimx + x, 0.5 * dimy + y);
+ scratchPen.setBrush(swatch.brush(color));
+ scratchPen.setCapStyle(Qt::RoundCap);
+ scratchPen.setWidthF(penWidth);
+ Phantom::PSave save(painter);
+ if (!painter->testRenderHint(QPainter::Antialiasing))
+ painter->setRenderHint(QPainter::Antialiasing);
+ painter->setPen(scratchPen);
+ painter->setBrush(Qt::NoBrush);
+ painter->drawLine(p0, p1);
+ }
+
+ Q_NEVER_INLINE void
+ drawMdiButton(QPainter* painter, const QStyleOptionTitleBar* option, QRect tmp, bool hover, bool sunken)
+ {
+ QColor dark;
+ dark.setHsv(option->palette.button().color().hue(),
+ qMin<int>(255, (option->palette.button().color().saturation())),
+ qMin<int>(255, option->palette.button().color().value() * 0.7));
+ QColor highlight = option->palette.highlight().color();
+ bool active = (option->titleBarState & QStyle::State_Active);
+ QColor titleBarHighlight(255, 255, 255, 60);
+ if (sunken)
+ painter->fillRect(tmp.adjusted(1, 1, -1, -1), option->palette.highlight().color().darker(120));
+ else if (hover)
+ painter->fillRect(tmp.adjusted(1, 1, -1, -1), QColor(255, 255, 255, 20));
+ if (sunken)
+ titleBarHighlight = highlight.darker(130);
+ QColor mdiButtonBorderColor(active ? option->palette.highlight().color().darker(180) : dark.darker(110));
+ painter->setPen(QPen(mdiButtonBorderColor));
+ const QLine lines[4] = {QLine(tmp.left() + 2, tmp.top(), tmp.right() - 2, tmp.top()),
+ QLine(tmp.left() + 2, tmp.bottom(), tmp.right() - 2, tmp.bottom()),
+ QLine(tmp.left(), tmp.top() + 2, tmp.left(), tmp.bottom() - 2),
+ QLine(tmp.right(), tmp.top() + 2, tmp.right(), tmp.bottom() - 2)};
+ painter->drawLines(lines, 4);
+ const QPoint points[4] = {QPoint(tmp.left() + 1, tmp.top() + 1),
+ QPoint(tmp.right() - 1, tmp.top() + 1),
+ QPoint(tmp.left() + 1, tmp.bottom() - 1),
+ QPoint(tmp.right() - 1, tmp.bottom() - 1)};
+ painter->drawPoints(points, 4);
+ painter->setPen(titleBarHighlight);
+ painter->drawLine(tmp.left() + 2, tmp.top() + 1, tmp.right() - 2, tmp.top() + 1);
+ painter->drawLine(tmp.left() + 1, tmp.top() + 2, tmp.left() + 1, tmp.bottom() - 2);
+ }
+
+ Q_NEVER_INLINE void fillRectOutline(QPainter* p, QRect rect, QMargins margins, const QColor& brush)
+ {
+ int x, y, w, h;
+ rect.getRect(&x, &y, &w, &h);
+ int ml = margins.left();
+ int mt = margins.top();
+ int mr = margins.right();
+ int mb = margins.bottom();
+ QRect r0(x, y, w, mt);
+ QRect r1(x, y + mt, ml, h - (mt + mb));
+ QRect r2((x + w) - mr, y + mt, mr, h - (mt + mb));
+ QRect r3(x, (y + h) - mb, w, mb);
+ p->fillRect(r0 & rect, brush);
+ p->fillRect(r1 & rect, brush);
+ p->fillRect(r2 & rect, brush);
+ p->fillRect(r3 & rect, brush);
+ }
+ void fillRectOutline(QPainter* p, QRect rect, int thickness, const QColor& color)
+ {
+ fillRectOutline(p, rect, QMargins(thickness, thickness, thickness, thickness), color);
+ }
+ Q_NEVER_INLINE void
+ fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, QMargins margins, const QColor& color)
+ {
+ int x, y, w, h;
+ rect.getRect(&x, &y, &w, &h);
+ if (edges & Qt::LeftEdge) {
+ int ml = margins.left();
+ QRect r0(x, y, ml, h);
+ p->fillRect(r0 & rect, color);
+ }
+ if (edges & Qt::TopEdge) {
+ int mt = margins.top();
+ QRect r1(x, y, w, mt);
+ p->fillRect(r1 & rect, color);
+ }
+ if (edges & Qt::RightEdge) {
+ int mr = margins.right();
+ QRect r2((x + w) - mr, y, mr, h);
+ p->fillRect(r2 & rect, color);
+ }
+ if (edges & Qt::BottomEdge) {
+ int mb = margins.bottom();
+ QRect r3(x, (y + h) - mb, w, mb);
+ p->fillRect(r3 & rect, color);
+ }
+ }
+ void fillRectEdges(QPainter* p, QRect rect, Qt::Edges edges, int thickness, const QColor& color)
+ {
+ fillRectEdges(p, rect, edges, QMargins(thickness, thickness, thickness, thickness), color);
+ }
+ inline QRect expandRect(QRect rect, Qt::Edges edges, int delta)
+ {
+ int l = edges & Qt::LeftEdge ? -delta : 0;
+ int t = edges & Qt::TopEdge ? -delta : 0;
+ int r = edges & Qt::RightEdge ? delta : 0;
+ int b = edges & Qt::BottomEdge ? delta : 0;
+ return rect.adjusted(l, t, r, b);
+ }
+ inline Qt::Edge oppositeEdge(Qt::Edge edge)
+ {
+ switch (edge) {
+ case Qt::LeftEdge:
+ return Qt::RightEdge;
+ case Qt::TopEdge:
+ return Qt::BottomEdge;
+ case Qt::RightEdge:
+ return Qt::LeftEdge;
+ case Qt::BottomEdge:
+ return Qt::TopEdge;
+ }
+ return Qt::TopEdge;
+ }
+ inline QRect rectTranslatedTowardEdge(QRect rect, Qt::Edge edge, int delta)
+ {
+ switch (edge) {
+ case Qt::LeftEdge:
+ return rect.translated(-delta, 0);
+ case Qt::TopEdge:
+ return rect.translated(0, -delta);
+ case Qt::RightEdge:
+ return rect.translated(delta, 0);
+ case Qt::BottomEdge:
+ return rect.translated(0, delta);
+ }
+ return rect;
+ }
+ Q_NEVER_INLINE QRect rectFromInnerEdgeWithThickness(QRect rect, Qt::Edge edge, int thickness)
+ {
+ int x, y, w, h;
+ rect.getRect(&x, &y, &w, &h);
+ QRect r;
+ switch (edge) {
+ case Qt::LeftEdge:
+ r = QRect(x, y, thickness, h);
+ break;
+ case Qt::TopEdge:
+ r = QRect(x, y, w, thickness);
+ break;
+ case Qt::RightEdge:
+ r = QRect((x + w) - thickness, y, thickness, h);
+ break;
+ case Qt::BottomEdge:
+ r = QRect(x, (y + h) - thickness, w, thickness);
+ break;
+ }
+ return r & rect;
+ }
+ Q_NEVER_INLINE void
+ paintSolidRoundRect(QPainter* p, QRect rect, qreal radius, const PhSwatch& swatch, Swatchy fill)
+ {
+ if (!fill)
+ return;
+ bool aa = p->testRenderHint(QPainter::Antialiasing);
+ if (radius > 0.5) {
+ if (!aa)
+ p->setRenderHint(QPainter::Antialiasing);
+ p->setPen(swatch.pen(SwatchColors::S_none));
+ p->setBrush(swatch.brush(fill));
+ p->drawRoundedRect(rect, radius, radius);
+ } else {
+ if (aa)
+ p->setRenderHint(QPainter::Antialiasing, false);
+ p->fillRect(rect, swatch.color(fill));
+ }
+ }
+ Q_NEVER_INLINE void paintBorderedRoundRect(QPainter* p,
+ QRect rect,
+ qreal radius,
+ const PhSwatch& swatch,
+ Swatchy stroke,
+ Swatchy fill)
+ {
+ if (rect.width() < 1 || rect.height() < 1)
+ return;
+ if (!stroke && !fill)
+ return;
+ bool aa = p->testRenderHint(QPainter::Antialiasing);
+ if (radius > 0.5) {
+ if (!aa)
+ p->setRenderHint(QPainter::Antialiasing);
+ p->setPen(swatch.pen(stroke));
+ p->setBrush(swatch.brush(fill));
+ QRectF rf(rect.x() + 0.5, rect.y() + 0.5, rect.width() - 1.0, rect.height() - 1.0);
+ p->drawRoundedRect(rf, radius, radius);
+ } else {
+ if (aa)
+ p->setRenderHint(QPainter::Antialiasing, false);
+ if (stroke) {
+ fillRectOutline(p, rect, 1, swatch.color(stroke));
+ }
+ if (fill) {
+ p->fillRect(rect.adjusted(1, 1, -1, -1), swatch.color(fill));
+ }
+ }
+ }
+ } // namespace
+} // namespace Phantom
+
+BaseStylePrivate::BaseStylePrivate()
+ : headSwatchFastKey(0)
+{
+}
+
+BaseStyle::BaseStyle()
+ : d(new BaseStylePrivate)
+{
+ setObjectName(QLatin1String("Phantom"));
+}
+
+BaseStyle::~BaseStyle()
+{
+ delete d;
+}
+
+// Draw text in a rectangle. The current pen set on the painter is used, unless
+// an explicit textRole is set, in which case the palette will be used. The
+// enabled bool indicates whether the text is enabled or not, and can influence
+// how the text is drawn outside of just color. Wrapping and alignment flags
+// can be passed in `alignment`.
+void BaseStyle::drawItemText(QPainter* painter,
+ const QRect& rect,
+ int alignment,
+ const QPalette& pal,
+ bool enabled,
+ const QString& text,
+ QPalette::ColorRole textRole) const
+{
+ Q_UNUSED(enabled);
+ if (text.isEmpty())
+ return;
+ if (textRole == QPalette::NoRole) {
+ painter->drawText(rect, alignment, text);
+ return;
+ }
+ QPen savedPen = painter->pen();
+ const QBrush& newBrush = pal.brush(textRole);
+ bool changed = false;
+ if (savedPen.brush() != newBrush) {
+ changed = true;
+ painter->setPen(QPen(newBrush, savedPen.widthF()));
+ }
+ painter->drawText(rect, alignment, text);
+ if (changed) {
+ painter->setPen(savedPen);
+ }
+}
+
+void BaseStyle::drawPrimitive(PrimitiveElement elem,
+ const QStyleOption* option,
+ QPainter* painter,
+ const QWidget* widget) const
+{
+ Q_ASSERT(option);
+ if (!option)
+ return;
+#ifdef BUILD_WITH_EASY_PROFILER
+ EASY_BLOCK("drawPrimitive");
+ const char* elemCString = QMetaEnum::fromType<QStyle::PrimitiveElement>().valueToKey(elem);
+ EASY_TEXT("Element", elemCString);
+#endif
+ using Swatchy = Phantom::Swatchy;
+ using namespace Phantom::SwatchColors;
+ namespace Ph = Phantom;
+ auto ph_swatchPtr = getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette);
+ const Ph::PhSwatch& swatch = *ph_swatchPtr.data();
+ const int state = option->state;
+ // Cast to int here to suppress warnings about cases listed which are not in
+ // the original enum. This is for custom primitive elements.
+ switch (static_cast<int>(elem)) {
+ case PE_Frame: {
+ if (widget && widget->inherits("QComboBoxPrivateContainer")) {
+ QStyleOption copy = *option;
+ copy.state |= State_Raised;
+ proxy()->drawPrimitive(PE_PanelMenu, &copy, painter, widget);
+ break;
+ }
+ Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline));
+ break;
+ }
+ case PE_FrameMenu: {
+ break;
+ }
+ case PE_FrameDockWidget: {
+ painter->save();
+ QColor softshadow = option->palette.background().color().darker(120);
+ QRect r = option->rect;
+ painter->setPen(softshadow);
+ painter->drawRect(r.adjusted(0, 0, -1, -1));
+ painter->setPen(QPen(option->palette.light(), 1));
+ painter->drawLine(QPoint(r.left() + 1, r.top() + 1), QPoint(r.left() + 1, r.bottom() - 1));
+ painter->setPen(QPen(option->palette.background().color().darker(120)));
+ painter->drawLine(QPoint(r.left() + 1, r.bottom() - 1), QPoint(r.right() - 2, r.bottom() - 1));
+ painter->drawLine(QPoint(r.right() - 1, r.top() + 1), QPoint(r.right() - 1, r.bottom() - 1));
+ painter->restore();
+ break;
+ }
+ case PE_FrameGroupBox: {
+ QRect frame = option->rect;
+ Ph::PSave save(painter);
+ bool isFlat = false;
+ if (auto groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option)) {
+ isFlat = groupBox->features & QStyleOptionFrame::Flat;
+ } else if (auto frameOpt = qstyleoption_cast<const QStyleOptionFrame*>(option)) {
+ isFlat = frameOpt->features & QStyleOptionFrame::Flat;
+ }
+ if (isFlat) {
+ Ph::fillRectEdges(painter, frame, Qt::TopEdge, 1, swatch.color(S_window_divider));
+ } else {
+ Ph::paintBorderedRoundRect(painter, frame, Ph::GroupBox_Rounding, swatch, S_frame_outline, S_none);
+ }
+ break;
+ }
+ case PE_IndicatorBranch: {
+ if (!(option->state & State_Children))
+ break;
+ Qt::ArrowType arrow;
+ if (option->state & State_Open) {
+ arrow = Qt::DownArrow;
+ } else if (option->direction != Qt::RightToLeft) {
+ arrow = Qt::RightArrow;
+ } else {
+ arrow = Qt::LeftArrow;
+ }
+ bool useSelectionColor = false;
+ if (option->state & State_Selected) {
+ if (auto ivopt = qstyleoption_cast<const QStyleOptionViewItem*>(option)) {
+ useSelectionColor = ivopt->showDecorationSelected;
+ }
+ }
+ Swatchy color = useSelectionColor ? S_highlightedText : S_indicator_current;
+ QRect r = option->rect;
+ if (Ph::BranchesOnEdge) {
+ // TODO RTL
+ r.moveLeft(0);
+ if (r.width() < r.height())
+ r.setWidth(r.height());
+ }
+ int adj = qMin(r.width(), r.height()) / 4;
+ r.adjust(adj, adj, -adj, -adj);
+ Ph::drawArrow(painter, r, arrow, swatch.brush(color));
+ break;
+ }
+ case PE_IndicatorMenuCheckMark: {
+ // For this PE, QCommonStyle treats State_On as drawing the check with the
+ // highlighted text color, and otherwise with the regular text color. I
+ // guess we should match that behavior, even though it's not consistent
+ // with other check box/mark drawing in QStyle (buttons and item view
+ // items.) QCommonStyle also doesn't care about tri-state or unchecked
+ // states -- it seems that if you call this, you want a check, and nothing
+ // else.
+ //
+ // We'll also catch State_Selected and treat it equivalently (the way you'd
+ // expect.) We'll use windowText instead of text, though -- probably
+ // doesn't matter.
+ Swatchy fgColor = S_windowText;
+ bool isSelected = option->state & (State_Selected | State_On);
+ bool isEnabled = option->state & State_Enabled;
+ if (isSelected) {
+ fgColor = S_highlightedText;
+ } else if (!isEnabled) {
+ fgColor = S_windowText_disabled;
+ }
+ qreal rx, ry, rw, rh;
+ QRectF(option->rect).getRect(&rx, &ry, &rw, &rh);
+ qreal dim = qMin(rw, rh);
+ const qreal insetScale = 0.8;
+ qreal dimx = dim * insetScale * Ph::CheckMark_WidthOfHeightScale;
+ qreal dimy = dim * insetScale;
+ QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy);
+ Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor);
+ break;
+ }
+ // Called for the content area on tree view rows that are selected
+ case PE_PanelItemViewItem: {
+ QCommonStyle::drawPrimitive(elem, option, painter, widget);
+ break;
+ }
+ // Called for left-of-item-content-area on tree view rows that are selected
+ case PE_PanelItemViewRow: {
+ QCommonStyle::drawPrimitive(elem, option, painter, widget);
+ break;
+ }
+ case PE_FrameTabBarBase: {
+ auto tbb = qstyleoption_cast<const QStyleOptionTabBarBase*>(option);
+ if (!tbb)
+ break;
+ Qt::Edge edge = Qt::TopEdge;
+ switch (tbb->shape) {
+ case QTabBar::RoundedNorth:
+ case QTabBar::TriangularNorth:
+ edge = Qt::TopEdge;
+ break;
+ case QTabBar::RoundedSouth:
+ case QTabBar::TriangularSouth:
+ edge = Qt::BottomEdge;
+ break;
+ case QTabBar::RoundedWest:
+ case QTabBar::TriangularWest:
+ edge = Qt::LeftEdge;
+ break;
+ case QTabBar::RoundedEast:
+ case QTabBar::TriangularEast:
+ edge = Qt::RightEdge;
+ break;
+ }
+ Ph::fillRectEdges(painter, option->rect, edge, 1, swatch.color(S_frame_outline));
+ // TODO need to check here if we're drawing with window or button color as
+ // the frame fill. Assuming window right now, but could be wrong.
+ Ph::fillRectEdges(painter, Ph::expandRect(option->rect, edge, -1), edge, 1, swatch.color(S_tabFrame_specular));
+ break;
+ }
+ case PE_PanelScrollAreaCorner: {
+ bool isLeftToRight = option->direction != Qt::RightToLeft;
+ Qt::Edges edges = Qt::TopEdge;
+ QRect bgRect = option->rect;
+ if (isLeftToRight) {
+ edges |= Qt::LeftEdge;
+ bgRect.setX(bgRect.x() + 1);
+ } else {
+ edges |= Qt::RightEdge;
+ bgRect.setWidth(bgRect.width() - 1);
+ }
+ painter->fillRect(bgRect, swatch.color(S_window));
+ Ph::fillRectEdges(painter, option->rect, edges, 1, swatch.color(S_window_outline));
+ break;
+ }
+ case PE_IndicatorArrowUp:
+ case PE_IndicatorArrowDown:
+ case PE_IndicatorArrowRight:
+ case PE_IndicatorArrowLeft: {
+ int rx, ry, rw, rh;
+ option->rect.getRect(&rx, &ry, &rw, &rh);
+ if (rw <= 1 || rh <= 1)
+ break;
+ Qt::ArrowType arrow = Qt::UpArrow;
+ switch (elem) {
+ case PE_IndicatorArrowUp:
+ arrow = Qt::UpArrow;
+ break;
+ case PE_IndicatorArrowDown:
+ arrow = Qt::DownArrow;
+ break;
+ case PE_IndicatorArrowRight:
+ arrow = Qt::RightArrow;
+ break;
+ case PE_IndicatorArrowLeft:
+ arrow = Qt::LeftArrow;
+ break;
+ default:
+ break;
+ }
+ // The caller may give us a huge rect and expect a normal-sized icon inside
+ // of it, so we don't want to fill the entire thing with an arrow,
+ // otherwise certain buttons will look weird, like the tab bar scroll
+ // buttons. Might want to break these out into editable parameters?
+ const int MaxArrowExt = Ph::dpiScaled(12);
+ const int MinMargin = qMin(rw, rh) / 4;
+ int aw, ah;
+ aw = qMin(MaxArrowExt, rw) - MinMargin;
+ ah = qMin(MaxArrowExt, rh) - MinMargin;
+ if (aw <= 2 || ah <= 2)
+ break;
+ // QCommonStyle's implementation of CC_ToolButton for non-instant popups
+ // gives us a pretty big rectangle to draw the arrow in -- shrink it. This
+ // is kind of a dirty temp hack thing until we do something smarter, like
+ // fully reimplement CC_ToolButton. Note that it passes us a regular
+ // QStyleOption and not a QStyleOptionToolButton in this case, so try to
+ // save some work before doing the inherits test.
+ if (arrow == Qt::DownArrow && !qstyleoption_cast<const QStyleOptionToolButton*>(option) && widget) {
+ auto tbutton = qobject_cast<const QToolButton*>(widget);
+ if (tbutton && tbutton->popupMode() != QToolButton::InstantPopup && tbutton->defaultAction()) {
+ int dim = static_cast<int>(qMin(rw, rh) * 0.25);
+ aw -= dim;
+ ah -= dim;
+ // We have another hack in PE_IndicatorButtonDropDown where we shift
+ // the edge left or right by 1px to avoid having two borders touching
+ // (we make it overlap instead.) So we'll need to compensate for that
+ // in the arrow's position to avoid it looking off-center.
+ rw += 1;
+ if (option->direction != Qt::RightToLeft) {
+ rx -= 1;
+ }
+ }
+ }
+ aw += (rw - aw) % 2;
+ ah += (rh - ah) % 2;
+ int ax = (rw - aw) / 2 + rx;
+ int ay = (rh - ah) / 2 + ry;
+ Ph::drawArrow(painter, QRect(ax, ay, aw, ah), arrow, swatch);
+ break;
+ }
+ case PE_IndicatorItemViewItemCheck: {
+ QStyleOptionButton button;
+ button.QStyleOption::operator=(*option);
+ button.state &= ~State_MouseOver;
+ proxy()->drawPrimitive(PE_IndicatorCheckBox, &button, painter, widget);
+ return;
+ }
+ case PE_IndicatorHeaderArrow: {
+ auto header = qstyleoption_cast<const QStyleOptionHeader*>(option);
+ if (!header)
+ return;
+ QRect r = header->rect;
+ QPoint offset = QPoint(Phantom::HeaderSortIndicator_HOffset, Phantom::HeaderSortIndicator_VOffset);
+ qreal lightness = Phantom::DeriveColors::hack_isLightPalette(widget->palette()) ? 0.03 : 0.0;
+ if (header->sortIndicator & QStyleOptionHeader::SortUp) {
+ Ph::drawArrow(painter, r.translated(offset), Qt::DownArrow, swatch, true, lightness);
+ } else if (header->sortIndicator & QStyleOptionHeader::SortDown) {
+ Ph::drawArrow(painter, r.translated(offset), Qt::UpArrow, swatch, true, lightness);
+ }
+ break;
+ }
+ case PE_IndicatorButtonDropDown: {
+ // Temp hack until we implement CC_ToolButton: avoid double-stacked border
+ // by clipping off one edge slightly.
+ QStyleOption opt0 = *option;
+ if (opt0.direction != Qt::RightToLeft) {
+ opt0.rect.adjust(-1, 0, 0, 0);
+ } else {
+ opt0.rect.adjust(0, 0, 1, 0);
+ }
+ proxy()->drawPrimitive(PE_PanelButtonTool, &opt0, painter, widget);
+ break;
+ }
+
+ case PE_IndicatorToolBarSeparator: {
+ QRect r = option->rect;
+ if (option->state & State_Horizontal) {
+ if (r.height() >= 10)
+ r.adjust(0, 3, 0, -3);
+ r.setWidth(r.width() / 2 + 1);
+ Ph::fillRectEdges(painter, r, Qt::RightEdge, 1, swatch.color(S_window_divider));
+ } else {
+ // TODO replace with new code
+ const int margin = 6;
+ const int offset = r.height() / 2;
+ painter->setPen(QPen(option->palette.background().color().darker(110)));
+ painter->drawLine(r.topLeft().x() + margin,
+ r.topLeft().y() + offset,
+ r.topRight().x() - margin,
+ r.topRight().y() + offset);
+ painter->setPen(QPen(option->palette.background().color().lighter(110)));
+ painter->drawLine(r.topLeft().x() + margin,
+ r.topLeft().y() + offset + 1,
+ r.topRight().x() - margin,
+ r.topRight().y() + offset + 1);
+ }
+ break;
+ }
+ case PE_PanelButtonTool: {
+ bool isDown = option->state & State_Sunken;
+ bool isOn = option->state & State_On;
+ bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange);
+ const qreal rounding = Ph::ToolButton_Rounding;
+ Swatchy outline = S_window_outline;
+ Swatchy fill = S_button;
+ Swatchy specular = S_button_specular;
+ if (isDown) {
+ fill = S_button_pressed;
+ specular = S_button_pressed_specular;
+ } else if (isOn) {
+ fill = S_button_on;
+ specular = S_none;
+ }
+ if (hasFocus) {
+ outline = S_highlight_outline;
+ }
+ QRect r = option->rect;
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill);
+ Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none);
+ break;
+ }
+ case PE_IndicatorDockWidgetResizeHandle: {
+ QStyleOption dockWidgetHandle = *option;
+ bool horizontal = option->state & State_Horizontal;
+ dockWidgetHandle.state =
+ !horizontal ? (dockWidgetHandle.state | State_Horizontal) : (dockWidgetHandle.state & ~State_Horizontal);
+ proxy()->drawControl(CE_Splitter, &dockWidgetHandle, painter, widget);
+ break;
+ }
+ case PE_FrameWindow: {
+ break;
+ }
+ case PE_FrameLineEdit: {
+ QRect r = option->rect;
+ bool hasFocus = option->state & State_HasFocus;
+ bool isEnabled = option->state & State_Enabled;
+ const qreal rounding = Ph::LineEdit_Rounding;
+ auto pen = hasFocus ? S_highlight_outline : S_window_outline;
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, r, rounding, swatch, pen, S_none);
+ save.restore();
+ if (Ph::OverhangShadows && !hasFocus && isEnabled) {
+ // Imperfect when rounded, may leave a gap on left and right. Going
+ // closer would eat into the outline, though.
+ Ph::fillRectEdges(painter,
+ r.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1),
+ Qt::TopEdge,
+ 1,
+ swatch.color(S_base_shadow));
+ }
+ break;
+ }
+ case PE_PanelLineEdit: {
+ auto panel = qstyleoption_cast<const QStyleOptionFrame*>(option);
+ if (!panel)
+ break;
+ Ph::PSave save(painter);
+ // We intentionally don't inset the fill rect, even if the frame will paint
+ // over the perimeter, because an inset with rounding enabled may cause
+ // some miscolored separated pixels between the fill and the border, since
+ // we're forced to paint them in two separate draw calls.
+ Ph::paintSolidRoundRect(painter, option->rect, Ph::LineEdit_Rounding, swatch, S_base);
+ save.restore();
+ if (panel->lineWidth > 0)
+ proxy()->drawPrimitive(PE_FrameLineEdit, option, painter, widget);
+ break;
+ }
+ case PE_IndicatorCheckBox: {
+ auto checkbox = qstyleoption_cast<const QStyleOptionButton*>(option);
+ if (!checkbox)
+ break;
+ QRect r = option->rect;
+ bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange;
+ bool isSelected = option->state & State_Selected;
+ bool isFlat = checkbox->features & QStyleOptionButton::Flat;
+ bool isEnabled = option->state & State_Enabled;
+ bool isPressed = state & State_Sunken;
+ Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline;
+ Swatchy bgFillColor = isPressed ? S_highlight : S_base;
+ Swatchy fgColor = isFlat ? S_windowText : S_text;
+ if (isPressed && !isFlat) {
+ fgColor = S_highlightedText;
+ }
+ // Bare checkmarks that are selected should draw with the highlighted text
+ // color.
+ if (isSelected && isFlat) {
+ fgColor = S_highlightedText;
+ }
+ if (!isFlat) {
+ QRect fillR = r;
+ Ph::fillRectOutline(painter, fillR, 1, swatch.color(outlineColor));
+ fillR.adjust(1, 1, -1, -1);
+ if (Ph::IndicatorShadows && !isPressed && isEnabled) {
+ Ph::fillRectEdges(painter, fillR, Qt::TopEdge, 1, swatch.color(S_base_shadow));
+ fillR.adjust(0, 1, 0, 0);
+ }
+ painter->fillRect(fillR, swatch.color(bgFillColor));
+ }
+ if (checkbox->state & State_NoChange) {
+ const qreal insetScale = 0.7;
+ qreal rx, ry, rw, rh;
+ QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh);
+ qreal dimx = rw * insetScale;
+ qreal dimy = rh * insetScale;
+ QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy);
+ Ph::drawHyphen(painter, d->checkBox_pen_scratch, r_, swatch, fgColor);
+ } else if (checkbox->state & State_On) {
+ const qreal insetScale = 0.8;
+ qreal rx, ry, rw, rh;
+ QRectF(r.adjusted(1, 1, -1, -1)).getRect(&rx, &ry, &rw, &rh);
+ // kinda wrong, assumes we're already square, but we probably are
+ qreal dimx = rw * insetScale * Ph::CheckMark_WidthOfHeightScale;
+ qreal dimy = rh * insetScale;
+ QRectF r_(rx + (rw - dimx) / 2, ry + (rh - dimy) / 2, dimx, dimy);
+ Ph::drawCheck(painter, d->checkBox_pen_scratch, r_, swatch, fgColor);
+ }
+ break;
+ }
+ case PE_IndicatorRadioButton: {
+ qreal rx, ry, rw, rh;
+ QRectF(option->rect).getRect(&rx, &ry, &rw, &rh);
+ bool isHighlighted = option->state & State_HasFocus && option->state & State_KeyboardFocusChange;
+ bool isSunken = state & State_Sunken;
+ bool isEnabled = state & State_Enabled;
+ Swatchy outlineColor = isHighlighted ? S_highlight_outline : S_window_outline;
+ Swatchy bgFillColor = isSunken ? S_highlight : S_base;
+ QPointF circleCenter(rx + rw / 2.0, ry + rh / 2.0);
+ const qreal lineThickness = 1.0;
+ qreal outlineRadius = (qMin(rw, rh) - lineThickness) / 2.0;
+ qreal fillRadius = outlineRadius - lineThickness / 2.0;
+ Ph::PSave save(painter);
+ painter->setRenderHint(QPainter::Antialiasing);
+ painter->setBrush(swatch.brush(bgFillColor));
+ painter->setPen(swatch.pen(outlineColor));
+ painter->drawEllipse(circleCenter, outlineRadius, outlineRadius);
+ if (Ph::IndicatorShadows && !isSunken && isEnabled) {
+ // Really slow, just a temp demo test
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(swatch.brush(S_base_shadow));
+ QPainterPath path0, path1;
+ path0.addEllipse(circleCenter, fillRadius, fillRadius);
+ path1.addEllipse(circleCenter + QPointF(0, 1.25), fillRadius, fillRadius);
+ QPainterPath path2 = path0 - path1;
+ painter->drawPath(path2);
+ }
+ if (state & State_On) {
+ Swatchy fgColor = isSunken ? S_highlightedText : S_windowText;
+ qreal checkmarkRadius = outlineRadius / 2.32;
+ painter->setPen(Qt::NoPen);
+ painter->setBrush(swatch.brush(fgColor));
+ painter->drawEllipse(circleCenter, checkmarkRadius, checkmarkRadius);
+ }
+ break;
+ }
+ case PE_IndicatorToolBarHandle: {
+ if (!option)
+ break;
+ QRect r = option->rect;
+ if (r.width() < 3 || r.height() < 3)
+ break;
+ int rows = 3;
+ int columns = 2;
+ if (option->state & State_Horizontal) {
+ } else {
+ qSwap(columns, rows);
+ }
+ int dotLen = Ph::dpiScaled(2);
+ QSize occupied(dotLen * (columns * 2 - 1), dotLen * (rows * 2 - 1));
+ QRect rr = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(occupied), r);
+ int x = rr.x();
+ int y = rr.y();
+ for (int row = 0; row < rows; ++row) {
+ for (int col = 0; col < columns; ++col) {
+ int x_ = x + col * 2 * dotLen;
+ int y_ = y + row * 2 * dotLen;
+ painter->fillRect(x_, y_, dotLen, dotLen, swatch.color(S_window_divider));
+ }
+ }
+ break;
+ }
+ case PE_FrameDefaultButton:
+ break;
+ case PE_FrameFocusRect: {
+ auto fropt = qstyleoption_cast<const QStyleOptionFocusRect*>(option);
+ if (!fropt)
+ break;
+ //### check for d->alt_down
+ if (!(fropt->state & State_KeyboardFocusChange))
+ return;
+ if (fropt->state & State_Item) {
+ if (auto itemView = qobject_cast<const QAbstractItemView*>(widget)) {
+ // TODO either our grid line hack is interfering, or Qt has a bug, but
+ // in RTL layout the grid borders can leave junk behind in the grid
+ // areas and the right edge of the focus rect may not get painted.
+ // (Sometimes it will, though.) To replicate, set to RTL mode, and move
+ // the current around in a table view without the selection being on
+ // the current.
+ if (option->state & QStyle::State_Selected) {
+ bool showCurrent = true;
+ bool hasTableGrid = false;
+ const auto selectionMode = itemView->selectionMode();
+ if (selectionMode == QAbstractItemView::SingleSelection) {
+ showCurrent = false;
+ } else {
+ // Table views will can have a "current" frame drawn even if the
+ // "current" is within the selected range. Other item views won't,
+ // which means the "current" frame will be invisible if it's on a
+ // selected item. This is a compromise between the broken drawing
+ // behavior of Qt item views of drawing "current" frames when they
+ // don't make sense (like a tree view where you can only select
+ // entire rows, but Qt will the frame rect around whatever column
+ // was last clicked on by the mouse, but using keyboard navigation
+ // has no effect) and not drawing them at all.
+ bool isTableView = false;
+ if (auto tableView = qobject_cast<const QTableView*>(itemView)) {
+ hasTableGrid = tableView->showGrid();
+ isTableView = true;
+ }
+ const auto selectionModel = itemView->selectionModel();
+ if (selectionModel) {
+ const auto selection = selectionModel->selection();
+ if (selection.count() == 1) {
+ const auto& range = selection.at(0);
+ if (isTableView) {
+ // For table views, we don't draw the "current" frame if
+ // there is exactly one cell selected and the "current" is
+ // that cell, or if there is exactly one row or one column
+ // selected with the behavior set to the corresponding
+ // selection, and the "current" is that one row or column.
+ const auto selectionBehavior = itemView->selectionBehavior();
+ if ((range.width() == 1 && range.height() == 1)
+ || (selectionBehavior == QAbstractItemView::SelectRows && range.height() == 1)
+ || (selectionBehavior == QAbstractItemView::SelectColumns
+ && range.width() == 1)) {
+ showCurrent = false;
+ }
+ } else {
+ // For any other type of item view, don't draw the "current"
+ // frame if there is a single contiguous selection, and the
+ // "current" is within that selection. If there's a
+ // discontiguous selection, that means the user is probably
+ // doing something more advanced, and we should just draw the
+ // focus frame, even if Qt might be doing it badly in some
+ // cases.
+ showCurrent = false;
+ }
+ }
+ }
+ }
+ if (showCurrent) {
+ // TODO handle dark-highlight-light-text
+ const QColor& borderColor = swatch.color(S_itemView_multiSelection_currentBorder);
+ const int thickness = hasTableGrid ? 2 : 1;
+ Ph::fillRectOutline(painter, option->rect, thickness, borderColor);
+ }
+ } else {
+ Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_highlight_outline));
+ }
+ break;
+ }
+ }
+ // It would be nice to also handle QTreeView's allColumnsShowFocus thing in
+ // the above code, in addition to the normal cases for focus rects in item
+ // views. Unfortunately, with allColumnsShowFocus set to true,
+ // QTreeView::drawRow() calls the style to paint with PE_FrameFocusRect for
+ // the row frame with the widget set to nullptr. This makes it basically
+ // impossible to figure out that we need to draw a special frame for it.
+ // So, if any application code is using that mode in a QTreeView, it won't
+ // get special item view frames. Too bad.
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(
+ painter, option->rect, Ph::FrameFocusRect_Rounding, swatch, S_highlight_outline, S_none);
+ break;
+ }
+ case PE_PanelButtonCommand:
+ case PE_PanelButtonBevel: {
+ bool isDefault = false;
+ bool isFlat = false;
+ bool isDown = option->state & State_Sunken;
+ bool isOn = option->state & State_On;
+ if (auto button = qstyleoption_cast<const QStyleOptionButton*>(option)) {
+ isDefault = (button->features & QStyleOptionButton::DefaultButton) && (button->state & State_Enabled);
+ isFlat = (button->features & QStyleOptionButton::Flat);
+ }
+ if (isFlat && !isDown && !isOn)
+ break;
+ bool isEnabled = option->state & State_Enabled;
+ Q_UNUSED(isEnabled);
+ bool hasFocus = (option->state & State_HasFocus && option->state & State_KeyboardFocusChange);
+ const qreal rounding = Ph::PushButton_Rounding;
+ Swatchy outline = S_window_outline;
+ Swatchy fill = S_button;
+ Swatchy specular = S_button_specular;
+ if (isDown) {
+ fill = S_button_pressed;
+ specular = S_button_pressed_specular;
+ } else if (isOn) {
+ // kinda repurposing this, hmm
+ fill = S_scrollbarGutter;
+ specular = S_button_pressed_specular;
+ }
+ if (hasFocus || isDefault) {
+ outline = S_highlight_outline;
+ }
+ QRect r = option->rect;
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, r, rounding, swatch, outline, fill);
+ Ph::paintBorderedRoundRect(painter, r.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none);
+ break;
+ }
+ case PE_FrameTabWidget: {
+ QRect bgRect = option->rect.adjusted(1, 1, -1, -1);
+ painter->fillRect(bgRect, swatch.color(S_tabFrame));
+ auto twf = qstyleoption_cast<const QStyleOptionTabWidgetFrame*>(option);
+ if (!twf)
+ break;
+ Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_frame_outline));
+ Ph::fillRectOutline(painter, bgRect, 1, swatch.color(S_tabFrame_specular));
+ break;
+ }
+ case PE_FrameStatusBarItem:
+ break;
+ case PE_IndicatorTabClose:
+ case Phantom_PE_IndicatorTabNew: {
+ Swatchy fg = S_windowText;
+ Swatchy bg = S_none;
+ if ((option->state & State_Enabled) && (option->state & State_MouseOver)) {
+ fg = S_highlightedText;
+ bg = option->state & State_Sunken ? S_highlight_outline : S_highlight;
+ }
+ // temp code
+ Ph::PSave save(painter);
+ if (bg) {
+ Ph::paintSolidRoundRect(painter, option->rect, Ph::PushButton_Rounding, swatch, bg);
+ }
+ QPen pen = swatch.pen(fg);
+ pen.setCapStyle(Qt::RoundCap);
+ pen.setWidthF(1.5);
+ painter->setBrush(Qt::NoBrush);
+ painter->setPen(pen);
+ painter->setRenderHint(QPainter::Antialiasing);
+ QRect r = option->rect;
+ // int adj = (int)((qreal)qMin(r.width(), r.height()) * (1.0 / 2.5));
+ int adj = Ph::dpiScaled(5.0);
+ r.adjust(adj, adj, -adj, -adj);
+ qreal x, y, w, h;
+ QRectF(r).getRect(&x, &y, &w, &h);
+ // painter->translate(-0.5, -0.5);
+ switch (static_cast<int>(elem)) {
+ case PE_IndicatorTabClose:
+ painter->drawLine(QPointF(x - 0.5, y - 0.5), QPointF(x + 0.5 + w, y + 0.5 + h));
+ painter->drawLine(QPointF(x - 0.5, y + h + 0.5), QPointF(x + 0.5 + w, y - 0.5));
+ break;
+ case Phantom_PE_IndicatorTabNew:
+ // kinda hacky here on extra len
+ painter->drawLine(QPointF(x + w / 2, y - 1.0), QPointF(x + w / 2, y + h + 1.0));
+ painter->drawLine(QPointF(x - 1.0, y + h / 2), QPointF(x + w + 1.0, y + h / 2));
+ break;
+ }
+ save.restore();
+ // painter->fillRect(option->rect, QColor(255, 0, 0, 30));
+ break;
+ }
+ case PE_PanelMenu: {
+ bool isBelowMenuBar = false;
+ // works but currently unused
+ // QPoint gp = widget->mapToGlobal(widget->rect().topLeft());
+ // gp.setY(gp.y() - 1);
+ // QWidget* bar = qApp->widgetAt(gp);
+ // if (bar && bar->inherits("QMenuBar")) {
+ // isBelowMenuBar = true;
+ // }
+ Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_divider));
+ QRect bgRect = option->rect.adjusted(1, isBelowMenuBar ? 0 : 1, -1, -1);
+ painter->fillRect(bgRect, swatch.color(S_window));
+ break;
+ }
+ case Phantom_PE_ScrollBarSliderVertical: {
+ bool isLeftToRight = option->direction != Qt::RightToLeft;
+ bool isSunken = option->state & State_Sunken;
+ Swatchy thumbFill, thumbSpecular;
+ if (isSunken) {
+ thumbFill = S_button_pressed;
+ thumbSpecular = S_button_pressed_specular;
+ } else {
+ thumbFill = S_scrollbarSlider;
+ thumbSpecular = S_button_specular;
+ }
+ Qt::Edges edges;
+ QRect edgeRect = option->rect;
+ QRect mainRect = option->rect;
+ edgeRect.adjust(0, -1, 0, 1);
+ if (isLeftToRight) {
+ edges = Qt::LeftEdge | Qt::TopEdge | Qt::BottomEdge;
+ mainRect.setX(mainRect.x() + 1);
+ } else {
+ edges = Qt::TopEdge | Qt::BottomEdge | Qt::RightEdge;
+ mainRect.setWidth(mainRect.width() - 1);
+ }
+ Ph::fillRectEdges(painter, edgeRect, edges, 1, swatch.color(S_window_outline));
+ painter->fillRect(mainRect, swatch.color(thumbFill));
+ Ph::fillRectOutline(painter, mainRect, 1, swatch.color(thumbSpecular));
+ break;
+ }
+ case Phantom_PE_WindowFrameColor: {
+ painter->fillRect(option->rect, swatch.color(S_window_outline));
+ break;
+ }
+ default:
+ QCommonStyle::drawPrimitive(elem, option, painter, widget);
+ break;
+ }
+}
+
+void BaseStyle::drawControl(ControlElement element,
+ const QStyleOption* option,
+ QPainter* painter,
+ const QWidget* widget) const
+{
+#ifdef BUILD_WITH_EASY_PROFILER
+ EASY_BLOCK("drawControl");
+ const char* elemCString = QMetaEnum::fromType<QStyle::ControlElement>().valueToKey(element);
+ EASY_TEXT("Element", elemCString);
+#endif
+ using Swatchy = Phantom::Swatchy;
+ using namespace Phantom::SwatchColors;
+ namespace Ph = Phantom;
+ auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette);
+ const Ph::PhSwatch& swatch = *ph_swatchPtr.data();
+
+ switch (element) {
+ case CE_CheckBox: {
+ QCommonStyle::drawControl(element, option, painter, widget);
+ // painter->fillRect(option->rect, QColor(255, 0, 0, 90));
+ break;
+ }
+ case CE_ComboBoxLabel: {
+ auto cb = qstyleoption_cast<const QStyleOptionComboBox*>(option);
+ if (!cb)
+ break;
+ QRect editRect = proxy()->subControlRect(CC_ComboBox, cb, SC_ComboBoxEditField, widget);
+ painter->save();
+ painter->setClipRect(editRect);
+ if (!cb->currentIcon.isNull()) {
+ QIcon::Mode mode = cb->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
+ QPixmap pixmap = cb->currentIcon.pixmap(cb->iconSize, mode);
+ QRect iconRect(editRect);
+ iconRect.setWidth(cb->iconSize.width() + 4);
+ iconRect = alignedRect(cb->direction, Qt::AlignLeft | Qt::AlignVCenter, iconRect.size(), editRect);
+ if (cb->editable)
+ painter->fillRect(iconRect, cb->palette.brush(QPalette::Base));
+ proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pixmap);
+
+ if (cb->direction == Qt::RightToLeft)
+ editRect.translate(-4 - cb->iconSize.width(), 0);
+ else
+ editRect.translate(cb->iconSize.width() + 4, 0);
+ }
+ if (!cb->currentText.isEmpty() && !cb->editable) {
+ proxy()->drawItemText(painter,
+ editRect.adjusted(1, 0, -1, 0),
+ visualAlignment(cb->direction, Qt::AlignLeft | Qt::AlignVCenter),
+ cb->palette,
+ cb->state & State_Enabled,
+ cb->currentText,
+ cb->editable ? QPalette::Text : QPalette::ButtonText);
+ }
+ painter->restore();
+ break;
+ }
+ case CE_Splitter: {
+ QRect r = option->rect;
+ // We don't have anything useful to draw if it's too thin
+ if (r.width() < 5 || r.height() < 5)
+ break;
+ int length = Ph::dpiScaled(Ph::SplitterMaxLength);
+ int thickness = Ph::dpiScaled(1);
+ QSize size;
+ if (option->state & State_Horizontal) {
+ if (r.height() < length)
+ length = r.height();
+ size = QSize(thickness, length);
+ } else {
+ if (r.width() < length)
+ length = r.width();
+ size = QSize(length, thickness);
+ }
+ QRect filledRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, size, r);
+ painter->fillRect(filledRect, swatch.color(S_button_specular));
+ Ph::fillRectOutline(painter, filledRect.adjusted(-1, 0, 1, 0), 1, swatch.color(S_window_divider));
+ break;
+ }
+ // TODO update this for phantom
+ case CE_RubberBand: {
+ if (!qstyleoption_cast<const QStyleOptionRubberBand*>(option))
+ break;
+ QColor highlight = option->palette.color(QPalette::Active, QPalette::Highlight);
+ painter->save();
+ QColor penColor = highlight.darker(120);
+ penColor.setAlpha(180);
+ painter->setPen(penColor);
+ QColor dimHighlight(qMin(highlight.red() / 2 + 110, 255),
+ qMin(highlight.green() / 2 + 110, 255),
+ qMin(highlight.blue() / 2 + 110, 255));
+ dimHighlight.setAlpha(widget && widget->isTopLevel() ? 255 : 80);
+ painter->setRenderHint(QPainter::Antialiasing, true);
+ painter->translate(0.5, 0.5);
+ painter->setBrush(dimHighlight);
+ painter->drawRoundedRect(option->rect.adjusted(0, 0, -1, -1), 1, 1);
+ QColor innerLine = Qt::white;
+ innerLine.setAlpha(40);
+ painter->setPen(innerLine);
+ painter->drawRoundedRect(option->rect.adjusted(1, 1, -2, -2), 1, 1);
+ painter->restore();
+ break;
+ }
+ case CE_SizeGrip: {
+ Qt::LayoutDirection dir = option->direction;
+ QRect rect = option->rect;
+ int rcx = rect.center().x();
+ int rcy = rect.center().y();
+ // draw grips
+ for (int i = -6; i < 12; i += 3) {
+ for (int j = -6; j < 12; j += 3) {
+ if ((dir == Qt::LeftToRight && i > -j) || (dir == Qt::RightToLeft && j > i)) {
+ painter->fillRect(rcx + i, rcy + j, 2, 2, swatch.color(S_window_lighter));
+ painter->fillRect(rcx + i, rcy + j, 1, 1, swatch.color(S_window_darker));
+ }
+ }
+ }
+ break;
+ }
+ case CE_ToolBar: {
+ auto toolBar = qstyleoption_cast<const QStyleOptionToolBar*>(option);
+ if (!toolBar)
+ break;
+ painter->fillRect(option->rect, option->palette.window().color());
+ bool isFloating = false;
+ if (auto tb = qobject_cast<const QToolBar*>(widget)) {
+ isFloating = tb->isFloating();
+ }
+ if (isFloating) {
+ Ph::fillRectOutline(painter, option->rect, 1, swatch.color(S_window_outline));
+ }
+ break;
+ }
+ case CE_DockWidgetTitle: {
+ auto dwOpt = qstyleoption_cast<const QStyleOptionDockWidget*>(option);
+ if (!dwOpt)
+ break;
+ painter->save();
+ bool verticalTitleBar = dwOpt->verticalTitleBar;
+
+ QRect titleRect = subElementRect(SE_DockWidgetTitleBarText, option, widget);
+ if (verticalTitleBar) {
+ QRect r = dwOpt->rect;
+ QRect rtrans = {r.x(), r.y(), r.height(), r.width()};
+ titleRect = QRect(rtrans.left() + r.bottom() - titleRect.bottom(),
+ rtrans.top() + titleRect.left() - r.left(),
+ titleRect.height(),
+ titleRect.width());
+ painter->translate(rtrans.left(), rtrans.top() + rtrans.width());
+ painter->rotate(-90);
+ painter->translate(-rtrans.left(), -rtrans.top());
+ }
+ if (!dwOpt->title.isEmpty()) {
+ QString titleText = painter->fontMetrics().elidedText(dwOpt->title, Qt::ElideRight, titleRect.width());
+ proxy()->drawItemText(painter,
+ titleRect,
+ Qt::AlignLeft | Qt::AlignVCenter | Qt::TextShowMnemonic,
+ dwOpt->palette,
+ dwOpt->state & State_Enabled,
+ titleText,
+ QPalette::WindowText);
+ }
+ painter->restore();
+ break;
+ }
+ case CE_HeaderSection: {
+ auto header = qstyleoption_cast<const QStyleOptionHeader*>(option);
+ if (!header)
+ break;
+ QRect rect = header->rect;
+ Qt::Orientation orientation = header->orientation;
+ QStyleOptionHeader::SectionPosition position = header->position;
+ // See the "Table header layout reference" comment block at the bottom of
+ // this file for more information to help understand what's going on.
+ bool isLeftToRight = header->direction != Qt::RightToLeft;
+ bool isHorizontal = orientation == Qt::Horizontal;
+ bool isVertical = orientation == Qt::Vertical;
+ bool isEnd = position == QStyleOptionHeader::End;
+ bool isBegin = position == QStyleOptionHeader::Beginning;
+ bool isOnlyOne = position == QStyleOptionHeader::OnlyOneSection;
+ Qt::Edges edges;
+ bool spansToEnd = false;
+ bool isSpecialCorner = false;
+ if ((isHorizontal && isLeftToRight && isEnd) || (isHorizontal && !isLeftToRight && isBegin)
+ || (isVertical && isEnd) || isOnlyOne) {
+ auto hv = qobject_cast<const QHeaderView*>(widget);
+ if (hv) {
+ spansToEnd = hv->stretchLastSection();
+ // In the case where the header item is not stretched to the end, but
+ // could plausibly be in a position where it could happen to be exactly
+ // the right width or height to be appear to be stretched to the end,
+ // we'll check to see if it actually does exactly meet the right (or
+ // bottom in vertical, or left in RTL) edge, and omit drawing the edge
+ // if that's the case. This can commonly happen if you have a tree or
+ // list view and don't set it to stretch, but the widget is still sized
+ // exactly to hold the one column. (It could also happen if there's
+ // user code running to manually stretch the last section as
+ // necessary.)
+ if (!spansToEnd) {
+ QRect viewBound = hv->contentsRect();
+ if (isHorizontal) {
+ if (isLeftToRight) {
+ spansToEnd = rect.right() == viewBound.right();
+ } else {
+ spansToEnd = rect.left() == viewBound.left();
+ }
+ } else if (isVertical) {
+ spansToEnd = rect.bottom() == viewBound.bottom();
+ }
+ }
+ } else {
+ // We only need to do this check in RTL, because the corner button in
+ // RTL *doesn't* need hacks applied. In LTR, we can just treat the
+ // corner button like anything else on the horizontal header bar, and
+ // can skip doing this inherits check.
+ if (isOnlyOne && !isLeftToRight && widget && widget->inherits("QTableCornerButton")) {
+ isSpecialCorner = true;
+ }
+ }
+ }
+
+ if (isSpecialCorner) {
+ // In RTL layout, the corner button in a table view doesn't have any
+ // offset problems. This branch we're on is only taken if we're in RTL
+ // layout and this is the corner button being drawn.
+ edges |= Qt::BottomEdge;
+ if (isLeftToRight)
+ edges |= Qt::RightEdge;
+ else
+ edges |= Qt::LeftEdge;
+ } else if (isHorizontal) {
+ // This branch is taken for horizontal headers in either layout direction
+ // or for the corner button in LTR.
+ edges |= Qt::BottomEdge;
+ if (isLeftToRight) {
+ // In LTR, this code path may be for both the corner button *and* the
+ // actual header item. It doesn't matter in this case, and we were able
+ // to avoid doing an extra inherits call earlier.
+ if (!spansToEnd) {
+ edges |= Qt::RightEdge;
+ }
+ } else {
+ // Note: in right-to-left layouts for horizontal headers, the header
+ // view will unfortunately be shifted to the right by 1 pixel, due to
+ // what appears to be a Qt bug. This causes the vertical lines we draw
+ // in the header view to misalign with the grid, and causes the
+ // rightmost section to have its right edge clipped off. Therefore,
+ // we'll draw the separator on the on the right edge instead of the
+ // left edge. (We would have expected to draw it on the left edge in
+ // RTL layout.) This makes it line up with the grid again, except for
+ // the last section. right by 1 pixel.
+ //
+ // In RTL, the "Begin" position is on the left side for some reason
+ // (the same as LTR.) So "End" is always on the right. Ok, whatever.
+ // See the table at the bottom of this file if you're confused.
+ if (!isOnlyOne && !isEnd) {
+ edges |= Qt::RightEdge;
+ }
+ // The leftmost section in RTL has to draw on both its right and left
+ // edges, instead of just 1 edge like every other configuration. The
+ // left edge will be offset by 1 pixel from the grid, but it's the best
+ // we can do.
+ if (isBegin && !spansToEnd) {
+ edges |= Qt::LeftEdge;
+ }
+ }
+ } else if (isVertical) {
+ if (isLeftToRight) {
+ edges |= Qt::RightEdge;
+ } else {
+ edges |= Qt::LeftEdge;
+ }
+ if (!spansToEnd) {
+ edges |= Qt::BottomEdge;
+ }
+ }
+ QRect bgRect = Ph::expandRect(rect, edges, -1);
+ painter->fillRect(bgRect, swatch.color(S_window));
+ Ph::fillRectEdges(painter, rect, edges, 1, swatch.color(S_frame_outline));
+ break;
+ }
+ case CE_HeaderLabel: {
+ auto header = qstyleoption_cast<const QStyleOptionHeader*>(option);
+ if (!header)
+ break;
+ QRect rect = header->rect;
+ if (!header->icon.isNull()) {
+ int iconExtent = qMin(qMin(rect.height(), rect.width()), option->fontMetrics.height());
+ auto window = widget ? widget->window()->windowHandle() : nullptr;
+ QPixmap pixmap = header->icon.pixmap(window,
+ QSize(iconExtent, iconExtent),
+ (header->state & State_Enabled) ? QIcon::Normal : QIcon::Disabled);
+ int pixw = static_cast<int>(pixmap.width() / pixmap.devicePixelRatio());
+ QRect aligned = alignedRect(
+ header->direction, QFlag(header->iconAlignment), pixmap.size() / pixmap.devicePixelRatio(), rect);
+ QRect inter = aligned.intersected(rect);
+ painter->drawPixmap(inter.x(),
+ inter.y(),
+ pixmap,
+ inter.x() - aligned.x(),
+ inter.y() - aligned.y(),
+ static_cast<int>(aligned.width() * pixmap.devicePixelRatio()),
+ static_cast<int>(pixmap.height() * pixmap.devicePixelRatio()));
+ int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, option, widget);
+ if (header->direction == Qt::LeftToRight)
+ rect.setLeft(rect.left() + pixw + margin);
+ else
+ rect.setRight(rect.right() - pixw - margin);
+ }
+ proxy()->drawItemText(painter,
+ rect,
+ header->textAlignment,
+ header->palette,
+ (header->state & State_Enabled),
+ header->text,
+ QPalette::ButtonText);
+
+ // But we still need some kind of indicator, so draw a line
+ bool drawHighlightLine = option->state & State_On;
+ // Special logic: if the selection mode of the item view is to select every
+ // row or every column, there's no real need to draw special "this
+ // row/column is selected" highlight indicators in the header view. The
+ // application programmer can also disable this explicitly on the header
+ // view, but it's nice to have it done automatically, I think.
+ if (drawHighlightLine) {
+ const QAbstractItemView* itemview = nullptr;
+ // Header view itself is an item view, and we don't care about its
+ // selection behavior -- we care about the actual item view. So try to
+ // get the widget as the header first, then find the item view from
+ // there.
+ auto headerview = qobject_cast<const QHeaderView*>(widget);
+ if (headerview) {
+ // Also don't care about highlights if there's only one row or column.
+ drawHighlightLine = headerview->count() > 1;
+ itemview = qobject_cast<const QAbstractItemView*>(headerview->parentWidget());
+ }
+ if (drawHighlightLine && itemview) {
+ auto selBehavior = itemview->selectionBehavior();
+ if (selBehavior == QAbstractItemView::SelectRows && header->orientation == Qt::Horizontal)
+ drawHighlightLine = false;
+ else if (selBehavior == QAbstractItemView::SelectColumns && header->orientation == Qt::Vertical)
+ drawHighlightLine = false;
+ }
+ }
+
+ if (drawHighlightLine) {
+ QRect r = option->rect;
+ Qt::Edge edge;
+ if (header->orientation == Qt::Horizontal) {
+ edge = Qt::BottomEdge;
+ r.adjust(-2, 1, 1, 1);
+ } else {
+ bool isLeftToRight = option->direction != Qt::RightToLeft;
+ if (isLeftToRight) {
+ edge = Qt::RightEdge;
+ r.adjust(1, -2, 1, 1);
+ } else {
+ edge = Qt::LeftEdge;
+ r.adjust(-1, -2, -1, 1);
+ }
+ }
+ Ph::fillRectEdges(painter, r, edge, 1, swatch.color(S_itemView_headerOnLine));
+ }
+ break;
+ }
+ case CE_ProgressBarGroove: {
+ const qreal rounding = Ph::ProgressBar_Rounding;
+ QRect rect = option->rect;
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_window_outline, S_base);
+ save.restore();
+ if (Ph::OverhangShadows && option->state & State_Enabled) {
+ // Inner shadow
+ const QColor& shadowColor = swatch.color(S_base_shadow);
+ // We can either have the shadow cut into the rounded corners, or leave a
+ // 1px gap, due to AA.
+ Ph::fillRectEdges(painter,
+ rect.adjusted(qRound(rounding / 2) + 1, 1, -(qRound(rounding / 2) + 1), -1),
+ Qt::TopEdge,
+ 1,
+ shadowColor);
+ }
+ break;
+ }
+ case CE_ProgressBarContents: {
+ auto bar = qstyleoption_cast<const QStyleOptionProgressBar*>(option);
+ if (!bar)
+ break;
+ const qreal rounding = Ph::ProgressBar_Rounding;
+ QRect filled, nonFilled;
+ bool isIndeterminate = false;
+ Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate);
+ if (isIndeterminate || bar->progress > bar->minimum) {
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, filled, rounding, swatch, S_progressBar_outline, S_progressBar);
+ Ph::paintBorderedRoundRect(
+ painter, filled.adjusted(1, 1, -1, -1), rounding, swatch, S_progressBar_specular, S_none);
+ if (isIndeterminate) {
+ // TODO paint indeterminate indicator
+ }
+ }
+ break;
+ }
+ case CE_ProgressBarLabel: {
+ auto bar = qstyleoption_cast<const QStyleOptionProgressBar*>(option);
+ if (!bar)
+ break;
+ if (bar->text.isEmpty())
+ break;
+ QRect r = bar->rect.adjusted(2, 2, -2, -2);
+ if (r.isEmpty() || !r.isValid())
+ break;
+ QSize textSize = option->fontMetrics.size(Qt::TextBypassShaping, bar->text);
+ QRect textRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, textSize, option->rect);
+ textRect &= r;
+ if (textRect.isEmpty())
+ break;
+ QRect filled, nonFilled;
+ bool isIndeterminate = false;
+ Ph::progressBarFillRects(bar, filled, nonFilled, isIndeterminate);
+ QRect textNonFilledR = textRect & nonFilled;
+ QRect textFilledR = textRect & filled;
+ bool needsNonFilled = !textNonFilledR.isEmpty();
+ bool needsFilled = !textFilledR.isEmpty();
+ bool needsMasking = needsNonFilled && needsFilled;
+ Ph::PSave save(painter);
+ if (needsNonFilled) {
+ if (needsMasking) {
+ painter->save();
+ painter->setClipRect(textNonFilledR);
+ }
+ painter->setPen(swatch.pen(S_text));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter);
+ if (needsMasking) {
+ painter->restore();
+ }
+ }
+ if (needsFilled) {
+ if (needsMasking) {
+ painter->save();
+ painter->setClipRect(textFilledR);
+ }
+ painter->setPen(swatch.pen(S_highlightedText));
+ painter->setBrush(Qt::NoBrush);
+ painter->drawText(textRect, bar->text, Qt::AlignHCenter | Qt::AlignVCenter);
+ if (needsMasking) {
+ painter->restore();
+ }
+ }
+ break;
+ }
+ case CE_MenuBarItem: {
+ auto mbi = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
+ if (!mbi)
+ break;
+ const QRect r = option->rect;
+ QRect textRect = r;
+ textRect.setY(textRect.y() + (r.height() - option->fontMetrics.height()) / 2);
+ int alignment = Qt::AlignHCenter | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine;
+ if (!proxy()->styleHint(SH_UnderlineShortcut, mbi, widget))
+ alignment |= Qt::TextHideMnemonic;
+ const auto itemState = mbi->state;
+ bool maybeHasAltKeyNavFocus = itemState & State_Selected && itemState & State_HasFocus;
+ bool isSelected = itemState & State_Selected || itemState & State_Sunken;
+ if (!isSelected && maybeHasAltKeyNavFocus && widget) {
+ isSelected = widget->hasFocus();
+ }
+ Swatchy fill = isSelected ? S_highlight : S_window;
+ painter->fillRect(r, swatch.color(fill));
+ QPalette::ColorRole textRole = isSelected ? QPalette::HighlightedText : QPalette::Text;
+ proxy()->drawItemText(
+ painter, textRect, alignment, mbi->palette, mbi->state & State_Enabled, mbi->text, textRole);
+ if (Phantom::MenuBarDrawBorder && !isSelected) {
+ Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider));
+ }
+ break;
+ }
+
+ case CE_MenuItem: {
+ auto menuItem = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
+ if (!menuItem)
+ break;
+ const auto metrics = Ph::MenuItemMetrics::ofFontHeight(option->fontMetrics.height());
+ // Draws one item in a popup menu.
+ if (menuItem->menuItemType == QStyleOptionMenuItem::Separator) {
+ // Phantom ignores text and icons in menu separators, because
+ // 1) The text and icons for separators don't render on Mac native menus
+ // 2) There doesn't seem to be a way to account for the width of the text
+ // properly (Fusion will often draw separator text clipped off)
+ // 3) Setting text on separators also seems to mess up the metrics for
+ // menu items on Mac native menus
+ QRect r = option->rect;
+ r.setHeight(r.height() / 2 + 1);
+ Ph::fillRectEdges(painter, r, Qt::BottomEdge, 1, swatch.color(S_window_divider));
+ break;
+ }
+ const QRect itemRect = option->rect;
+ painter->save();
+ bool isSelected = menuItem->state & State_Selected && menuItem->state & State_Enabled;
+ bool isCheckable = menuItem->checkType != QStyleOptionMenuItem::NotCheckable;
+ bool isChecked = menuItem->checked;
+ bool isSunken = menuItem->state & State_Sunken;
+ bool isEnabled = menuItem->state & State_Enabled;
+ bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu;
+ if (isSelected) {
+ Swatchy fillColor = isSunken ? S_highlight_outline : S_highlight;
+ painter->fillRect(option->rect, swatch.color(fillColor));
+ }
+
+ if (isCheckable) {
+ // Note: check rect might be misaligned vertically if it's a menu from a
+ // combo box. Probably a bug in Qt code?
+ QRect checkRect = Ph::menuItemCheckRect(metrics, option->direction, itemRect, hasSubMenu);
+ Swatchy signColor = !isEnabled ? S_windowText : isSelected ? S_highlightedText : S_windowText;
+ if (menuItem->checkType & QStyleOptionMenuItem::Exclusive) {
+ // Radio button
+ if (isChecked) {
+ painter->setRenderHint(QPainter::Antialiasing);
+ painter->setPen(Qt::NoPen);
+ QPalette::ColorRole textRole =
+ !isEnabled ? QPalette::Text : isSelected ? QPalette::HighlightedText : QPalette::ButtonText;
+ painter->setBrush(option->palette.brush(option->palette.currentColorGroup(), textRole));
+ qreal rx, ry, rw, rh;
+ QRectF(checkRect).getRect(&rx, &ry, &rw, &rh);
+ qreal dim = qMin(checkRect.width(), checkRect.height()) * 0.75;
+ QRectF rf(rx + rw / dim, ry + rh / dim, dim, dim);
+ painter->drawEllipse(rf);
+ }
+ } else {
+ // If we want mouse-down to immediately show the item as
+ // checked/unchecked (kinda bad if the user is click-holding on the
+ // menu instead of click-clicking.)
+ //
+ // if ((isChecked && !isSunken) || (!isChecked && isSunken)) {
+ if (isChecked) {
+ Ph::drawCheck(painter, d->checkBox_pen_scratch, checkRect, swatch, signColor);
+ }
+ }
+ }
+
+ const bool hasIcon = !menuItem->icon.isNull();
+
+ if (hasIcon) {
+ QRect iconRect = Ph::menuItemIconRect(metrics, option->direction, itemRect, hasSubMenu);
+ QIcon::Mode mode = isEnabled ? QIcon::Normal : QIcon::Disabled;
+ if (isSelected && isEnabled)
+ mode = QIcon::Selected;
+ QIcon::State state = isChecked ? QIcon::On : QIcon::Off;
+
+ // TODO hmm, we might be ending up with blurry icons at size 15 instead
+ // of 16 for example on Windows.
+ //
+ // int smallIconSize =
+ // proxy()->pixelMetric(PM_SmallIconSize, option, widget);
+ // QSize iconSize(smallIconSize, smallIconSize);
+ int iconExtent = qMin(iconRect.width(), iconRect.height());
+ QSize iconSize(iconExtent, iconExtent);
+ if (auto combo = qobject_cast<const QComboBox*>(widget)) {
+ iconSize = combo->iconSize();
+ }
+ QWindow* window = widget ? widget->windowHandle() : nullptr;
+ QPixmap pixmap = menuItem->icon.pixmap(window, iconSize, mode, state);
+ const int pixw = static_cast<int>(pixmap.width() / pixmap.devicePixelRatio());
+ const int pixh = static_cast<int>(pixmap.height() / pixmap.devicePixelRatio());
+ QRect pixmapRect = QStyle::alignedRect(option->direction, Qt::AlignCenter, QSize(pixw, pixh), iconRect);
+ painter->drawPixmap(pixmapRect.topLeft(), pixmap);
+ }
+
+ // Draw main text and mnemonic text
+ QStringRef s(&menuItem->text);
+ if (!s.isEmpty()) {
+ QRect textRect =
+ Ph::menuItemTextRect(metrics, option->direction, itemRect, hasSubMenu, hasIcon, menuItem->tabWidth);
+ int t = s.indexOf(QLatin1Char('\t'));
+ int text_flags =
+ Qt::AlignLeft | Qt::AlignTop | Qt::TextShowMnemonic | Qt::TextDontClip | Qt::TextSingleLine;
+ if (!styleHint(SH_UnderlineShortcut, menuItem, widget))
+ text_flags |= Qt::TextHideMnemonic;
+#if 0
+ painter->save();
+#endif
+ painter->setPen(swatch.pen(isSelected ? S_highlightedText : S_text));
+
+ // Comment from original Qt code which did some dance with the font:
+ //
+ // font may not have any "hard" flags set. We override the point size so
+ // that when it is resolved against the device, this font will win. This
+ // is mainly to handle cases where someone sets the font on the window
+ // and then the combo inherits it and passes it onward. At that point the
+ // resolve mask is very, very weak. This makes it stonger.
+#if 0
+ QFont font = menuItem->font;
+ font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF());
+ painter->setFont(font);
+#endif
+
+ // My comment:
+ //
+ // What actually looks like is happening is that the qplatformtheme may
+ // have set a per-class font for menus. The QComboMenuDelegate sets the
+ // combo box's own font on the QStyleOptionMenuItem when passing it in
+ // here and when calling sizeFromContents with CT_MenuItem, but the
+ // QPainter we're called with hasn't had its font set to it -- it's still
+ // set to the QMenu/QMenuItem app fonts hash font. So if it's a menu
+ // coming from a combo box, let's just go ahead and set the font for it
+ // if it doesn't match, since that's probably what it wanted to do. I
+ // think. And as described above, we have to do the weird dance with the
+ // resolve mask... which is some internal Qt detail that we aren't
+ // supposed to have to deal with, but here we are.
+ //
+ // Ok, there's another problem, and QFusionStyle also suffers from it: in
+ // high DPI, setting the pointSizeF and setting the font again won't
+ // necessarily give us the right font (at least in Windows.) The font
+ // might have too thin of a weight, and probably other problems. So just
+ // forget about it: we'll have Phantom return 0 for the style hint that
+ // the combo box uses to determine if it should use a QMenu popup instead
+ // of a regular dropdown menu thing. The popup menu might actually be
+ // better for usability in some cases, and it's how combos work on Mac
+ // and BeOS, but it won't work anyway for editable combo boxes in Qt, and
+ // the font issues just make it not worth it. So we'll have a dropdown
+ // guy like a traditional Windows thing.
+ //
+ // If you want to try it out again, go to SH_ComboBox_Popup and have it
+ // return 1.
+ //
+ // Alternatively, we could instead have the CT_MenuItem handling code try
+ // to be aggressively clever and use the qt app font hash to look up the
+ // expected font for a QMenu and use that for calculating its metrics.
+ // Unfortunately, that probably won't work so great if the combo/menu
+ // actually wants to use custom fonts in its listing, since we'd be
+ // ignoring it. That's how UseQMenuForComboBoxPopup currently works,
+ // though it tests for Qt::WA_SetFont as an attempt at recognizing when
+ // it shouldn't use the qt font hash for QMenu.
+#if 0
+ if (qobject_cast<const QComboBox*>(widget)) {
+ QFont font = menuItem->font;
+ font.setPointSizeF(QFontInfo(menuItem->font).pointSizeF());
+ painter->setFont(font);
+ }
+#endif
+
+ // Draw mnemonic text
+ if (t >= 0) {
+ QRect mnemonicR =
+ Ph::menuItemMnemonicRect(metrics, option->direction, itemRect, hasSubMenu, menuItem->tabWidth);
+ const QStringRef textToDrawRef = s.mid(t + 1);
+ const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size());
+ painter->drawText(mnemonicR, text_flags, unsafeTextToDraw);
+ s = s.left(t);
+ }
+ const QStringRef textToDrawRef = s.left(t);
+ const QString unsafeTextToDraw = QString::fromRawData(textToDrawRef.constData(), textToDrawRef.size());
+ painter->drawText(textRect, text_flags, unsafeTextToDraw);
+
+#if 0
+ painter->restore();
+#endif
+ }
+
+ // SubMenu Arrow
+ if (hasSubMenu) {
+ Qt::ArrowType arrow = option->direction == Qt::RightToLeft ? Qt::LeftArrow : Qt::RightArrow;
+ QRect arrowRect = Ph::menuItemArrowRect(metrics, option->direction, itemRect);
+ Swatchy arrowColor = isSelected ? S_highlightedText : S_indicator_current;
+ Ph::drawArrow(painter, arrowRect, arrow, swatch.brush(arrowColor));
+ }
+ painter->restore();
+ break;
+ }
+ case CE_MenuHMargin:
+ case CE_MenuVMargin:
+ case CE_MenuEmptyArea:
+ break;
+ case CE_PushButton: {
+ auto btn = qstyleoption_cast<const QStyleOptionButton*>(option);
+ if (!btn)
+ break;
+ proxy()->drawControl(CE_PushButtonBevel, btn, painter, widget);
+ QStyleOptionButton subopt = *btn;
+ subopt.rect = subElementRect(SE_PushButtonContents, btn, widget);
+ proxy()->drawControl(CE_PushButtonLabel, &subopt, painter, widget);
+ break;
+ }
+ case CE_PushButtonLabel: {
+ auto button = qstyleoption_cast<const QStyleOptionButton*>(option);
+ if (!button)
+ break;
+ // This code is very similar to QCommonStyle's implementation, but doesn't
+ // set the icon mode to active when focused.
+ QRect textRect = button->rect;
+ int tf = Qt::AlignVCenter | Qt::TextShowMnemonic;
+ if (!proxy()->styleHint(SH_UnderlineShortcut, button, widget))
+ tf |= Qt::TextHideMnemonic;
+ if (!button->icon.isNull()) {
+ // Center both icon and text
+ QRect iconRect;
+ QIcon::Mode mode = button->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
+ QIcon::State state = button->state & State_On ? QIcon::On : QIcon::Off;
+ auto window = widget ? widget->window()->windowHandle() : nullptr;
+ QPixmap pixmap = button->icon.pixmap(window, button->iconSize, mode, state);
+ int pixmapWidth = static_cast<int>(pixmap.width() / pixmap.devicePixelRatio());
+ int pixmapHeight = static_cast<int>(pixmap.height() / pixmap.devicePixelRatio());
+ int labelWidth = pixmapWidth;
+ int labelHeight = pixmapHeight;
+ // 4 is hardcoded in QPushButton::sizeHint()
+ int iconSpacing = 4;
+ int textWidth = button->fontMetrics.boundingRect(option->rect, tf, button->text).width();
+ if (!button->text.isEmpty())
+ labelWidth += (textWidth + iconSpacing);
+ iconRect = QRect(textRect.x() + (textRect.width() - labelWidth) / 2,
+ textRect.y() + (textRect.height() - labelHeight) / 2,
+ pixmapWidth,
+ pixmapHeight);
+ iconRect = visualRect(button->direction, textRect, iconRect);
+ tf |= Qt::AlignLeft; // left align, we adjust the text-rect instead
+ if (button->direction == Qt::RightToLeft)
+ textRect.setRight(iconRect.left() - iconSpacing);
+ else
+ textRect.setLeft(iconRect.left() + iconRect.width() + iconSpacing);
+ if (button->state & (State_On | State_Sunken))
+ iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget),
+ proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget));
+ painter->drawPixmap(iconRect, pixmap);
+ } else {
+ tf |= Qt::AlignHCenter;
+ }
+ if (button->state & (State_On | State_Sunken))
+ textRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, option, widget),
+ proxy()->pixelMetric(PM_ButtonShiftVertical, option, widget));
+ if (button->features & QStyleOptionButton::HasMenu) {
+ int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, button, widget);
+ if (button->direction == Qt::LeftToRight)
+ textRect = textRect.adjusted(0, 0, -indicatorSize, 0);
+ else
+ textRect = textRect.adjusted(indicatorSize, 0, 0, 0);
+ }
+ proxy()->drawItemText(painter,
+ textRect,
+ tf,
+ button->palette,
+ (button->state & State_Enabled),
+ button->text,
+ QPalette::ButtonText);
+ break;
+ }
+ case CE_MenuBarEmptyArea: {
+ QRect rect = option->rect;
+ if (Phantom::MenuBarDrawBorder) {
+ Ph::fillRectEdges(painter, rect, Qt::BottomEdge, 1, swatch.color(S_window_divider));
+ }
+ painter->fillRect(rect.adjusted(0, 0, 0, -1), swatch.color(S_window));
+ break;
+ }
+ case CE_TabBarTabShape: {
+ auto tab = qstyleoption_cast<const QStyleOptionTab*>(option);
+ if (!tab)
+ break;
+ bool rtlHorTabs = (tab->direction == Qt::RightToLeft
+ && (tab->shape == QTabBar::RoundedNorth || tab->shape == QTabBar::RoundedSouth));
+ bool isSelected = tab->state & State_Selected;
+ bool lastTab = ((!rtlHorTabs && tab->position == QStyleOptionTab::End)
+ || (rtlHorTabs && tab->position == QStyleOptionTab::Beginning));
+ bool onlyOne = tab->position == QStyleOptionTab::OnlyOneTab;
+ int tabOverlap = pixelMetric(PM_TabBarTabOverlap, option, widget);
+ const qreal rounding = Ph::TabBarTab_Rounding;
+ Qt::Edge outerEdge = Qt::TopEdge;
+ Qt::Edge edgeTowardNextTab = Qt::RightEdge;
+ switch (tab->shape) {
+ case QTabBar::RoundedNorth:
+ outerEdge = Qt::TopEdge;
+ edgeTowardNextTab = Qt::RightEdge;
+ break;
+ case QTabBar::RoundedSouth:
+ outerEdge = Qt::BottomEdge;
+ edgeTowardNextTab = Qt::RightEdge;
+ break;
+ case QTabBar::RoundedWest:
+ outerEdge = Qt::LeftEdge;
+ edgeTowardNextTab = Qt::BottomEdge;
+ break;
+ case QTabBar::RoundedEast:
+ outerEdge = Qt::RightEdge;
+ edgeTowardNextTab = Qt::BottomEdge;
+ break;
+ default:
+ QCommonStyle::drawControl(element, tab, painter, widget);
+ return;
+ }
+ Qt::Edge innerEdge = Ph::oppositeEdge(outerEdge);
+ Qt::Edge edgeAwayNextTab = Ph::oppositeEdge(edgeTowardNextTab);
+ QRect shapeClipRect = Ph::expandRect(option->rect, innerEdge, -2);
+ QRect drawRect = Ph::expandRect(shapeClipRect, innerEdge, 3 + 2 * rounding + 1);
+ if (!onlyOne && !lastTab) {
+ drawRect = Ph::expandRect(drawRect, edgeTowardNextTab, tabOverlap);
+ shapeClipRect = Ph::expandRect(shapeClipRect, edgeTowardNextTab, tabOverlap);
+ }
+ if (!isSelected) {
+ int offset = proxy()->pixelMetric(PM_TabBarTabShiftVertical, option, widget);
+ drawRect = Ph::expandRect(drawRect, outerEdge, -offset);
+ }
+ painter->save();
+ painter->setClipRect(shapeClipRect);
+ bool hasFrame = tab->features & QStyleOptionTab::HasFrame && !tab->documentMode;
+ Swatchy tabFrameColor, thisFillColor, specular;
+ if (hasFrame) {
+ tabFrameColor = S_tabFrame;
+ if (isSelected) {
+ thisFillColor = S_tabFrame;
+ specular = S_tabFrame_specular;
+ } else {
+ thisFillColor = S_inactiveTabYesFrame;
+ specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabYesFrame_specular : S_none;
+ }
+ } else {
+ tabFrameColor = S_window;
+ if (isSelected) {
+ thisFillColor = S_window;
+ specular = S_window_specular;
+ } else {
+ thisFillColor = S_inactiveTabNoFrame;
+ specular = Ph::TabBar_InactiveTabsHaveSpecular ? S_inactiveTabNoFrame_specular : S_none;
+ }
+ }
+ auto frameColor = isSelected ? S_frame_outline : S_window_outline;
+ Ph::paintBorderedRoundRect(painter, drawRect, rounding, swatch, frameColor, thisFillColor);
+ Ph::paintBorderedRoundRect(painter, drawRect.adjusted(1, 1, -1, -1), rounding, swatch, specular, S_none);
+ painter->restore();
+ if (isSelected) {
+ QRect highlightRect = drawRect.adjusted(2, 1, -2, 0);
+ highlightRect.setHeight(Ph::dpiScaled(2.0));
+ QRect highlightRectSpec = highlightRect.adjusted(-1, -1, 1, 0);
+ painter->fillRect(highlightRectSpec, Ph::DeriveColors::lightSpecularOf(swatch.color(S_highlight)));
+ painter->fillRect(highlightRect, swatch.color(S_highlight));
+
+ QRect refillRect = Ph::rectFromInnerEdgeWithThickness(shapeClipRect, innerEdge, 2);
+ refillRect = Ph::rectTranslatedTowardEdge(refillRect, innerEdge, 2);
+ refillRect = Ph::expandRect(refillRect, edgeAwayNextTab | edgeTowardNextTab, -1);
+ painter->fillRect(refillRect, swatch.color(tabFrameColor));
+ Ph::fillRectEdges(painter, refillRect, edgeAwayNextTab | edgeTowardNextTab, 1, swatch.color(specular));
+ }
+ break;
+ }
+ case CE_ItemViewItem: {
+ auto ivopt = qstyleoption_cast<const QStyleOptionViewItem*>(option);
+ if (!ivopt)
+ break;
+ // Hack to work around broken grid line drawing in Qt's table view code:
+ //
+ // We tell it that the grid line color is a color via
+ // SH_Table_GridLineColor. It draws the grid lines, but it in high DPI it's
+ // broken because it uses a pen/path to draw the line, which makes it too
+ // narrow, subpixel-incorrectly-antialiased, and/or offset from its correct
+ // position. So when we draw the item view items in a table view, we'll
+ // also try to paint 1 pixel outside of our current rect to try to fill in
+ // the incorrectly painted areas where the grid lines are.
+ //
+ // Also note that the table views with the bad drawing code, when
+ // scrolling, will leave garbage behind in the incorrectly-drawn grid line
+ // areas. This will also paint over that.
+ bool overdrawGridHack = false;
+ if (auto tableWidget = qobject_cast<const QTableView*>(widget)) {
+ overdrawGridHack = tableWidget->showGrid() && tableWidget->gridStyle() == Qt::SolidLine;
+ }
+ if (overdrawGridHack) {
+ QRect r = option->rect.adjusted(-1, -1, 1, 1);
+ Ph::fillRectOutline(painter, r, 1, swatch.color(S_base_divider));
+ }
+ QCommonStyle::drawControl(element, option, painter, widget);
+ break;
+ }
+ case CE_ShapedFrame: {
+ auto frameopt = qstyleoption_cast<const QStyleOptionFrame*>(option);
+ if (frameopt) {
+ if (frameopt->frameShape == QFrame::HLine) {
+ QRect r = option->rect;
+ r.setY(r.y() + r.height() / 2);
+ r.setHeight(2);
+ painter->fillRect(r, swatch.color(S_tabFrame_specular));
+ r.setHeight(1);
+ painter->fillRect(r, swatch.color(S_frame_outline));
+ break;
+ } else if (frameopt->frameShape == QFrame::VLine) {
+ QRect r = option->rect;
+ r.setX(r.x() + r.width() / 2);
+ r.setWidth(2);
+ painter->fillRect(r, swatch.color(S_tabFrame_specular));
+ r.setWidth(1);
+ painter->fillRect(r, swatch.color(S_frame_outline));
+ break;
+ }
+ }
+ QCommonStyle::drawControl(element, option, painter, widget);
+ break;
+ }
+ default:
+ QCommonStyle::drawControl(element, option, painter, widget);
+ break;
+ }
+}
+
+QPalette BaseStyle::standardPalette() const
+{
+ return QCommonStyle::standardPalette();
+}
+
+void BaseStyle::drawComplexControl(ComplexControl control,
+ const QStyleOptionComplex* option,
+ QPainter* painter,
+ const QWidget* widget) const
+{
+#ifdef BUILD_WITH_EASY_PROFILER
+ EASY_BLOCK("drawControl");
+ const char* controlCString = QMetaEnum::fromType<QStyle::ComplexControl>().valueToKey(control);
+ EASY_TEXT("ComplexControl", controlCString);
+#endif
+ using Swatchy = Phantom::Swatchy;
+ using namespace Phantom::SwatchColors;
+ namespace Ph = Phantom;
+ auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette);
+ const Ph::PhSwatch& swatch = *ph_swatchPtr.data();
+
+ switch (control) {
+ case CC_GroupBox: {
+ auto groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option);
+ if (!groupBox)
+ break;
+ painter->save();
+ // Draw frame
+ QRect textRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxLabel, widget);
+ QRect checkBoxRect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxCheckBox, widget);
+
+ if (groupBox->subControls & QStyle::SC_GroupBoxFrame) {
+ QStyleOptionFrame frame;
+ frame.QStyleOption::operator=(*groupBox);
+ frame.features = groupBox->features;
+ frame.lineWidth = groupBox->lineWidth;
+ frame.midLineWidth = groupBox->midLineWidth;
+ frame.rect = proxy()->subControlRect(CC_GroupBox, option, SC_GroupBoxFrame, widget);
+ proxy()->drawPrimitive(PE_FrameGroupBox, &frame, painter, widget);
+ }
+
+ // Draw title
+ if ((groupBox->subControls & QStyle::SC_GroupBoxLabel) && !groupBox->text.isEmpty()) {
+ // groupBox->textColor gets the incorrect palette here
+ painter->setPen(QPen(option->palette.windowText(), 1));
+ unsigned alignment = groupBox->textAlignment;
+ if (!proxy()->styleHint(QStyle::SH_UnderlineShortcut, option, widget))
+ alignment |= Qt::TextHideMnemonic;
+
+ proxy()->drawItemText(painter,
+ textRect,
+ alignment | Qt::TextShowMnemonic | Qt::AlignLeft,
+ groupBox->palette,
+ groupBox->state & State_Enabled,
+ groupBox->text,
+ QPalette::NoRole);
+
+ if (groupBox->state & State_HasFocus) {
+ QStyleOptionFocusRect fropt;
+ fropt.QStyleOption::operator=(*groupBox);
+ fropt.rect = textRect.adjusted(-1, 0, 1, 0);
+ proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, painter, widget);
+ }
+ }
+
+ // Draw checkbox
+ if (groupBox->subControls & SC_GroupBoxCheckBox) {
+ QStyleOptionButton box;
+ box.QStyleOption::operator=(*groupBox);
+ box.rect = checkBoxRect;
+ proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, painter, widget);
+ }
+ painter->restore();
+ break;
+ }
+ case CC_SpinBox: {
+ auto spinBox = qstyleoption_cast<const QStyleOptionSpinBox*>(option);
+ if (!spinBox)
+ break;
+ const qreal rounding = Ph::SpinBox_Rounding;
+ bool isLeftToRight = option->direction != Qt::RightToLeft;
+ const QRect rect = spinBox->rect;
+ bool sunken = spinBox->state & State_Sunken;
+ bool upIsActive = spinBox->activeSubControls == SC_SpinBoxUp;
+ bool downIsActive = spinBox->activeSubControls == SC_SpinBoxDown;
+ bool hasFocus = option->state & State_HasFocus;
+ bool isEnabled = option->state & State_Enabled;
+ QRect upRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxUp, widget);
+ QRect downRect = proxy()->subControlRect(CC_SpinBox, spinBox, SC_SpinBoxDown, widget);
+ if (spinBox->frame) {
+ QRect upDownRect = upRect | downRect;
+ upDownRect.adjust(0, -1, 0, 1);
+ painter->save(); // 0
+ // Fill background
+ Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, S_none, S_base);
+ // Draw button fill
+ painter->setClipRect(upDownRect);
+ // Side with the border
+ Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge;
+ Ph::paintBorderedRoundRect(
+ painter, Ph::expandRect(upDownRect, Ph::oppositeEdge(edge), -1), rounding, swatch, S_none, S_button);
+ painter->restore(); // 0
+ if (Ph::OverhangShadows && !hasFocus && isEnabled) {
+ // Imperfect, leaves tiny gap on left and right. Going closer would eat
+ // into the outline, though.
+ QRect shadowRect = rect.adjusted(qRound(rounding / 2), 1, -qRound(rounding / 2), -1);
+ if (isLeftToRight) {
+ shadowRect.setRight(upDownRect.left());
+ } else {
+ shadowRect.setLeft(upDownRect.right());
+ }
+ Ph::fillRectEdges(painter, shadowRect, Qt::TopEdge, 1, swatch.color(S_base_shadow));
+ }
+ if ((spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled) && upIsActive && sunken) {
+ painter->fillRect(upRect, swatch.color(S_button_pressed));
+ }
+ if ((spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled) && downIsActive && sunken) {
+ painter->fillRect(downRect, swatch.color(S_button_pressed));
+ }
+ // Left or right border line
+ Ph::fillRectEdges(painter, upDownRect, edge, 1, swatch.color(S_window_outline));
+ Ph::PSave save(painter);
+ // Outline over entire frame
+ Swatchy outlineColor = hasFocus ? S_highlight_outline : S_window_outline;
+ Ph::paintBorderedRoundRect(painter, rect, rounding, swatch, outlineColor, S_none);
+ save.restore();
+ }
+
+ if (spinBox->buttonSymbols == QAbstractSpinBox::PlusMinus) {
+ Ph::PSave save(painter);
+ // TODO fix up old fusion code here
+ int centerX = upRect.center().x();
+ int centerY = upRect.center().y();
+ Swatchy arrowColorUp =
+ spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled ? S_indicator_current : S_indicator_disabled;
+ Swatchy arrowColorDown =
+ spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled ? S_indicator_current : S_indicator_disabled;
+ painter->setPen(swatch.pen(arrowColorUp));
+ painter->drawLine(centerX - 1, centerY, centerX + 3, centerY);
+ painter->drawLine(centerX + 1, centerY - 2, centerX + 1, centerY + 2);
+ centerX = downRect.center().x();
+ centerY = downRect.center().y();
+ painter->setPen(arrowColorDown);
+ painter->drawLine(centerX - 1, centerY, centerX + 3, centerY);
+ } else if (spinBox->buttonSymbols == QAbstractSpinBox::UpDownArrows) {
+ int xoffs = isLeftToRight ? 0 : 1;
+ Ph::drawArrow(painter,
+ upRect.adjusted(4 + xoffs, 1, -5 + xoffs, 1),
+ Qt::UpArrow,
+ swatch,
+ spinBox->stepEnabled & QAbstractSpinBox::StepUpEnabled);
+ Ph::drawArrow(painter,
+ downRect.adjusted(4 + xoffs, 0, -5 + xoffs, -1),
+ Qt::DownArrow,
+ swatch,
+ spinBox->stepEnabled & QAbstractSpinBox::StepDownEnabled);
+ }
+ break;
+ }
+ case CC_TitleBar: {
+ auto titleBar = qstyleoption_cast<const QStyleOptionTitleBar*>(option);
+ if (!titleBar)
+ break;
+ painter->save();
+ const int buttonMargin = 5;
+ bool active = (titleBar->titleBarState & State_Active);
+ QRect fullRect = titleBar->rect;
+ QPalette palette = option->palette;
+ QColor highlight = option->palette.highlight().color();
+ QColor outline = option->palette.dark().color();
+
+ QColor titleBarFrameBorder(active ? highlight.darker(180) : outline.darker(110));
+ QColor titleBarHighlight(active ? highlight.lighter(120) : palette.background().color().lighter(120));
+ QColor textColor(active ? 0xffffff : 0xff000000);
+ QColor textAlphaColor(active ? 0xffffff : 0xff000000);
+
+ {
+ // Fill title
+ QColor titlebarColor = QColor(active ? highlight : palette.background().color());
+ painter->fillRect(option->rect.adjusted(1, 1, -1, 0), titlebarColor);
+ // Frame and rounded corners
+ painter->setPen(titleBarFrameBorder);
+
+ // top outline
+ painter->drawLine(fullRect.left() + 5, fullRect.top(), fullRect.right() - 5, fullRect.top());
+ painter->drawLine(fullRect.left(), fullRect.top() + 4, fullRect.left(), fullRect.bottom());
+ const QPoint points[5] = {QPoint(fullRect.left() + 4, fullRect.top() + 1),
+ QPoint(fullRect.left() + 3, fullRect.top() + 1),
+ QPoint(fullRect.left() + 2, fullRect.top() + 2),
+ QPoint(fullRect.left() + 1, fullRect.top() + 3),
+ QPoint(fullRect.left() + 1, fullRect.top() + 4)};
+ painter->drawPoints(points, 5);
+
+ painter->drawLine(fullRect.right(), fullRect.top() + 4, fullRect.right(), fullRect.bottom());
+ const QPoint points2[5] = {QPoint(fullRect.right() - 3, fullRect.top() + 1),
+ QPoint(fullRect.right() - 4, fullRect.top() + 1),
+ QPoint(fullRect.right() - 2, fullRect.top() + 2),
+ QPoint(fullRect.right() - 1, fullRect.top() + 3),
+ QPoint(fullRect.right() - 1, fullRect.top() + 4)};
+ painter->drawPoints(points2, 5);
+
+ // draw bottomline
+ painter->drawLine(fullRect.right(), fullRect.bottom(), fullRect.left(), fullRect.bottom());
+
+ // top highlight
+ painter->setPen(titleBarHighlight);
+ painter->drawLine(fullRect.left() + 6, fullRect.top() + 1, fullRect.right() - 6, fullRect.top() + 1);
+ }
+ // draw title
+ QRect textRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarLabel, widget);
+ painter->setPen(active ? (titleBar->palette.text().color().lighter(120)) : titleBar->palette.text().color());
+ // Note workspace also does elliding but it does not use the correct font
+ QString title = painter->fontMetrics().elidedText(titleBar->text, Qt::ElideRight, textRect.width() - 14);
+ painter->drawText(textRect.adjusted(1, 1, 1, 1), title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter));
+ painter->setPen(Qt::white);
+ if (active)
+ painter->drawText(textRect, title, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter));
+ // min button
+ if ((titleBar->subControls & SC_TitleBarMinButton) && (titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint)
+ && !(titleBar->titleBarState & Qt::WindowMinimized)) {
+ QRect minButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMinButton, widget);
+ if (minButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_MouseOver);
+ bool sunken = (titleBar->activeSubControls & SC_TitleBarMinButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, minButtonRect, hover, sunken);
+ QRect minButtonIconRect =
+ minButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin);
+ painter->setPen(textColor);
+ painter->drawLine(minButtonIconRect.center().x() - 2,
+ minButtonIconRect.center().y() + 3,
+ minButtonIconRect.center().x() + 3,
+ minButtonIconRect.center().y() + 3);
+ painter->drawLine(minButtonIconRect.center().x() - 2,
+ minButtonIconRect.center().y() + 4,
+ minButtonIconRect.center().x() + 3,
+ minButtonIconRect.center().y() + 4);
+ painter->setPen(textAlphaColor);
+ painter->drawLine(minButtonIconRect.center().x() - 3,
+ minButtonIconRect.center().y() + 3,
+ minButtonIconRect.center().x() - 3,
+ minButtonIconRect.center().y() + 4);
+ painter->drawLine(minButtonIconRect.center().x() + 4,
+ minButtonIconRect.center().y() + 3,
+ minButtonIconRect.center().x() + 4,
+ minButtonIconRect.center().y() + 4);
+ }
+ }
+ // max button
+ if ((titleBar->subControls & SC_TitleBarMaxButton) && (titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint)
+ && !(titleBar->titleBarState & Qt::WindowMaximized)) {
+ QRect maxButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarMaxButton, widget);
+ if (maxButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_MouseOver);
+ bool sunken = (titleBar->activeSubControls & SC_TitleBarMaxButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, maxButtonRect, hover, sunken);
+
+ QRect maxButtonIconRect =
+ maxButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin);
+
+ painter->setPen(textColor);
+ painter->drawRect(maxButtonIconRect.adjusted(0, 0, -1, -1));
+ painter->drawLine(maxButtonIconRect.left() + 1,
+ maxButtonIconRect.top() + 1,
+ maxButtonIconRect.right() - 1,
+ maxButtonIconRect.top() + 1);
+ painter->setPen(textAlphaColor);
+ const QPoint points[4] = {maxButtonIconRect.topLeft(),
+ maxButtonIconRect.topRight(),
+ maxButtonIconRect.bottomLeft(),
+ maxButtonIconRect.bottomRight()};
+ painter->drawPoints(points, 4);
+ }
+ }
+
+ // close button
+ if ((titleBar->subControls & SC_TitleBarCloseButton) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) {
+ QRect closeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarCloseButton, widget);
+ if (closeButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_MouseOver);
+ bool sunken =
+ (titleBar->activeSubControls & SC_TitleBarCloseButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, closeButtonRect, hover, sunken);
+ QRect closeIconRect =
+ closeButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin);
+ painter->setPen(textAlphaColor);
+ const QLine lines[4] = {QLine(closeIconRect.left() + 1,
+ closeIconRect.top(),
+ closeIconRect.right(),
+ closeIconRect.bottom() - 1),
+ QLine(closeIconRect.left(),
+ closeIconRect.top() + 1,
+ closeIconRect.right() - 1,
+ closeIconRect.bottom()),
+ QLine(closeIconRect.right() - 1,
+ closeIconRect.top(),
+ closeIconRect.left(),
+ closeIconRect.bottom() - 1),
+ QLine(closeIconRect.right(),
+ closeIconRect.top() + 1,
+ closeIconRect.left() + 1,
+ closeIconRect.bottom())};
+ painter->drawLines(lines, 4);
+ const QPoint points[4] = {closeIconRect.topLeft(),
+ closeIconRect.topRight(),
+ closeIconRect.bottomLeft(),
+ closeIconRect.bottomRight()};
+ painter->drawPoints(points, 4);
+
+ painter->setPen(textColor);
+ painter->drawLine(closeIconRect.left() + 1,
+ closeIconRect.top() + 1,
+ closeIconRect.right() - 1,
+ closeIconRect.bottom() - 1);
+ painter->drawLine(closeIconRect.left() + 1,
+ closeIconRect.bottom() - 1,
+ closeIconRect.right() - 1,
+ closeIconRect.top() + 1);
+ }
+ }
+
+ // normalize button
+ if ((titleBar->subControls & SC_TitleBarNormalButton)
+ && (((titleBar->titleBarFlags & Qt::WindowMinimizeButtonHint)
+ && (titleBar->titleBarState & Qt::WindowMinimized))
+ || ((titleBar->titleBarFlags & Qt::WindowMaximizeButtonHint)
+ && (titleBar->titleBarState & Qt::WindowMaximized)))) {
+ QRect normalButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarNormalButton, widget);
+ if (normalButtonRect.isValid()) {
+
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_MouseOver);
+ bool sunken =
+ (titleBar->activeSubControls & SC_TitleBarNormalButton) && (titleBar->state & State_Sunken);
+ QRect normalButtonIconRect =
+ normalButtonRect.adjusted(buttonMargin, buttonMargin, -buttonMargin, -buttonMargin);
+ Ph::drawMdiButton(painter, titleBar, normalButtonRect, hover, sunken);
+
+ QRect frontWindowRect = normalButtonIconRect.adjusted(0, 3, -3, 0);
+ painter->setPen(textColor);
+ painter->drawRect(frontWindowRect.adjusted(0, 0, -1, -1));
+ painter->drawLine(frontWindowRect.left() + 1,
+ frontWindowRect.top() + 1,
+ frontWindowRect.right() - 1,
+ frontWindowRect.top() + 1);
+ painter->setPen(textAlphaColor);
+ const QPoint points[4] = {frontWindowRect.topLeft(),
+ frontWindowRect.topRight(),
+ frontWindowRect.bottomLeft(),
+ frontWindowRect.bottomRight()};
+ painter->drawPoints(points, 4);
+
+ QRect backWindowRect = normalButtonIconRect.adjusted(3, 0, 0, -3);
+ QRegion clipRegion = backWindowRect;
+ clipRegion -= frontWindowRect;
+ painter->save();
+ painter->setClipRegion(clipRegion);
+ painter->setPen(textColor);
+ painter->drawRect(backWindowRect.adjusted(0, 0, -1, -1));
+ painter->drawLine(backWindowRect.left() + 1,
+ backWindowRect.top() + 1,
+ backWindowRect.right() - 1,
+ backWindowRect.top() + 1);
+ painter->setPen(textAlphaColor);
+ const QPoint points2[4] = {backWindowRect.topLeft(),
+ backWindowRect.topRight(),
+ backWindowRect.bottomLeft(),
+ backWindowRect.bottomRight()};
+ painter->drawPoints(points2, 4);
+ painter->restore();
+ }
+ }
+
+ // context help button
+ if (titleBar->subControls & SC_TitleBarContextHelpButton
+ && (titleBar->titleBarFlags & Qt::WindowContextHelpButtonHint)) {
+ QRect contextHelpButtonRect =
+ proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarContextHelpButton, widget);
+ if (contextHelpButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_MouseOver);
+ bool sunken =
+ (titleBar->activeSubControls & SC_TitleBarContextHelpButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, contextHelpButtonRect, hover, sunken);
+ // This is lame, but I doubt it will get used often. Previously, XPM
+ // icon was used here (very poorly, by re-allocating a QImage over and
+ // over and modifying/painting it)
+ QIcon helpIcon = QCommonStyle::standardIcon(QStyle::SP_DialogHelpButton);
+ helpIcon.paint(painter, contextHelpButtonRect.adjusted(4, 4, -4, -4));
+ }
+ }
+
+ // shade button
+ if (titleBar->subControls & SC_TitleBarShadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) {
+ QRect shadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarShadeButton, widget);
+ if (shadeButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_MouseOver);
+ bool sunken =
+ (titleBar->activeSubControls & SC_TitleBarShadeButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, shadeButtonRect, hover, sunken);
+ Ph::drawArrow(painter, shadeButtonRect.adjusted(5, 7, -5, -7), Qt::UpArrow, swatch);
+ }
+ }
+
+ // unshade button
+ if (titleBar->subControls & SC_TitleBarUnshadeButton && (titleBar->titleBarFlags & Qt::WindowShadeButtonHint)) {
+ QRect unshadeButtonRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarUnshadeButton, widget);
+ if (unshadeButtonRect.isValid()) {
+ bool hover =
+ (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_MouseOver);
+ bool sunken =
+ (titleBar->activeSubControls & SC_TitleBarUnshadeButton) && (titleBar->state & State_Sunken);
+ Ph::drawMdiButton(painter, titleBar, unshadeButtonRect, hover, sunken);
+ Ph::drawArrow(painter, unshadeButtonRect.adjusted(5, 7, -5, -7), Qt::DownArrow, swatch);
+ }
+ }
+
+ if ((titleBar->subControls & SC_TitleBarSysMenu) && (titleBar->titleBarFlags & Qt::WindowSystemMenuHint)) {
+ QRect iconRect = proxy()->subControlRect(CC_TitleBar, titleBar, SC_TitleBarSysMenu, widget);
+ if (iconRect.isValid()) {
+ if (!titleBar->icon.isNull()) {
+ titleBar->icon.paint(painter, iconRect);
+ } else {
+ QStyleOption tool = *titleBar;
+ QPixmap pm = proxy()->standardIcon(SP_TitleBarMenuButton, &tool, widget).pixmap(16, 16);
+ tool.rect = iconRect;
+ painter->save();
+ proxy()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, pm);
+ painter->restore();
+ }
+ }
+ }
+ painter->restore();
+ break;
+ }
+ case CC_ScrollBar: {
+ auto scrollBar = qstyleoption_cast<const QStyleOptionSlider*>(option);
+ if (!scrollBar)
+ break;
+ auto pr = proxy();
+ QRect scrollBarSubLine = pr->subControlRect(control, scrollBar, SC_ScrollBarSubLine, widget);
+ QRect scrollBarAddLine = pr->subControlRect(control, scrollBar, SC_ScrollBarAddLine, widget);
+ QRect scrollBarSlider = pr->subControlRect(control, scrollBar, SC_ScrollBarSlider, widget);
+ QRect scrollBarGroove = pr->subControlRect(control, scrollBar, SC_ScrollBarGroove, widget);
+
+ int padding = Ph::dpiScaled(4);
+ scrollBarSlider.setX(scrollBarSlider.x() + padding);
+ scrollBarSlider.setY(scrollBarSlider.y() + padding);
+ // Width and height should be reduced by 2 * padding, but somehow padding is enough.
+ scrollBarSlider.setWidth(scrollBarSlider.width() - padding);
+ scrollBarSlider.setHeight(scrollBarSlider.height() - padding);
+
+ // Groove/gutter/trench area
+ if (scrollBar->subControls & SC_ScrollBarGroove) {
+ painter->fillRect(scrollBarGroove, swatch.color(S_window));
+ }
+
+ // Slider thumb
+ if (scrollBar->subControls & SC_ScrollBarSlider) {
+ qreal radius =
+ (scrollBar->orientation == Qt::Horizontal ? scrollBarSlider.height() : scrollBarSlider.width()) / 2.0;
+ painter->fillRect(scrollBarSlider, swatch.color(S_window));
+ Ph::paintSolidRoundRect(painter, scrollBarSlider, radius, swatch, S_scrollbarSlider);
+ }
+
+ // The SubLine (up/left) buttons
+ if (scrollBar->subControls & SC_ScrollBarSubLine) {
+ painter->fillRect(scrollBarSubLine, swatch.color(S_window));
+ }
+
+ // The AddLine (down/right) button
+ if (scrollBar->subControls & SC_ScrollBarAddLine) {
+ painter->fillRect(scrollBarAddLine, swatch.color(S_window));
+ }
+ break;
+ }
+ case CC_ComboBox: {
+ auto comboBox = qstyleoption_cast<const QStyleOptionComboBox*>(option);
+ if (!comboBox)
+ break;
+ painter->save();
+ bool isLeftToRight = option->direction != Qt::RightToLeft;
+ bool hasFocus = option->state & State_HasFocus && option->state & State_KeyboardFocusChange;
+ bool isSunken = comboBox->state & State_Sunken;
+ QRect rect = comboBox->rect;
+ QRect downArrowRect = proxy()->subControlRect(CC_ComboBox, comboBox, SC_ComboBoxArrow, widget);
+ // Draw a line edit
+ if (comboBox->editable) {
+ Swatchy buttonFill = isSunken ? S_button_pressed : S_button;
+ // if (!hasOptions)
+ // buttonFill = S_window;
+ painter->fillRect(rect, swatch.color(buttonFill));
+ if (comboBox->frame) {
+ QStyleOptionFrame buttonOption;
+ buttonOption.QStyleOption::operator=(*comboBox);
+ buttonOption.rect = rect;
+ buttonOption.state =
+ (comboBox->state & (State_Enabled | State_MouseOver | State_HasFocus)) | State_KeyboardFocusChange;
+ if (isSunken) {
+ buttonOption.state |= State_Sunken;
+ buttonOption.state &= ~State_MouseOver;
+ }
+ proxy()->drawPrimitive(PE_FrameLineEdit, &buttonOption, painter, widget);
+ QRect fr = proxy()->subControlRect(CC_ComboBox, option, SC_ComboBoxEditField, widget);
+ QRect br = rect;
+ if (isLeftToRight) {
+ br.setLeft(fr.x() + fr.width());
+ } else {
+ br.setRight(fr.left() - 1);
+ }
+ Qt::Edge edge = isLeftToRight ? Qt::LeftEdge : Qt::RightEdge;
+ Swatchy color = hasFocus ? S_highlight_outline : S_window_outline;
+ br.adjust(0, 1, 0, -1);
+ Ph::fillRectEdges(painter, br, edge, 1, swatch.color(color));
+ br.adjust(1, 0, -1, 0);
+ Swatchy specular = isSunken ? S_button_pressed_specular : S_button_specular;
+ Ph::fillRectOutline(painter, br, 1, swatch.color(specular));
+ }
+ } else {
+ QStyleOptionButton buttonOption;
+ buttonOption.QStyleOption::operator=(*comboBox);
+ buttonOption.rect = rect;
+ buttonOption.state =
+ comboBox->state
+ & (State_Enabled | State_MouseOver | State_HasFocus | State_Active | State_KeyboardFocusChange);
+ // Combo boxes should be shown to be keyboard interactive if they're
+ // focused at all, not just if the user has pressed tab to enter keyboard
+ // focus change mode. This is because the up/down arrows can, regardless
+ // of having pressed tab, control the combo box selection.
+ if (comboBox->state & State_HasFocus)
+ buttonOption.state |= State_KeyboardFocusChange;
+ if (isSunken) {
+ buttonOption.state |= State_Sunken;
+ buttonOption.state &= ~State_MouseOver;
+ }
+ proxy()->drawPrimitive(PE_PanelButtonCommand, &buttonOption, painter, widget);
+ }
+ if (comboBox->subControls & SC_ComboBoxArrow) {
+ int margin =
+ static_cast<int>(qMin(downArrowRect.width(), downArrowRect.height()) * Ph::ComboBox_ArrowMarginRatio);
+ QRect r = downArrowRect;
+ r.adjust(margin, margin, -margin, -margin);
+ // Draw the up/down arrow
+ Ph::drawArrow(painter, r, Qt::DownArrow, swatch);
+ }
+ painter->restore();
+ break;
+ }
+ case CC_Slider: {
+ auto slider = qstyleoption_cast<const QStyleOptionSlider*>(option);
+ if (!slider)
+ break;
+ const QRect groove = proxy()->subControlRect(CC_Slider, option, SC_SliderGroove, widget);
+ const QRect handle = proxy()->subControlRect(CC_Slider, option, SC_SliderHandle, widget);
+ bool horizontal = slider->orientation == Qt::Horizontal;
+ bool ticksAbove = slider->tickPosition & QSlider::TicksAbove;
+ bool ticksBelow = slider->tickPosition & QSlider::TicksBelow;
+ Swatchy outlineColor = S_window_outline;
+ if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange)
+ outlineColor = S_highlight_outline;
+ if ((option->subControls & SC_SliderGroove) && groove.isValid()) {
+ QRect g0 = groove;
+ if (g0.height() > 5)
+ g0.adjust(0, 1, 0, -1);
+ Ph::PSave saver(painter);
+ Swatchy gutterColor = option->state & State_Enabled ? S_scrollbarGutter : S_window;
+ Ph::paintBorderedRoundRect(painter, groove, Ph::SliderGroove_Rounding, swatch, outlineColor, gutterColor);
+ }
+ if (option->subControls & SC_SliderTickmarks) {
+ Ph::PSave save(painter);
+ painter->setPen(swatch.pen(S_window_outline));
+ int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget);
+ int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget);
+ int interval = slider->tickInterval;
+ if (interval <= 0) {
+ interval = slider->singleStep;
+ if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval, available)
+ - QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, 0, available)
+ < 3)
+ interval = slider->pageStep;
+ }
+ if (interval <= 0)
+ interval = 1;
+
+ int v = slider->minimum;
+ int len = proxy()->pixelMetric(PM_SliderLength, slider, widget);
+ while (v <= slider->maximum + 1) {
+ if (v == slider->maximum + 1 && interval == 1)
+ break;
+ const int v_ = qMin(v, slider->maximum);
+ int pos = sliderPositionFromValue(slider->minimum,
+ slider->maximum,
+ v_,
+ (horizontal ? slider->rect.width() : slider->rect.height()) - len,
+ slider->upsideDown)
+ + len / 2;
+ int extra = 2 - ((v_ == slider->minimum || v_ == slider->maximum) ? 1 : 0);
+
+ if (horizontal) {
+ if (ticksAbove) {
+ painter->drawLine(pos, slider->rect.top() + extra, pos, slider->rect.top() + tickSize);
+ }
+ if (ticksBelow) {
+ painter->drawLine(pos, slider->rect.bottom() - extra, pos, slider->rect.bottom() - tickSize);
+ }
+ } else {
+ if (ticksAbove) {
+ painter->drawLine(slider->rect.left() + extra, pos, slider->rect.left() + tickSize, pos);
+ }
+ if (ticksBelow) {
+ painter->drawLine(slider->rect.right() - extra, pos, slider->rect.right() - tickSize, pos);
+ }
+ }
+ // in the case where maximum is max int
+ int nextInterval = v + interval;
+ if (nextInterval < v)
+ break;
+ v = nextInterval;
+ }
+ }
+ // draw handle
+ if ((option->subControls & SC_SliderHandle)) {
+ bool isPressed = option->state & QStyle::State_Sunken && option->activeSubControls & SC_SliderHandle;
+ QRect r = handle;
+ Swatchy handleOutline, handleFill, handleSpecular;
+ if (option->state & State_HasFocus && option->state & State_KeyboardFocusChange) {
+ handleOutline = S_highlight_outline;
+ } else {
+ handleOutline = S_window_outline;
+ }
+ if (isPressed) {
+ handleFill = S_sliderHandle_pressed;
+ handleSpecular = S_sliderHandle_pressed_specular;
+ } else {
+ handleFill = S_sliderHandle;
+ handleSpecular = S_sliderHandle_specular;
+ }
+ Ph::PSave save(painter);
+ Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleOutline, handleFill);
+ r.adjust(1, 1, -1, -1);
+ Ph::paintBorderedRoundRect(painter, r, Ph::SliderHandle_Rounding, swatch, handleSpecular, S_none);
+ }
+ break;
+ }
+ case CC_ToolButton: {
+ auto tbopt = qstyleoption_cast<const QStyleOptionToolButton*>(option);
+ if (Ph::AllowToolBarAutoRaise || !tbopt || !widget || !widget->parent()
+ || !widget->parent()->inherits("QToolBar")) {
+ QCommonStyle::drawComplexControl(control, option, painter, widget);
+ break;
+ }
+ QStyleOptionToolButton opt_;
+ opt_.QStyleOptionToolButton::operator=(*tbopt);
+ opt_.state &= ~State_AutoRaise;
+ QCommonStyle::drawComplexControl(control, &opt_, painter, widget);
+ break;
+ }
+ case CC_Dial:
+ if (auto dial = qstyleoption_cast<const QStyleOptionSlider*>(option))
+ Ph::drawDial(dial, painter);
+ break;
+ default:
+ QCommonStyle::drawComplexControl(control, option, painter, widget);
+ break;
+ }
+}
+
+int BaseStyle::pixelMetric(PixelMetric metric, const QStyleOption* option, const QWidget* widget) const
+{
+ // Calculate pixel metrics.
+ // Use immediate return if value is not supposed to be dpi-scaled.
+ int val = -1;
+ switch (metric) {
+ case PM_SliderTickmarkOffset:
+ val = 6;
+ break;
+ case PM_ToolTipLabelFrameWidth:
+ case PM_HeaderMargin:
+ case PM_ButtonMargin:
+ case PM_SpinBoxFrameWidth:
+ val = Phantom::DefaultFrameWidth;
+ break;
+ case PM_ButtonDefaultIndicator:
+ case PM_ButtonShiftHorizontal:
+ val = 0;
+ break;
+ case PM_ButtonShiftVertical:
+ if (qobject_cast<const QToolButton*>(widget)) {
+ return 0;
+ }
+ val = 1;
+ break;
+ case PM_ComboBoxFrameWidth:
+ return 1;
+ case PM_DefaultFrameWidth:
+ // Original comment from fusion:
+ // Do not dpi-scale because the drawn frame is always exactly 1 pixel thick
+ // My note:
+ // I seriously doubt, with all of the hacky add-or-remove-1 things
+ // everywhere in fusion (and still in phantom), and the fact that fusion is
+ // totally broken in high dpi, that this actually holds true.
+ if (qobject_cast<const QAbstractItemView*>(widget)) {
+ return 1;
+ }
+ val = qMax(1, Phantom::DefaultFrameWidth - 2);
+ break;
+ case PM_MessageBoxIconSize:
+ val = 48;
+ break;
+ case PM_DialogButtonsSeparator:
+ case PM_ScrollBarSliderMin:
+ val = 26;
+ break;
+ case PM_TitleBarHeight:
+ val = 24;
+ break;
+ case PM_ScrollBarExtent:
+ val = 12;
+ break;
+ case PM_SliderThickness:
+ case PM_SliderLength:
+ val = 15;
+ break;
+ case PM_DockWidgetTitleMargin:
+ val = 1;
+ break;
+ case PM_MenuVMargin:
+ case PM_MenuHMargin:
+ case PM_MenuPanelWidth:
+ val = 0;
+ break;
+ case PM_MenuBarItemSpacing:
+ val = 0;
+ break;
+ case PM_MenuBarHMargin:
+ // option is usually nullptr, use widget instead to get font metrics
+ if (!Phantom::MenuBarLeftMargin || !widget) {
+ val = 0;
+ break;
+ }
+ return widget->fontMetrics().height() * Phantom::MenuBar_HorizontalPaddingFontRatio;
+ case PM_MenuBarVMargin:
+ case PM_MenuBarPanelWidth:
+ val = 0;
+ break;
+ case PM_ToolBarSeparatorExtent:
+ val = 9;
+ break;
+ case PM_ToolBarHandleExtent: {
+ int dotLen = Phantom::dpiScaled(2);
+ return dotLen * (3 * 2 - 1);
+ }
+ case PM_ToolBarItemSpacing:
+ val = 1;
+ break;
+ case PM_ToolBarFrameWidth:
+ val = Phantom::MenuBar_FrameWidth;
+ break;
+ case PM_ToolBarItemMargin:
+ val = 1;
+ break;
+ case PM_ToolBarExtensionExtent:
+ val = 32;
+ break;
+ case PM_ListViewIconSize:
+ case PM_SmallIconSize:
+ if (Phantom::ItemView_UseFontHeightForDecorationSize && widget
+ && qobject_cast<const QAbstractItemView*>(widget)) {
+ // QAbstractItemView::viewOptions() always uses nullptr for the
+ // styleoption when querying for PM_SmallIconSize. The best we can do is
+ // use the font set on the widget itself, which is obviously going to be
+ // wrong if the row has a custom font set on it. Hmm.
+ return widget->fontMetrics().height();
+ }
+ val = 16;
+ break;
+ case PM_ButtonIconSize: {
+ if (option)
+ return option->fontMetrics.height();
+ if (widget)
+ return widget->fontMetrics().height();
+ val = 16;
+ break;
+ }
+ case PM_DockWidgetTitleBarButtonMargin:
+ val = 2;
+ break;
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
+ case PM_TitleBarButtonSize:
+ val = 19;
+ break;
+#endif
+ case PM_MaximumDragDistance:
+ return -1; // Do not dpi-scale because the value is magic
+ case PM_TabCloseIndicatorWidth:
+ case PM_TabCloseIndicatorHeight:
+ val = 16;
+ break;
+ case PM_TabBarTabHSpace:
+ // Contents may clip out horizontally if we don't some extra pixels here or
+ // in sizeFromContents for CT_TabBarTab.
+ if (!option)
+ break;
+ return static_cast<int>(option->fontMetrics.height() * Phantom::TabBar_HPaddingFontRatio)
+ + static_cast<int>(Phantom::dpiScaled(4));
+ case PM_TabBarTabVSpace:
+ if (!option)
+ break;
+ return static_cast<int>(option->fontMetrics.height() * Phantom::TabBar_VPaddingFontRatio)
+ + static_cast<int>(Phantom::dpiScaled(2));
+ case PM_TabBarTabOverlap:
+ val = 1;
+ break;
+ case PM_TabBarBaseOverlap:
+ val = 2;
+ break;
+ case PM_TabBarIconSize: {
+ if (!widget)
+ break;
+ return widget->fontMetrics().height();
+ }
+ case PM_TabBarTabShiftVertical: {
+ val = Phantom::TabBar_InctiveVShift;
+ break;
+ }
+ case PM_SubMenuOverlap:
+ val = 0;
+ break;
+ case PM_DockWidgetHandleExtent:
+ case PM_SplitterWidth:
+ val = 5;
+ break;
+ case PM_IndicatorHeight:
+ case PM_IndicatorWidth:
+ case PM_ExclusiveIndicatorHeight:
+ case PM_ExclusiveIndicatorWidth:
+ if (option)
+ return option->fontMetrics.height();
+ if (widget)
+ return widget->fontMetrics().height();
+ val = 14;
+ break;
+ case PM_ScrollView_ScrollBarOverlap:
+ case PM_ScrollView_ScrollBarSpacing:
+ val = 0;
+ break;
+ case PM_TreeViewIndentation: {
+ if (widget)
+ return widget->fontMetrics().height();
+ val = 12;
+ break;
+ }
+ default:
+ val = QCommonStyle::pixelMetric(metric, option, widget);
+ }
+ return Phantom::dpiScaled(val);
+}
+
+QSize BaseStyle::sizeFromContents(ContentsType type,
+ const QStyleOption* option,
+ const QSize& size,
+ const QWidget* widget) const
+{
+ namespace Ph = Phantom;
+ // Cases which do not rely on the parent class to do any work
+ switch (type) {
+ case CT_RadioButton:
+ case CT_CheckBox: {
+ auto btn = qstyleoption_cast<const QStyleOptionButton*>(option);
+ if (!btn)
+ break;
+ bool isRadio = type == CT_RadioButton;
+ int w = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorWidth : PM_IndicatorWidth, btn, widget);
+ int h = proxy()->pixelMetric(isRadio ? PM_ExclusiveIndicatorHeight : PM_IndicatorHeight, btn, widget);
+ int margins = 0;
+ if (!btn->icon.isNull() || !btn->text.isEmpty())
+ margins =
+ proxy()->pixelMetric(isRadio ? PM_RadioButtonLabelSpacing : PM_CheckBoxLabelSpacing, option, widget);
+ return QSize(size.width() + w + margins, qMax(size.height(), h));
+ }
+ case CT_MenuBarItem: {
+ int fontHeight = option ? option->fontMetrics.height() : size.height();
+ int w = static_cast<int>(fontHeight * Ph::MenuBar_HorizontalPaddingFontRatio);
+ int h = static_cast<int>(fontHeight * Ph::MenuBar_VerticalPaddingFontRatio);
+ int line = Ph::dpiScaled(1);
+ return QSize(size.width() + w * 2, size.height() + h * 2 + line);
+ }
+ case CT_MenuItem: {
+ auto menuItem = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
+ if (!menuItem)
+ return size;
+ bool hasTabChar = menuItem->text.contains(QLatin1Char('\t'));
+ bool hasSubMenu = menuItem->menuItemType == QStyleOptionMenuItem::SubMenu;
+ bool isSeparator = menuItem->menuItemType == QStyleOptionMenuItem::Separator;
+ int fontMetricsHeight = -1;
+ // See notes at CE_MenuItem and SH_ComboBox_Popup for more information
+ if (Ph::UseQMenuForComboBoxPopup && qobject_cast<const QComboBox*>(widget)) {
+ if (!widget->testAttribute(Qt::WA_SetFont))
+ fontMetricsHeight = QFontMetrics(qApp->font("QMenu")).height();
+ }
+ if (fontMetricsHeight == -1) {
+ fontMetricsHeight = option->fontMetrics.height();
+ }
+ auto metrics = Ph::MenuItemMetrics::ofFontHeight(fontMetricsHeight);
+ // Incoming width is the sum of the visual widths of the main item text and
+ // the mnemonic text (if any). To this width we will add the widths of the
+ // other features for this menu item -- the icon/checkbox, spacing between
+ // icon/text/mnemonic, etc. For cases like separators without any text, we
+ // may disregard the width.
+ //
+ // Height is the text height, probably.
+ int w = size.width();
+ // Frame
+ w += metrics.frameThickness * 2;
+ // Left margins don't depend on whether or not we have a submenu arrow.
+ // Calculating the right margins requires knowing whether or not the menu
+ // item has a submenu arrow.
+ w += metrics.leftMargin;
+ // Phantom treats every menu item with the same space on the left for a
+ // check mark, even if it doesn't have the checkable property.
+ w += metrics.checkWidth + metrics.checkRightSpace;
+
+ if (!menuItem->icon.isNull()) {
+ // Phantom disregards any user-specified icon sizing at the moment.
+ w += metrics.fontHeight;
+ w += metrics.iconRightSpace;
+ }
+
+ // Tab character is used for separating the shortcut text
+ if (hasTabChar)
+ w += metrics.mnemonicSpace;
+ if (hasSubMenu)
+ w += metrics.arrowSpace + metrics.arrowWidth + metrics.rightMarginForArrow;
+ else
+ w += metrics.rightMarginForText;
+ int h;
+ if (isSeparator) {
+ h = metrics.separatorHeight;
+ } else {
+ h = metrics.totalHeight;
+ }
+ if (!menuItem->icon.isNull()) {
+ if (auto combo = qobject_cast<const QComboBox*>(widget)) {
+ h = qMax(combo->iconSize().height() + 2, h);
+ }
+ }
+ QSize sz;
+ sz.setWidth(qMax<int>(w, Ph::dpiScaled(Ph::MenuMinimumWidth)));
+ sz.setHeight(h);
+ return sz;
+ }
+ case CT_Menu: {
+ if (!Ph::MenuExtraBottomMargin || !option || !widget)
+ break;
+ // Trick the QMenu into putting a margin only at the bottom by adding extra
+ // height to the contents size. We only want to add this tricky space if
+ // there is at least more than 1 item in the menu.
+ const auto acts = widget->actions();
+ if (acts.count() < 2)
+ break;
+ // We only want to add the tricky space if there's at least 1 separator,
+ // otherwise it looks weird.
+ bool anySeps = false;
+ for (auto act : acts) {
+ if (act->isSeparator()) {
+ anySeps = true;
+ break;
+ }
+ }
+ if (!anySeps)
+ break;
+ int fheight = option->fontMetrics.height();
+ int vmargin = static_cast<int>(fheight * Ph::MenuItem_SeparatorHeightFontRatio) / 2;
+ QSize sz = size;
+ sz.setHeight(sz.height() + vmargin);
+ return sz;
+ }
+ case CT_TabBarTab: {
+ // Placeholder in case we change this in the future
+ return size;
+ }
+ case CT_Slider: {
+ QSize sz = size;
+ if (qobject_cast<const QSlider*>(widget)->orientation() == Qt::Horizontal) {
+ sz.setHeight(sz.height() + PM_SliderTickmarkOffset);
+ } else {
+ sz.setWidth(sz.width() + PM_SliderTickmarkOffset);
+ }
+ return sz;
+ }
+ case CT_GroupBox: {
+ // This doesn't seem to get used except once by QGroupBox for
+ // minimumSizeHint(). After that, the sizing/layout calculations seem to
+ // use the rects given by subControlRect().
+ auto opt = qstyleoption_cast<const QStyleOptionGroupBox*>(option);
+ if (!opt)
+ break;
+ // Checkbox and text height already accounted for, but margin between text
+ // and frame isn't.
+ int xadd = 0;
+ int yadd = 0;
+ if (opt->subControls & (SC_GroupBoxCheckBox | SC_GroupBoxLabel)) {
+ int fontHeight = option->fontMetrics.height();
+ yadd += static_cast<int>(fontHeight * Phantom::GroupBox_LabelBottomMarginFontRatio);
+ }
+ // We can test for the frame in general, but unfortunately testing to see
+ // if it's the 1-line "flat" style or 4-line box/rect "anything else" style
+ // doesn't seem to be possible here, only when painting.
+ if (opt->subControls & SC_GroupBoxFrame) {
+ xadd += 2;
+ yadd += 2;
+ }
+ return QSize(size.width() + xadd, size.height() + yadd);
+ }
+ case CT_ItemViewItem: {
+ auto vopt = qstyleoption_cast<const QStyleOptionViewItem*>(option);
+ if (!vopt)
+ break;
+ QSize sz = QCommonStyle::sizeFromContents(type, option, size, widget);
+ sz += QSize(0, Phantom::DefaultFrameWidth);
+ // QCommonStyle has a bunch of complicated logic for laying out/calculating
+ // rects of view items, which is locked behind a private data guy. In
+ // sizeFromContents for CT_ItemViewItem, it unions all of the item row's
+ // rects together and then, if the decoration height is exactly the same as
+ // the row height, it adds 2 pixels (not dpi scaled) to the height. The
+ // comment says it's to prevent "icons from overlapping" but I have no idea
+ // how that's supposed to help. And we don't necessarily want those extra 2
+ // pixels. Anyway, I don't want to copy and paste all of that code into
+ // Phantom and then maintain it. So when Phantom is in the mode where we're
+ // basing the item view decoration sizes off of the font size, we'll just
+ // take a guess when QCommonStyle has added 2 to the height (because the
+ // row height and decoration height are both the font height), and
+ // re-remove those two pixels.
+#if 1
+ if (Phantom::ItemView_UseFontHeightForDecorationSize) {
+ int fh = vopt->fontMetrics.height();
+ if (sz.height() == fh + 2 && vopt->decorationSize.height() == fh) {
+ sz.setHeight(fh);
+ }
+ }
+#endif
+ return sz;
+ }
+ case CT_HeaderSection: {
+ auto hdr = qstyleoption_cast<const QStyleOptionHeader*>(option);
+ if (!hdr)
+ break;
+ // This is pretty crummy. Should also check if we need multi-line support
+ // or not.
+ bool nullIcon = hdr->icon.isNull();
+ int margin = proxy()->pixelMetric(QStyle::PM_HeaderMargin, hdr, widget);
+ int iconSize = nullIcon ? 0 : option->fontMetrics.height();
+ QSize txt = hdr->fontMetrics.size(Qt::TextSingleLine | Qt::TextBypassShaping, hdr->text);
+ QSize sz;
+ sz.setHeight(margin + qMax(iconSize, txt.height()) + margin);
+ sz.setWidth((nullIcon ? 0 : margin) + iconSize + (hdr->text.isNull() ? 0 : margin) + txt.width() + margin);
+ if (hdr->sortIndicator != QStyleOptionHeader::None) {
+ if (hdr->orientation == Qt::Horizontal)
+ sz.rwidth() += sz.height() + margin;
+ else
+ sz.rheight() += sz.width() + margin;
+ }
+ return sz;
+ }
+ default:
+ break;
+ }
+
+ // Cases which modify the size given by the parent class
+ QSize newSize = QCommonStyle::sizeFromContents(type, option, size, widget);
+ switch (type) {
+ case CT_PushButton: {
+ auto pbopt = qstyleoption_cast<const QStyleOptionButton*>(option);
+ if (!pbopt || pbopt->text.isEmpty())
+ break;
+ int hpad = static_cast<int>(pbopt->fontMetrics.height() * Phantom::PushButton_HorizontalPaddingFontHeightRatio);
+ newSize.rwidth() += hpad * 2;
+ if (widget && qobject_cast<const QDialogButtonBox*>(widget->parent())) {
+ int dialogButtonMinWidth = Phantom::dpiScaled(80);
+ newSize.rwidth() = qMax(newSize.width(), dialogButtonMinWidth);
+ }
+ break;
+ }
+ case CT_ToolButton:
+#if defined(Q_OS_MACOS)
+ newSize += QSize(Ph::dpiScaled(6 + Phantom::DefaultFrameWidth), Ph::dpiScaled(6 + Phantom::DefaultFrameWidth));
+#elif defined(Q_OS_WIN)
+ newSize += QSize(Ph::dpiScaled(4 + Phantom::DefaultFrameWidth), Ph::dpiScaled(4 + Phantom::DefaultFrameWidth));
+#else
+ newSize += QSize(Ph::dpiScaled(3 + Phantom::DefaultFrameWidth), Ph::dpiScaled(3 + Phantom::DefaultFrameWidth));
+#endif
+ break;
+ case CT_ComboBox: {
+ newSize += QSize(0, Ph::dpiScaled(4 + Phantom::DefaultFrameWidth));
+ auto cb = qstyleoption_cast<const QStyleOptionComboBox*>(option);
+ // Non-editable combo boxes have some extra padding on the left side,
+ // similar to push buttons. We should account for that here to avoid text
+ // being clipped off.
+ if (cb) {
+ int pad = 0;
+ if (cb->editable) {
+ pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad);
+ } else {
+ pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad);
+ }
+ newSize.rwidth() += pad * 2;
+ }
+ break;
+ }
+ case CT_LineEdit: {
+ newSize += QSize(0, 4);
+ int pad = Ph::dpiScaled(Ph::LineEdit_ContentsHPad);
+ newSize.rwidth() += pad * 2;
+ break;
+ }
+ case CT_SpinBox:
+ // No changes needed
+ break;
+ case CT_SizeGrip:
+ newSize += QSize(4, 4);
+ break;
+ case CT_MdiControls:
+ newSize -= QSize(1, 0);
+ break;
+ default:
+ break;
+ }
+ return newSize;
+}
+
+void BaseStyle::polish(QApplication* app)
+{
+ if (!app) {
+ return;
+ }
+
+ Q_INIT_RESOURCE(styles);
+
+ QString stylesheet;
+ QFile baseStylesheetFile(":/styles/base/basestyle.qss");
+ if (baseStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ stylesheet = baseStylesheetFile.readAll();
+ baseStylesheetFile.close();
+ } else {
+ qWarning("Failed to load base theme stylesheet.");
+ }
+
+ stylesheet.append(getAppStyleSheet());
+ app->setStyleSheet(stylesheet);
+ QCommonStyle::polish(app);
+}
+
+QRect BaseStyle::subControlRect(ComplexControl control,
+ const QStyleOptionComplex* option,
+ SubControl subControl,
+ const QWidget* widget) const
+{
+ namespace Ph = Phantom;
+ QRect rect = QCommonStyle::subControlRect(control, option, subControl, widget);
+ switch (control) {
+ case CC_Slider: {
+ auto slider = qstyleoption_cast<const QStyleOptionSlider*>(option);
+ if (!slider)
+ break;
+ int tickSize = proxy()->pixelMetric(PM_SliderTickmarkOffset, option, widget);
+ switch (subControl) {
+ case SC_SliderHandle: {
+ if (slider->orientation == Qt::Horizontal) {
+ rect.setHeight(proxy()->pixelMetric(PM_SliderThickness));
+ rect.setWidth(proxy()->pixelMetric(PM_SliderLength));
+ int centerY = slider->rect.center().y() - rect.height() / 2;
+ if (slider->tickPosition & QSlider::TicksAbove)
+ centerY += tickSize;
+ if (slider->tickPosition & QSlider::TicksBelow)
+ centerY -= tickSize;
+ rect.moveTop(centerY);
+ } else {
+ rect.setWidth(proxy()->pixelMetric(PM_SliderThickness));
+ rect.setHeight(proxy()->pixelMetric(PM_SliderLength));
+ int centerX = slider->rect.center().x() - rect.width() / 2;
+ if (slider->tickPosition & QSlider::TicksAbove)
+ centerX += tickSize;
+ if (slider->tickPosition & QSlider::TicksBelow)
+ centerX -= tickSize;
+ rect.moveLeft(centerX);
+ }
+ break;
+ }
+ case SC_SliderGroove: {
+ QPoint grooveCenter = slider->rect.center();
+ const int grooveThickness = Ph::dpiScaled(7);
+ if (slider->orientation == Qt::Horizontal) {
+ rect.setHeight(grooveThickness);
+ if (slider->tickPosition & QSlider::TicksAbove)
+ grooveCenter.ry() += tickSize;
+ if (slider->tickPosition & QSlider::TicksBelow)
+ grooveCenter.ry() -= tickSize;
+ } else {
+ rect.setWidth(grooveThickness);
+ if (slider->tickPosition & QSlider::TicksAbove)
+ grooveCenter.rx() += tickSize;
+ if (slider->tickPosition & QSlider::TicksBelow)
+ grooveCenter.rx() -= tickSize;
+ }
+ rect.moveCenter(grooveCenter);
+ break;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ case CC_SpinBox: {
+ auto spinbox = qstyleoption_cast<const QStyleOptionSpinBox*>(option);
+ if (!spinbox)
+ break;
+ // Some leftover Fusion code here. Should clean up this mess.
+ int center = spinbox->rect.height() / 2;
+ int fw = spinbox->frame ? 1 : 0;
+ int y = fw;
+ const int buttonWidth = static_cast<int>(Ph::dpiScaled(Ph::SpinBox_ButtonWidth)) + 2;
+ int x, lx, rx;
+ x = spinbox->rect.width() - y - buttonWidth + 2;
+ lx = fw;
+ rx = x - fw;
+ switch (subControl) {
+ case SC_SpinBoxUp:
+ if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons)
+ return {};
+ rect = QRect(x, fw, buttonWidth, center - fw);
+ break;
+ case SC_SpinBoxDown:
+ if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons)
+ return QRect();
+
+ rect = QRect(x, center, buttonWidth, spinbox->rect.bottom() - center - fw + 1);
+ break;
+ case SC_SpinBoxEditField:
+ if (spinbox->buttonSymbols == QAbstractSpinBox::NoButtons) {
+ rect = QRect(lx, fw, spinbox->rect.width() - 2 * fw, spinbox->rect.height() - 2 * fw);
+ } else {
+ rect = QRect(lx, fw, rx - qMax(fw - 1, 0), spinbox->rect.height() - 2 * fw);
+ }
+ break;
+ case SC_SpinBoxFrame:
+ rect = spinbox->rect;
+ break;
+ default:
+ break;
+ }
+ rect = visualRect(spinbox->direction, spinbox->rect, rect);
+ break;
+ }
+ case CC_GroupBox: {
+ auto groupBox = qstyleoption_cast<const QStyleOptionGroupBox*>(option);
+ if (!groupBox)
+ break;
+ switch (subControl) {
+ case SC_GroupBoxFrame:
+ case SC_GroupBoxContents: {
+ QRect r = option->rect;
+ if (groupBox->subControls & (SC_GroupBoxLabel | SC_GroupBoxCheckBox)) {
+ int fontHeight = option->fontMetrics.height();
+ int topMargin = qMax(pixelMetric(PM_ExclusiveIndicatorHeight), fontHeight);
+ topMargin += static_cast<int>(fontHeight * Ph::GroupBox_LabelBottomMarginFontRatio);
+ r.setTop(r.top() + topMargin);
+ }
+ if (subControl == SC_GroupBoxContents && groupBox->subControls & SC_GroupBoxFrame) {
+ // Testing against groupBox->features for the frame type doesn't seem
+ // to work here.
+ r.adjust(1, 1, -1, -1);
+ }
+ return r;
+ }
+ case SC_GroupBoxCheckBox:
+ case SC_GroupBoxLabel: {
+ // Accurate height doesn't matter -- the other group box style
+ // implementations also fail with multi-line or too-tall text.
+ int textHeight = option->fontMetrics.height();
+ // width()/horizontalAdvance() is faster than size() and good enough for
+ // us, since we only support a single line of text here anyway.
+ int textWidth = Phantom::fontMetricsWidth(option->fontMetrics, groupBox->text);
+ int indicatorWidth = proxy()->pixelMetric(PM_IndicatorWidth, option, widget);
+ int indicatorHeight = proxy()->pixelMetric(PM_IndicatorHeight, option, widget);
+ int margin = 0;
+ int indicatorRightSpace = textHeight / 3;
+ int contentWidth = textWidth;
+ if (option->subControls & QStyle::SC_GroupBoxCheckBox) {
+ contentWidth += indicatorWidth + indicatorRightSpace;
+ }
+ int x = margin;
+ int y = 0;
+ switch (groupBox->textAlignment & Qt::AlignHorizontal_Mask) {
+ case Qt::AlignHCenter:
+ x += (option->rect.width() - contentWidth) / 2;
+ break;
+ case Qt::AlignRight:
+ x += option->rect.width() - contentWidth;
+ break;
+ default:
+ break;
+ }
+ int w, h;
+ if (subControl == SC_GroupBoxCheckBox) {
+ w = indicatorWidth;
+ h = indicatorHeight;
+ if (textHeight > indicatorHeight) {
+ y = (textHeight - indicatorHeight) / 2;
+ }
+ } else {
+ w = contentWidth;
+ h = textHeight;
+ if (option->subControls & QStyle::SC_GroupBoxCheckBox) {
+ x += indicatorWidth + indicatorRightSpace;
+ w -= indicatorWidth + indicatorRightSpace;
+ }
+ }
+ return visualRect(option->direction, option->rect, QRect(x, y, w, h));
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ case CC_ComboBox: {
+ auto cb = qstyleoption_cast<const QStyleOptionComboBox*>(option);
+ if (!cb)
+ return QRect();
+ int frame = cb->frame ? proxy()->pixelMetric(PM_ComboBoxFrameWidth, cb, widget) : 0;
+ QRect r = option->rect;
+ r.adjust(frame, frame, -frame, -frame);
+ int dim = qMin(r.width(), r.height());
+ if (dim < 1)
+ return QRect();
+ switch (subControl) {
+ case SC_ComboBoxFrame:
+ return cb->rect;
+ case SC_ComboBoxArrow: {
+ QRect r0 = r;
+ r0.setX((r0.x() + r0.width()) - dim + 1);
+ return visualRect(option->direction, option->rect, r0);
+ }
+ case SC_ComboBoxEditField: {
+ // Add extra padding if not editable
+ int pad = 0;
+ if (cb->editable) {
+ // Line edit padding already added
+ } else {
+ pad = Ph::dpiScaled(Ph::ComboBox_NonEditable_ContentsHPad);
+ }
+ r.adjust(pad, 0, -dim, 0);
+ return visualRect(option->direction, option->rect, r);
+ }
+ case SC_ComboBoxListBoxPopup: {
+ return cb->rect;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ case CC_TitleBar: {
+ auto tb = qstyleoption_cast<const QStyleOptionTitleBar*>(option);
+ if (!tb)
+ break;
+ SubControl sc = subControl;
+ QRect& ret = rect;
+ const int indent = 3;
+ const int controlTopMargin = 3;
+ const int controlBottomMargin = 3;
+ const int controlWidthMargin = 2;
+ const int controlHeight = tb->rect.height() - controlTopMargin - controlBottomMargin;
+ const int delta = controlHeight + controlWidthMargin;
+ int offset = 0;
+ bool isMinimized = tb->titleBarState & Qt::WindowMinimized;
+ bool isMaximized = tb->titleBarState & Qt::WindowMaximized;
+ switch (sc) {
+ case SC_TitleBarLabel:
+ if (tb->titleBarFlags & (Qt::WindowTitleHint | Qt::WindowSystemMenuHint)) {
+ ret = tb->rect;
+ if (tb->titleBarFlags & Qt::WindowSystemMenuHint)
+ ret.adjust(delta, 0, -delta, 0);
+ if (tb->titleBarFlags & Qt::WindowMinimizeButtonHint)
+ ret.adjust(0, 0, -delta, 0);
+ if (tb->titleBarFlags & Qt::WindowMaximizeButtonHint)
+ ret.adjust(0, 0, -delta, 0);
+ if (tb->titleBarFlags & Qt::WindowShadeButtonHint)
+ ret.adjust(0, 0, -delta, 0);
+ if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint)
+ ret.adjust(0, 0, -delta, 0);
+ }
+ break;
+ case SC_TitleBarContextHelpButton:
+ if (tb->titleBarFlags & Qt::WindowContextHelpButtonHint)
+ offset += delta;
+ Q_FALLTHROUGH();
+ case SC_TitleBarMinButton:
+ if (!isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint))
+ offset += delta;
+ else if (sc == SC_TitleBarMinButton)
+ break;
+ Q_FALLTHROUGH();
+ case SC_TitleBarNormalButton:
+ if (isMinimized && (tb->titleBarFlags & Qt::WindowMinimizeButtonHint))
+ offset += delta;
+ else if (isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint))
+ offset += delta;
+ else if (sc == SC_TitleBarNormalButton)
+ break;
+ Q_FALLTHROUGH();
+ case SC_TitleBarMaxButton:
+ if (!isMaximized && (tb->titleBarFlags & Qt::WindowMaximizeButtonHint))
+ offset += delta;
+ else if (sc == SC_TitleBarMaxButton)
+ break;
+ Q_FALLTHROUGH();
+ case SC_TitleBarShadeButton:
+ if (!isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint))
+ offset += delta;
+ else if (sc == SC_TitleBarShadeButton)
+ break;
+ Q_FALLTHROUGH();
+ case SC_TitleBarUnshadeButton:
+ if (isMinimized && (tb->titleBarFlags & Qt::WindowShadeButtonHint))
+ offset += delta;
+ else if (sc == SC_TitleBarUnshadeButton)
+ break;
+ Q_FALLTHROUGH();
+ case SC_TitleBarCloseButton:
+ if (tb->titleBarFlags & Qt::WindowSystemMenuHint)
+ offset += delta;
+ else if (sc == SC_TitleBarCloseButton)
+ break;
+ ret.setRect(
+ tb->rect.right() - indent - offset, tb->rect.top() + controlTopMargin, controlHeight, controlHeight);
+ break;
+ case SC_TitleBarSysMenu:
+ if (tb->titleBarFlags & Qt::WindowSystemMenuHint) {
+ ret.setRect(tb->rect.left() + controlWidthMargin + indent,
+ tb->rect.top() + controlTopMargin,
+ controlHeight,
+ controlHeight);
+ }
+ break;
+ default:
+ break;
+ }
+ ret = visualRect(tb->direction, tb->rect, ret);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return rect;
+}
+
+QRect BaseStyle::itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const
+{
+ return QCommonStyle::itemPixmapRect(r, flags, pixmap);
+}
+
+void BaseStyle::drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const
+{
+ QCommonStyle::drawItemPixmap(painter, rect, alignment, pixmap);
+}
+
+QStyle::SubControl BaseStyle::hitTestComplexControl(ComplexControl cc,
+ const QStyleOptionComplex* opt,
+ const QPoint& pt,
+ const QWidget* w) const
+{
+ return QCommonStyle::hitTestComplexControl(cc, opt, pt, w);
+}
+
+QPixmap BaseStyle::generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const
+{
+ // Default icon highlight is way too subtle
+ if (iconMode == QIcon::Selected) {
+ QImage img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
+ QPainter painter(&img);
+
+ painter.setCompositionMode(QPainter::CompositionMode_SourceAtop);
+
+ QColor color =
+ Phantom::DeriveColors::adjustLightness(opt->palette.color(QPalette::Normal, QPalette::Highlight), .25);
+ color.setAlphaF(0.25);
+ painter.fillRect(0, 0, img.width(), img.height(), color);
+
+ painter.end();
+
+ return QPixmap::fromImage(img);
+ }
+ return QCommonStyle::generatedIconPixmap(iconMode, pixmap, opt);
+}
+
+int BaseStyle::styleHint(StyleHint hint,
+ const QStyleOption* option,
+ const QWidget* widget,
+ QStyleHintReturn* returnData) const
+{
+ switch (hint) {
+ case SH_Slider_SnapToValue:
+ case SH_PrintDialog_RightAlignButtons:
+ case SH_FontDialog_SelectAssociatedText:
+ case SH_ComboBox_ListMouseTracking:
+ case SH_Slider_StopMouseOverSlider:
+ case SH_ScrollBar_MiddleClickAbsolutePosition:
+ case SH_TitleBar_AutoRaise:
+ case SH_TitleBar_NoBorder:
+ case SH_ItemView_ArrowKeysNavigateIntoChildren:
+ case SH_ItemView_ChangeHighlightOnFocus:
+ case SH_MenuBar_MouseTracking:
+ case SH_Menu_MouseTracking:
+ return 1;
+ case SH_Menu_SupportsSections:
+ return 0;
+#ifndef Q_OS_MAC
+ case SH_MenuBar_AltKeyNavigation:
+ return 1;
+#endif
+#if defined(QT_PLATFORM_UIKIT)
+ case SH_ComboBox_UseNativePopup:
+ return 1;
+#endif
+ case SH_ItemView_ShowDecorationSelected:
+ // QWindowsStyle does this as well -- QCommonStyle seems to have some
+ // internal confusion buried within its private implementation of laying
+ // out and drawing item views where it can't keep track of what's
+ // considered a decoration and what's not. For tree views, if you give 0
+ // for ShowDecorationSelected, it applies only to the disclosure indicator
+ // and not to the QIcon/pixmap that might be present for the item. So
+ // selecting an item in a tree view will have the selection color drawn
+ // underneath the icon/pixmap, but not the disclosure indicator. However,
+ // in list views, if you give 0 for ShowDecorationSelected, it will *not*
+ // draw the selection color underneath the icon/pixmap. There's no way to
+ // access this internal logic in QCommonStyle without fully reimplementing
+ // the huge mass of stuff for item view layout and drawing. Therefore, the
+ // best we can do is at least try to get consistent behavior: if it's a
+ // list view, just always return 1 for ShowDecorationSelected.
+ if (!Phantom::ShowItemViewDecorationSelected && qobject_cast<const QListView*>(widget))
+ return 1;
+ return Phantom::ShowItemViewDecorationSelected;
+ case SH_ItemView_MovementWithoutUpdatingSelection:
+ return 1;
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
+ case SH_ItemView_ScrollMode:
+ return QAbstractItemView::ScrollPerPixel;
+#endif
+ case SH_ScrollBar_ContextMenu:
+#ifdef Q_OS_MAC
+ return 0;
+#else
+ return 1;
+#endif
+ // Some Linux distros might want to enable this, but it doesn't behave very
+ // consistently with varied QPalettes, depending on how the QPA and icons
+ // deal with both light and dark themes. It might seem weird to just disable
+ // this, but none of (Mac, Windows, BeOS/Haiku) show icons in dialog buttons,
+ // and the results on Linux are generally pretty messy -- not sure why it's
+ // historically been the default, especially when other button types
+ // generally don't have any icons.
+ case SH_DialogButtonBox_ButtonsHaveIcons:
+ return 0;
+ case SH_ScrollBar_Transient:
+ return 1;
+ case SH_EtchDisabledText:
+ case SH_DitherDisabledText:
+ case SH_ToolBox_SelectedPageTitleBold:
+ case SH_Menu_AllowActiveAndDisabled:
+ case SH_MainWindow_SpaceBelowMenuBar:
+ case SH_MessageBox_CenterButtons:
+ case SH_RubberBand_Mask:
+ case SH_ScrollView_FrameOnlyAroundContents:
+ return 0;
+ case SH_ComboBox_Popup: {
+ return Phantom::UseQMenuForComboBoxPopup;
+ // Fusion did this, but we don't because of font bugs (especially in high
+ // DPI) with the QMenu that the combo box will create instead of a dropdown
+ // view. See notes in CE_MenuItem for more details.
+ if (auto cmb = qstyleoption_cast<const QStyleOptionComboBox*>(option))
+ return !cmb->editable;
+ return 0;
+ }
+ case SH_Table_GridLineColor: {
+ using namespace Phantom::SwatchColors;
+ namespace Ph = Phantom;
+ if (!option)
+ return 0;
+ auto ph_swatchPtr = Ph::getCachedSwatchOfQPalette(&d->swatchCache, &d->headSwatchFastKey, option->palette);
+ const Ph::PhSwatch& swatch = *ph_swatchPtr.data();
+ // Qt code in table views for drawing grid lines is broken. See case for
+ // CE_ItemViewItem painting for more information.
+ return static_cast<int>(swatch.color(S_base_divider).rgb());
+ }
+ case SH_MessageBox_TextInteractionFlags:
+ return Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse;
+ case SH_WizardStyle:
+ return QWizard::ClassicStyle;
+ case SH_Menu_SubMenuPopupDelay:
+ // Returning 0 will break sloppy submenus even if they're enabled
+ return 10;
+ case SH_Menu_SloppySubMenus:
+ return true;
+ case SH_Menu_SubMenuSloppyCloseTimeout:
+ return 500;
+ case SH_Menu_SubMenuDontStartSloppyOnLeave:
+ return 1;
+ case SH_Menu_SubMenuSloppySelectOtherActions:
+ return 1;
+ case SH_Menu_SubMenuUniDirection:
+ return 1;
+ case SH_Menu_SubMenuUniDirectionFailCount:
+ return 1;
+ case SH_Menu_SubMenuResetWhenReenteringParent:
+ return 0;
+#ifdef Q_OS_MAC
+ case SH_Menu_FlashTriggeredItem:
+ return 1;
+ case SH_Menu_FadeOutOnHide:
+ return 0;
+#endif
+ case SH_WindowFrame_Mask:
+ return 0;
+ case SH_UnderlineShortcut: {
+ return false;
+ }
+ case SH_Widget_Animate:
+ return 1;
+ default:
+ break;
+ }
+ return QCommonStyle::styleHint(hint, option, widget, returnData);
+}
+
+QRect BaseStyle::subElementRect(SubElement sr, const QStyleOption* opt, const QWidget* w) const
+{
+ switch (sr) {
+ case SE_ProgressBarLabel:
+ case SE_ProgressBarContents:
+ case SE_ProgressBarGroove:
+ return opt->rect;
+ case SE_PushButtonFocusRect: {
+ QRect r = QCommonStyle::subElementRect(sr, opt, w);
+ r.adjust(0, 1, 0, -1);
+ return r;
+ }
+ case SE_DockWidgetTitleBarText: {
+ auto titlebar = qstyleoption_cast<const QStyleOptionDockWidget*>(opt);
+ if (!titlebar)
+ break;
+ QRect r = QCommonStyle::subElementRect(sr, opt, w);
+ bool verticalTitleBar = titlebar->verticalTitleBar;
+ if (verticalTitleBar) {
+ r.adjust(0, 0, 0, -4);
+ } else {
+ if (opt->direction == Qt::LeftToRight)
+ r.adjust(4, 0, 0, 0);
+ else
+ r.adjust(0, 0, -4, 0);
+ }
+ return r;
+ }
+ case SE_TreeViewDisclosureItem: {
+ if (Phantom::BranchesOnEdge) {
+ // Shove it all the way to the left (or right) side, probably outside of
+ // the rect it gave us. Old-school.
+ QRect rect = opt->rect;
+ if (opt->direction != Qt::RightToLeft) {
+ rect.moveLeft(0);
+ if (rect.width() < rect.height())
+ rect.setWidth(rect.height());
+ } else {
+ // todo
+ }
+ return rect;
+ }
+ break;
+ }
+ case SE_LineEditContents: {
+ QRect r = QCommonStyle::subElementRect(sr, opt, w);
+ int pad = Phantom::dpiScaled(Phantom::LineEdit_ContentsHPad);
+ return r.adjusted(pad, 0, -pad, 0);
+ }
+ default:
+ break;
+ }
+ return QCommonStyle::subElementRect(sr, opt, w);
+}
+
+// Table header layout reference
+// -----------------------------
+//
+// begin: QStyleOptionHeader::Beginning;
+// mid: QStyleOptionHeader::Middle;
+// end: QStyleOptionHeader::End;
+// one: QStyleOptionHeader::OnlyOneSection;
+// one*:
+// This is specified as QStyleOptionHeader::OnlyOneSection, but the call to
+// drawControl(CE_HeaderSection...) is being performed by an instance of
+// QTableCornerButton, defined in qtableview.cpp as a subclass of
+// QAbstractButton. Only table views can have these corner buttons, and they
+// only appear if there are both at least 1 column and 1 row visible.
+//
+// Configuration A: A table view with both columns and rows
+//
+// Configuration B: A list view, or a tree view, or a table view with no rows
+// in the data or all rows hidden, such that the corner button is also made
+// hidden.
+//
+// Configuration C: A table view with no columns in the data or all columns
+// hidden, such that the corner button is also made hidden.
+//
+// Configuration A, Left-to-right, 4x4
+// [ one* ][ begin ][ mid ][ mid ][ end ]
+// [ begin ]
+// [ mid ]
+// [ mid ]
+// [ end ]
+//
+// Configuration A, Left-to-right, 2x2
+// [ one* ][ begin ][ end ]
+// [ begin ]
+// [ end ]
+//
+// Configuration A, Left-to-right, 1x1
+// [ one* ][ one ]
+// [ one ]
+//
+// Configuration A, Right-to-left, 4x4
+// [ begin ][ mid ][ mid ][ end ][ one* ]
+// [ begin ]
+// [ mid ]
+// [ mid ]
+// [ end ]
+//
+// Configuration A, Right-to-left, 2x2
+// [ begin ][ end ][ one* ]
+// [ begin ]
+// [ end ]
+//
+// Configuration A, Right-to-left, 1x1
+// [ one ][ one* ]
+// [ one ]
+//
+// Configuration B, Left-to-right and right-to-left, 4 columns (table view:
+// 4 columns with 0 rows, list/tree view: 4 columns, rows count doesn't matter):
+// [ begin ][ mid ][ mid ][ end ]
+//
+// Configuration B, Left-to-right and right-to-left, 2 columns (table view:
+// 2 columns with 0 rows, list/tree view: 2 columns, rows count doesn't matter):
+// [ begin ][ end ]
+//
+// Configuration B, Left-to-right and right-to-left, 1 column (table view:
+// 1 column with 0 rows, list view: 1 column, rows count doesn't matter):
+// [ one ]
+//
+// Configuration C, left-to-right and right-to-left, table view with no columns
+// and 4 rows:
+// [ begin ]
+// [ mid ]
+// [ mid ]
+// [ end ]
+//
+// Configuration C, left-to-right and right-to-left, table view with no columns
+// and 2 rows:
+// [ begin ]
+// [ end ]
+//
+// Configuration C, left-to-right and right-to-left, table view with no columns
+// and 1 row:
+// [ one ]
diff --git a/src/gui/styles/base/BaseStyle.h b/src/gui/styles/base/BaseStyle.h
new file mode 100644
index 000000000..d6269fad7
--- /dev/null
+++ b/src/gui/styles/base/BaseStyle.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ * Copyright (C) 2019 Andrew Richards
+ *
+ * Derived from Phantomstyle and relicensed under the GPLv2 or v3.
+ * https://github.com/randrew/phantomstyle
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_BASESTYLE_H
+#define KEEPASSXC_BASESTYLE_H
+
+#include <QCommonStyle>
+
+class BaseStylePrivate;
+
+class BaseStyle : public QCommonStyle
+{
+ Q_OBJECT
+
+public:
+ BaseStyle();
+ ~BaseStyle() override;
+
+ enum PhantomPrimitiveElement
+ {
+ Phantom_PE_IndicatorTabNew = PE_CustomBase + 1,
+ Phantom_PE_ScrollBarSliderVertical,
+ Phantom_PE_WindowFrameColor,
+ };
+
+ QPalette standardPalette() const override;
+ void drawPrimitive(PrimitiveElement elem,
+ const QStyleOption* option,
+ QPainter* painter,
+ const QWidget* widget = nullptr) const override;
+ void
+ drawControl(ControlElement ce, const QStyleOption* option, QPainter* painter, const QWidget* widget) const override;
+ int pixelMetric(PixelMetric metric,
+ const QStyleOption* option = nullptr,
+ const QWidget* widget = nullptr) const override;
+ void drawComplexControl(ComplexControl control,
+ const QStyleOptionComplex* option,
+ QPainter* painter,
+ const QWidget* widget) const override;
+ QRect subElementRect(SubElement r, const QStyleOption* opt, const QWidget* widget = nullptr) const override;
+ QSize sizeFromContents(ContentsType type,
+ const QStyleOption* option,
+ const QSize& size,
+ const QWidget* widget) const override;
+ SubControl hitTestComplexControl(ComplexControl cc,
+ const QStyleOptionComplex* opt,
+ const QPoint& pt,
+ const QWidget* w = nullptr) const override;
+ QRect subControlRect(ComplexControl cc,
+ const QStyleOptionComplex* opt,
+ SubControl sc,
+ const QWidget* widget) const override;
+ QPixmap generatedIconPixmap(QIcon::Mode iconMode, const QPixmap& pixmap, const QStyleOption* opt) const override;
+ int styleHint(StyleHint hint,
+ const QStyleOption* option = nullptr,
+ const QWidget* widget = nullptr,
+ QStyleHintReturn* returnData = nullptr) const override;
+ QRect itemPixmapRect(const QRect& r, int flags, const QPixmap& pixmap) const override;
+ void drawItemPixmap(QPainter* painter, const QRect& rect, int alignment, const QPixmap& pixmap) const override;
+ void drawItemText(QPainter* painter,
+ const QRect& rect,
+ int flags,
+ const QPalette& pal,
+ bool enabled,
+ const QString& text,
+ QPalette::ColorRole textRole = QPalette::NoRole) const override;
+
+ using QCommonStyle::polish;
+ void polish(QApplication* app) override;
+
+protected:
+ /**
+ * @return Paths to application stylesheets
+ */
+ virtual QString getAppStyleSheet() const
+ {
+ return {};
+ }
+
+ BaseStylePrivate* d;
+};
+
+#endif
diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss
new file mode 100644
index 000000000..597a5b9e0
--- /dev/null
+++ b/src/gui/styles/base/basestyle.qss
@@ -0,0 +1,48 @@
+QPushButton:default {
+ background: palette(highlight);
+ color: palette(highlighted-text);
+}
+
+QSpinBox {
+ min-width: 90px;
+}
+
+QDialogButtonBox QPushButton {
+ min-width: 55px;
+}
+
+QCheckBox, QRadioButton {
+ spacing: 10px;
+}
+
+DatabaseWidget, GroupView {
+ background-color: palette(window);
+ border: none;
+}
+
+EntryPreviewWidget QLineEdit, EntryPreviewWidget QTextEdit {
+ background-color: palette(window);
+ border: none;
+}
+
+DatabaseOpenWidget #loginFrame {
+ border: 2px groove palette(mid);
+ background: palette(light);
+}
+
+QGroupBox {
+ margin-top: 1.4em;
+ margin-bottom: 1.4em;
+ font-weight: bold;
+}
+
+QGroupBox::title {
+ margin-top: -3.35em;
+ margin-left: -.4em;
+ subcontrol-origin: padding;
+}
+
+QToolTip {
+ border: none;
+ padding: 3px;
+}
diff --git a/src/gui/styles/base/phantomcolor.cpp b/src/gui/styles/base/phantomcolor.cpp
new file mode 100644
index 000000000..3689cfc3f
--- /dev/null
+++ b/src/gui/styles/base/phantomcolor.cpp
@@ -0,0 +1,423 @@
+/*
+ * HSLuv-C: Human-friendly HSL
+ * <http://github.com/hsluv/hsluv-c>
+ * <http://www.hsluv.org/>
+ *
+ * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation)
+ * Copyright (c) 2015 Roger Tallada (Obj-C implementation)
+ * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "phantomcolor.h"
+#include <cfloat>
+#include <cmath>
+
+namespace Phantom
+{
+ namespace
+ {
+
+ // Th`ese declarations originate from hsluv.h, from the hsluv-c library. The
+ // hpluv functions have been removed, as they are unnecessary for Phantom.
+ /**
+ * Convert HSLuv to RGB.
+ *
+ * @param h Hue. Between 0.0 and 360.0.
+ * @param s Saturation. Between 0.0 and 100.0.
+ * @param l Lightness. Between 0.0 and 100.0.
+ * @param[out] pr Red component. Between 0.0 and 1.0.
+ * @param[out] pr Green component. Between 0.0 and 1.0.
+ * @param[out] pr Blue component. Between 0.0 and 1.0.
+ */
+ void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb);
+
+ /**
+ * Convert RGB to HSLuv.
+ *
+ * @param r Red component. Between 0.0 and 1.0.
+ * @param g Green component. Between 0.0 and 1.0.
+ * @param b Blue component. Between 0.0 and 1.0.
+ * @param[out] ph Hue. Between 0.0 and 360.0.
+ * @param[out] ps Saturation. Between 0.0 and 100.0.
+ * @param[out] pl Lightness. Between 0.0 and 100.0.
+ */
+ void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl);
+
+ // Contents below originate from hsluv.c from the hsluv-c library. They have
+ // been wrapped in a C++ namespace to avoid collisions and to reduce the
+ // translation unit count, and hsluv's own sRGB conversion code has been
+ // stripped out (sRGB conversion is now performed in the Phantom color code
+ // when going to/from the Rgb type.)
+ //
+ // If you need to update the hsluv-c code, be mindful of the removed sRGB
+ // conversions -- you will need to make similar modifications to the upstream
+ // hsluv-c code. Also note that that the hpluv (pastel) functions have been
+ // removed, as they are not used in Phantom.
+ typedef struct Triplet_tag Triplet;
+ struct Triplet_tag
+ {
+ double a;
+ double b;
+ double c;
+ };
+
+ /* for RGB */
+ const Triplet m[3] = {{3.24096994190452134377, -1.53738317757009345794, -0.49861076029300328366},
+ {-0.96924363628087982613, 1.87596750150772066772, 0.04155505740717561247},
+ {0.05563007969699360846, -0.20397695888897656435, 1.05697151424287856072}};
+
+ /* for XYZ */
+ const Triplet m_inv[3] = {{0.41239079926595948129, 0.35758433938387796373, 0.18048078840183428751},
+ {0.21263900587151035754, 0.71516867876775592746, 0.07219231536073371500},
+ {0.01933081871559185069, 0.11919477979462598791, 0.95053215224966058086}};
+
+ const double ref_u = 0.19783000664283680764;
+ const double ref_v = 0.46831999493879100370;
+
+ const double kappa = 903.29629629629629629630;
+ const double epsilon = 0.00885645167903563082;
+
+ typedef struct Bounds_tag Bounds;
+ struct Bounds_tag
+ {
+ double a;
+ double b;
+ };
+
+ void get_bounds(double l, Bounds bounds[6])
+ {
+ double tl = l + 16.0;
+ double sub1 = (tl * tl * tl) / 1560896.0;
+ double sub2 = (sub1 > epsilon ? sub1 : (l / kappa));
+ int channel;
+ int t;
+
+ for (channel = 0; channel < 3; channel++) {
+ double m1 = m[channel].a;
+ double m2 = m[channel].b;
+ double m3 = m[channel].c;
+
+ for (t = 0; t < 2; t++) {
+ double top1 = (284517.0 * m1 - 94839.0 * m3) * sub2;
+ double top2 = (838422.0 * m3 + 769860.0 * m2 + 731718.0 * m1) * l * sub2 - 769860.0 * t * l;
+ double bottom = (632260.0 * m3 - 126452.0 * m2) * sub2 + 126452.0 * t;
+
+ bounds[channel * 2 + t].a = top1 / bottom;
+ bounds[channel * 2 + t].b = top2 / bottom;
+ }
+ }
+ }
+
+ double ray_length_until_intersect(double theta, const Bounds* line)
+ {
+ return line->b / (sin(theta) - line->a * cos(theta));
+ }
+
+ double max_chroma_for_lh(double l, double h)
+ {
+ double min_len = DBL_MAX;
+ double hrad = h * 0.01745329251994329577; /* (2 * pi / 360) */
+ Bounds bounds[6];
+ int i;
+
+ get_bounds(l, bounds);
+ for (i = 0; i < 6; i++) {
+ double len = ray_length_until_intersect(hrad, &bounds[i]);
+
+ if (len >= 0 && len < min_len)
+ min_len = len;
+ }
+ return min_len;
+ }
+
+ double dot_product(const Triplet* t1, const Triplet* t2)
+ {
+ return (t1->a * t2->a + t1->b * t2->b + t1->c * t2->c);
+ }
+
+ void xyz2rgb(Triplet* in_out)
+ {
+ double r = dot_product(&m[0], in_out);
+ double g = dot_product(&m[1], in_out);
+ double b = dot_product(&m[2], in_out);
+ in_out->a = r;
+ in_out->b = g;
+ in_out->c = b;
+ }
+
+ void rgb2xyz(Triplet* in_out)
+ {
+ Triplet rgbl = {in_out->a, in_out->b, in_out->c};
+ double x = dot_product(&m_inv[0], &rgbl);
+ double y = dot_product(&m_inv[1], &rgbl);
+ double z = dot_product(&m_inv[2], &rgbl);
+ in_out->a = x;
+ in_out->b = y;
+ in_out->c = z;
+ }
+
+ /* http://en.wikipedia.org/wiki/CIELUV
+ * In these formulas, Yn refers to the reference white point. We are using
+ * illuminant D65, so Yn (see refY in Maxima file) equals 1. The formula is
+ * simplified accordingly.
+ */
+ double y2l(double y)
+ {
+ if (y <= epsilon) {
+ return y * kappa;
+ } else {
+ return 116.0 * cbrt(y) - 16.0;
+ }
+ }
+
+ double l2y(double l)
+ {
+ if (l <= 8.0) {
+ return l / kappa;
+ } else {
+ double x = (l + 16.0) / 116.0;
+ return (x * x * x);
+ }
+ }
+
+ void xyz2luv(Triplet* in_out)
+ {
+ double divisor = in_out->a + (15.0 * in_out->b) + (3.0 * in_out->c);
+ if (divisor <= 0.00000001) {
+ in_out->a = 0.0;
+ in_out->b = 0.0;
+ in_out->c = 0.0;
+ return;
+ }
+
+ double var_u = (4.0 * in_out->a) / divisor;
+ double var_v = (9.0 * in_out->b) / divisor;
+ double l = y2l(in_out->b);
+ double u = 13.0 * l * (var_u - ref_u);
+ double v = 13.0 * l * (var_v - ref_v);
+
+ in_out->a = l;
+ if (l < 0.00000001) {
+ in_out->b = 0.0;
+ in_out->c = 0.0;
+ } else {
+ in_out->b = u;
+ in_out->c = v;
+ }
+ }
+
+ void luv2xyz(Triplet* in_out)
+ {
+ if (in_out->a <= 0.00000001) {
+ /* Black will create a divide-by-zero error. */
+ in_out->a = 0.0;
+ in_out->b = 0.0;
+ in_out->c = 0.0;
+ return;
+ }
+
+ double var_u = in_out->b / (13.0 * in_out->a) + ref_u;
+ double var_v = in_out->c / (13.0 * in_out->a) + ref_v;
+ double y = l2y(in_out->a);
+ double x = -(9.0 * y * var_u) / ((var_u - 4.0) * var_v - var_u * var_v);
+ double z = (9.0 * y - (15.0 * var_v * y) - (var_v * x)) / (3.0 * var_v);
+ in_out->a = x;
+ in_out->b = y;
+ in_out->c = z;
+ }
+
+ void luv2lch(Triplet* in_out)
+ {
+ double l = in_out->a;
+ double u = in_out->b;
+ double v = in_out->c;
+ double h;
+ double c = sqrt(u * u + v * v);
+
+ /* Grays: disambiguate hue */
+ if (c < 0.00000001) {
+ h = 0;
+ } else {
+ h = atan2(v, u) * 57.29577951308232087680; /* (180 / pi) */
+ if (h < 0.0)
+ h += 360.0;
+ }
+
+ in_out->a = l;
+ in_out->b = c;
+ in_out->c = h;
+ }
+
+ void lch2luv(Triplet* in_out)
+ {
+ double hrad = in_out->c * 0.01745329251994329577; /* (pi / 180.0) */
+ double u = cos(hrad) * in_out->b;
+ double v = sin(hrad) * in_out->b;
+
+ in_out->b = u;
+ in_out->c = v;
+ }
+
+ void hsluv2lch(Triplet* in_out)
+ {
+ double h = in_out->a;
+ double s = in_out->b;
+ double l = in_out->c;
+ double c;
+
+ /* White and black: disambiguate chroma */
+ if (l > 99.9999999 || l < 0.00000001) {
+ c = 0.0;
+ } else {
+ c = max_chroma_for_lh(l, h) / 100.0 * s;
+ }
+
+ /* Grays: disambiguate hue */
+ if (s < 0.00000001)
+ h = 0.0;
+
+ in_out->a = l;
+ in_out->b = c;
+ in_out->c = h;
+ }
+
+ void lch2hsluv(Triplet* in_out)
+ {
+ double l = in_out->a;
+ double c = in_out->b;
+ double h = in_out->c;
+ double s;
+
+ /* White and black: disambiguate saturation */
+ if (l > 99.9999999 || l < 0.00000001) {
+ s = 0.0;
+ } else {
+ s = c / max_chroma_for_lh(l, h) * 100.0;
+ }
+
+ /* Grays: disambiguate hue */
+ if (c < 0.00000001)
+ h = 0.0;
+
+ in_out->a = h;
+ in_out->b = s;
+ in_out->c = l;
+ }
+
+ void hsluv2rgb(double h, double s, double l, double* pr, double* pg, double* pb)
+ {
+ Triplet tmp = {h, s, l};
+
+ hsluv2lch(&tmp);
+ lch2luv(&tmp);
+ luv2xyz(&tmp);
+ xyz2rgb(&tmp);
+
+ *pr = tmp.a;
+ *pg = tmp.b;
+ *pb = tmp.c;
+ }
+
+ void rgb2hsluv(double r, double g, double b, double* ph, double* ps, double* pl)
+ {
+ Triplet tmp = {r, g, b};
+
+ rgb2xyz(&tmp);
+ xyz2luv(&tmp);
+ luv2lch(&tmp);
+ lch2hsluv(&tmp);
+
+ *ph = tmp.a;
+ *ps = tmp.b;
+ *pl = tmp.c;
+ }
+
+ } // namespace
+} // namespace Phantom
+
+// The code below is for Phantom, and is used for the Rgb/Hsl-based interface
+// for color operations.
+namespace Phantom
+{
+ namespace
+ {
+ // Note: these constants might be out of range when qreal is defined as float
+ // instead of double.
+ inline qreal linear_of_srgb(qreal x)
+ {
+ return x < 0.0404482362771082 ? x / 12.92 : std::pow((x + 0.055) / 1.055, 2.4f);
+ }
+ inline qreal srgb_of_linear(qreal x)
+ {
+ return x < 0.00313066844250063 ? x * 12.92 : std::pow(x, 1.0 / 2.4) * 1.055 - 0.055;
+ }
+ } // namespace
+
+ Rgb rgb_of_qcolor(const QColor& color)
+ {
+ Rgb a;
+ a.r = linear_of_srgb(color.red() / 255.0);
+ a.g = linear_of_srgb(color.green() / 255.0);
+ a.b = linear_of_srgb(color.blue() / 255.0);
+ return a;
+ }
+
+ Hsl hsl_of_rgb(qreal r, qreal g, qreal b)
+ {
+ double h, s, l;
+ rgb2hsluv(r, g, b, &h, &s, &l);
+ s /= 100.0;
+ l /= 100.0;
+ return {h, s, l};
+ }
+
+ Rgb rgb_of_hsl(qreal h, qreal s, qreal l)
+ {
+ double r, g, b;
+ hsluv2rgb(h, s * 100.0, l * 100.0, &r, &g, &b);
+ return {r, g, b};
+ }
+
+ QColor qcolor_of_rgb(qreal r, qreal g, qreal b)
+ {
+ int r_ = static_cast<int>(std::lround(srgb_of_linear(r) * 255.0));
+ int g_ = static_cast<int>(std::lround(srgb_of_linear(g) * 255.0));
+ int b_ = static_cast<int>(std::lround(srgb_of_linear(b) * 255.0));
+ return {r_, g_, b_};
+ }
+
+ QColor lerpQColor(const QColor& x, const QColor& y, qreal a)
+ {
+ Rgb x_ = rgb_of_qcolor(x);
+ Rgb y_ = rgb_of_qcolor(y);
+ Rgb z = Rgb::lerp(x_, y_, a);
+ return qcolor_of_rgb(z.r, z.g, z.b);
+ }
+
+ Rgb Rgb::lerp(const Rgb& x, const Rgb& y, qreal a)
+ {
+ Rgb z;
+ z.r = (1.0 - a) * x.r + a * y.r;
+ z.g = (1.0 - a) * x.g + a * y.g;
+ z.b = (1.0 - a) * x.b + a * y.b;
+ return z;
+ }
+} // namespace Phantom
diff --git a/src/gui/styles/base/phantomcolor.h b/src/gui/styles/base/phantomcolor.h
new file mode 100644
index 000000000..f9573ba65
--- /dev/null
+++ b/src/gui/styles/base/phantomcolor.h
@@ -0,0 +1,165 @@
+/*
+ * HSLuv-C: Human-friendly HSL
+ * <http://github.com/hsluv/hsluv-c>
+ * <http://www.hsluv.org/>
+ *
+ * Copyright (c) 2015 Alexei Boronine (original idea, JavaScript implementation)
+ * Copyright (c) 2015 Roger Tallada (Obj-C implementation)
+ * Copyright (c) 2017 Martin Mitas (C implementation, based on Obj-C implementation)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef PHANTOMCOLOR_H
+#define PHANTOMCOLOR_H
+
+#include <QColor>
+
+namespace Phantom
+{
+ struct Rgb;
+ struct Hsl;
+
+ // A color presumed to be in linear space, represented as RGB. Values are in
+ // the range 0.0 - 1.0. Conversions to and from QColor will assume the QColor
+ // is in sRGB space, and sRGB conversion will be performed.
+ struct Rgb
+ {
+ qreal r, g, b;
+ Rgb()
+ {
+ }
+ Rgb(qreal r, qreal g, qreal b)
+ : r(r)
+ , g(g)
+ , b(b)
+ {
+ }
+
+ inline Hsl toHsl() const;
+ inline QColor toQColor() const;
+ static inline Rgb ofHsl(const Hsl&);
+ static inline Rgb ofQColor(const QColor&);
+
+ static Rgb lerp(const Rgb& x, const Rgb& y, qreal a);
+ };
+
+ // A color represented as pseudo-CIE hue, saturation, and lightness. Hue is in
+ // the range 0.0 - 360.0 (degrees). Lightness and saturation are in the range
+ // 0.0 - 1.0. Using this and making adjustments to the L value will produce
+ // more consistent and predictable results than QColor's .darker()/.lighter().
+ // Note that this is not strictly CIE -- some of the colorspace is distorted so
+ // that it can represented as a continuous coordinate space. Therefore not all
+ // adjustments to the parameters will produce perfectly linear results with
+ // regards to saturation and lightness. But it's still useful, and better than
+ // QColor's .darker()/.lighter(). Additionally, the L value is more useful for
+ // performing comparisons between two colors to measure relative and absolute
+ // brightness.
+ //
+ // See the documentation for the hsluv library for more information. (Note that
+ // for consistency we treat the S and L values in the range 0.0 - 1.0 instead
+ // of 0.0 - 100.0 like hsluv-c on its own does.)
+ struct Hsl
+ {
+ qreal h, s, l;
+ Hsl()
+ {
+ }
+ Hsl(qreal h, qreal s, qreal l)
+ : h(h)
+ , s(s)
+ , l(l)
+ {
+ }
+
+ inline Rgb toRgb() const;
+ inline QColor toQColor() const;
+ static inline Hsl ofRgb(const Rgb&);
+ static inline Hsl ofQColor(const QColor&);
+ };
+ Rgb rgb_of_qcolor(const QColor& color);
+ QColor qcolor_of_rgb(qreal r, qreal g, qreal b);
+ Hsl hsl_of_rgb(qreal r, qreal g, qreal b);
+ Rgb rgb_of_hsl(qreal h, qreal s, qreal l);
+
+ // Clip a floating point value to the range 0.0 - 1.0.
+ inline qreal saturate(qreal x)
+ {
+ if (x < 0.0)
+ return 0.0;
+ if (x > 1.0)
+ return 1.0;
+ return x;
+ }
+
+ inline qreal lerp(qreal x, qreal y, qreal a)
+ {
+ return (1.0 - a) * x + a * y;
+ }
+
+ // Linearly interpolate two QColors after trasnforming them to linear color
+ // space, treating the QColor values as if they were in sRGB space. The
+ // returned QColor is converted back to sRGB space.
+ QColor lerpQColor(const QColor& x, const QColor& y, qreal a);
+
+ Hsl Rgb::toHsl() const
+ {
+ return hsl_of_rgb(r, g, b);
+ }
+
+ QColor Rgb::toQColor() const
+ {
+ return qcolor_of_rgb(r, g, b);
+ }
+
+ Rgb Rgb::ofHsl(const Hsl& hsl)
+ {
+ return rgb_of_hsl(hsl.h, hsl.s, hsl.l);
+ }
+
+ Rgb Rgb::ofQColor(const QColor& color)
+ {
+ return rgb_of_qcolor(color);
+ }
+
+ Rgb Hsl::toRgb() const
+ {
+ return rgb_of_hsl(h, s, l);
+ }
+
+ QColor Hsl::toQColor() const
+ {
+ Rgb rgb = rgb_of_hsl(h, s, l);
+ return qcolor_of_rgb(rgb.r, rgb.g, rgb.b);
+ }
+
+ Hsl Hsl::ofRgb(const Rgb& rgb)
+ {
+ return hsl_of_rgb(rgb.r, rgb.g, rgb.b);
+ }
+
+ Hsl Hsl::ofQColor(const QColor& color)
+ {
+ Rgb rgb = rgb_of_qcolor(color);
+ return hsl_of_rgb(rgb.r, rgb.g, rgb.b);
+ }
+
+} // namespace Phantom
+
+#endif
diff --git a/src/gui/styles/dark/DarkStyle.cpp b/src/gui/styles/dark/DarkStyle.cpp
new file mode 100644
index 000000000..b8e548228
--- /dev/null
+++ b/src/gui/styles/dark/DarkStyle.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "DarkStyle.h"
+#include "gui/osutils/OSUtils.h"
+
+#include <QDialog>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QToolBar>
+
+QPalette DarkStyle::standardPalette() const
+{
+ auto palette = BaseStyle::standardPalette();
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x3B3B3D));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x404042));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x424242));
+
+ palette.setColor(QPalette::Active, QPalette::WindowText, QRgb(0xCACBCE));
+ palette.setColor(QPalette::Inactive, QPalette::WindowText, QRgb(0xC8C8C6));
+ palette.setColor(QPalette::Disabled, QPalette::WindowText, QRgb(0x707070));
+
+ palette.setColor(QPalette::Active, QPalette::Text, QRgb(0xCACBCE));
+ palette.setColor(QPalette::Inactive, QPalette::Text, QRgb(0xC8C8C6));
+ palette.setColor(QPalette::Disabled, QPalette::Text, QRgb(0x707070));
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ palette.setColor(QPalette::Active, QPalette::PlaceholderText, QRgb(0x7D7D82));
+ palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QRgb(0x87888C));
+ palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QRgb(0x737373));
+#endif
+
+ palette.setColor(QPalette::Active, QPalette::BrightText, QRgb(0x252627));
+ palette.setColor(QPalette::Inactive, QPalette::BrightText, QRgb(0x2D2D2F));
+ palette.setColor(QPalette::Disabled, QPalette::BrightText, QRgb(0x333333));
+
+ palette.setColor(QPalette::Active, QPalette::Base, QRgb(0x27272A));
+ palette.setColor(QPalette::Inactive, QPalette::Base, QRgb(0x2A2A2D));
+ palette.setColor(QPalette::Disabled, QPalette::Base, QRgb(0x343437));
+
+ palette.setColor(QPalette::Active, QPalette::AlternateBase, QRgb(0x2C2C30));
+ palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QRgb(0x2B2B2F));
+ palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QRgb(0x36363A));
+
+ palette.setColor(QPalette::All, QPalette::ToolTipBase, QRgb(0x2D532D));
+ palette.setColor(QPalette::All, QPalette::ToolTipText, QRgb(0xBFBFBF));
+
+ palette.setColor(QPalette::Active, QPalette::Button, QRgb(0x28282B));
+ palette.setColor(QPalette::Inactive, QPalette::Button, QRgb(0x28282B));
+ palette.setColor(QPalette::Disabled, QPalette::Button, QRgb(0x2B2A2A));
+
+ palette.setColor(QPalette::Active, QPalette::ButtonText, QRgb(0xB9B9BE));
+ palette.setColor(QPalette::Inactive, QPalette::ButtonText, QRgb(0x9E9FA5));
+ palette.setColor(QPalette::Disabled, QPalette::ButtonText, QRgb(0x73747E));
+
+ palette.setColor(QPalette::Active, QPalette::Highlight, QRgb(0x2D532D));
+ palette.setColor(QPalette::Inactive, QPalette::Highlight, QRgb(0x354637));
+ palette.setColor(QPalette::Disabled, QPalette::Highlight, QRgb(0x293D29));
+
+ palette.setColor(QPalette::Active, QPalette::HighlightedText, QRgb(0xCCCCCC));
+ palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QRgb(0xCECECE));
+ palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QRgb(0x707070));
+
+ palette.setColor(QPalette::All, QPalette::Light, QRgb(0x414145));
+ palette.setColor(QPalette::All, QPalette::Midlight, QRgb(0x39393C));
+ palette.setColor(QPalette::All, QPalette::Mid, QRgb(0x2F2F32));
+ palette.setColor(QPalette::All, QPalette::Dark, QRgb(0x202022));
+ palette.setColor(QPalette::All, QPalette::Shadow, QRgb(0x19191A));
+
+ palette.setColor(QPalette::All, QPalette::Link, QRgb(0x68B668));
+ palette.setColor(QPalette::Disabled, QPalette::Link, QRgb(0x74A474));
+ palette.setColor(QPalette::All, QPalette::LinkVisited, QRgb(0x75B875));
+ palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QRgb(0x77A677));
+
+ return palette;
+}
+
+QString DarkStyle::getAppStyleSheet() const
+{
+ QFile extStylesheetFile(QStringLiteral(":/styles/dark/darkstyle.qss"));
+ if (extStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return extStylesheetFile.readAll();
+ }
+ qWarning("Failed to load dark theme stylesheet.");
+ return {};
+}
+
+void DarkStyle::polish(QWidget* widget)
+{
+ if (qobject_cast<QMainWindow*>(widget) || qobject_cast<QDialog*>(widget) || qobject_cast<QMenuBar*>(widget)
+ || qobject_cast<QToolBar*>(widget)) {
+ auto palette = widget->palette();
+#if defined(Q_OS_MACOS)
+ if (osUtils->isDarkMode()) {
+ // Let the Cocoa platform plugin draw its own background
+ palette.setColor(QPalette::All, QPalette::Window, Qt::transparent);
+ } else {
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2A2A2A));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x2D2D2D));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x2A2A2A));
+ }
+#elif defined(Q_OS_WIN)
+ // Register event filter for better dark mode support
+ WinUtils::registerEventFilters();
+ palette.setColor(QPalette::All, QPalette::Window, QRgb(0x2F2F30));
+#else
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0x2F2F30));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0x313133));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0x3A3A3B));
+#endif
+
+ widget->setPalette(palette);
+ }
+}
diff --git a/src/gui/styles/dark/DarkStyle.h b/src/gui/styles/dark/DarkStyle.h
new file mode 100644
index 000000000..9b955d3a5
--- /dev/null
+++ b/src/gui/styles/dark/DarkStyle.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_DARKSTYLE_H
+#define KEEPASSXC_DARKSTYLE_H
+
+#include "gui/styles/base/BaseStyle.h"
+#include <QApplication>
+
+class DarkStyle : public BaseStyle
+{
+ Q_OBJECT
+
+public:
+ QPalette standardPalette() const override;
+
+ using BaseStyle::polish;
+ void polish(QWidget* widget) override;
+
+protected:
+ QString getAppStyleSheet() const override;
+};
+
+#endif // KEEPASSXC_DARKSTYLE_H
diff --git a/src/gui/styles/dark/darkstyle.qss b/src/gui/styles/dark/darkstyle.qss
new file mode 100644
index 000000000..39ec32a2b
--- /dev/null
+++ b/src/gui/styles/dark/darkstyle.qss
@@ -0,0 +1,18 @@
+DatabaseWidget:!active, GroupView:!active,
+EntryPreviewWidget QLineEdit:!active, EntryPreviewWidget QTextEdit:!active {
+ background-color: #404042;
+}
+
+DatabaseWidget:disabled, GroupView:disabled,
+EntryPreviewWidget QLineEdit:disabled, EntryPreviewWidget QTextEdit:disabled {
+ background-color: #424242;
+}
+
+QToolTip {
+ color: #BFBFBF;
+ background-color: #2D532D;
+}
+
+QGroupBox {
+ background-color: palette(light);
+}
diff --git a/src/gui/styles/light/LightStyle.cpp b/src/gui/styles/light/LightStyle.cpp
new file mode 100644
index 000000000..8dd9d6c53
--- /dev/null
+++ b/src/gui/styles/light/LightStyle.cpp
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "LightStyle.h"
+#include "gui/ApplicationSettingsWidget.h"
+#include "gui/osutils/OSUtils.h"
+
+#include <QDialog>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QToolBar>
+
+QPalette LightStyle::standardPalette() const
+{
+ auto palette = BaseStyle::standardPalette();
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xF7F7F7));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xFCFCFC));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xEDEDED));
+
+ palette.setColor(QPalette::Active, QPalette::WindowText, QRgb(0x1D1D20));
+ palette.setColor(QPalette::Inactive, QPalette::WindowText, QRgb(0x252528));
+ palette.setColor(QPalette::Disabled, QPalette::WindowText, QRgb(0x8C8C92));
+
+ palette.setColor(QPalette::Active, QPalette::Text, QRgb(0x1D1D20));
+ palette.setColor(QPalette::Inactive, QPalette::Text, QRgb(0x252528));
+ palette.setColor(QPalette::Disabled, QPalette::Text, QRgb(0x8C8C92));
+
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ palette.setColor(QPalette::Active, QPalette::PlaceholderText, QRgb(0x71727D));
+ palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QRgb(0x878893));
+ palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QRgb(0xA3A4AC));
+#endif
+
+ palette.setColor(QPalette::Active, QPalette::BrightText, QRgb(0xF3F3F4));
+ palette.setColor(QPalette::Inactive, QPalette::BrightText, QRgb(0xEAEAEB));
+ palette.setColor(QPalette::Disabled, QPalette::BrightText, QRgb(0xE4E5E7));
+
+ palette.setColor(QPalette::Active, QPalette::Base, QRgb(0xF9F9F9));
+ palette.setColor(QPalette::Inactive, QPalette::Base, QRgb(0xFCFCFC));
+ palette.setColor(QPalette::Disabled, QPalette::Base, QRgb(0xEFEFF2));
+
+ palette.setColor(QPalette::Active, QPalette::AlternateBase, QRgb(0xECF3E8));
+ palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QRgb(0xF1F6EE));
+ palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QRgb(0xE1E9DD));
+
+ palette.setColor(QPalette::All, QPalette::ToolTipBase, QRgb(0x4D7F1A));
+ palette.setColor(QPalette::All, QPalette::ToolTipText, QRgb(0xF9F9F9));
+
+ palette.setColor(QPalette::Active, QPalette::Button, QRgb(0xD4D5DD));
+ palette.setColor(QPalette::Inactive, QPalette::Button, QRgb(0xDCDCE0));
+ palette.setColor(QPalette::Disabled, QPalette::Button, QRgb(0xE5E5E6));
+
+ palette.setColor(QPalette::Active, QPalette::ButtonText, QRgb(0x181A18));
+ palette.setColor(QPalette::Inactive, QPalette::ButtonText, QRgb(0x454A54));
+ palette.setColor(QPalette::Disabled, QPalette::ButtonText, QRgb(0x97979B));
+
+ palette.setColor(QPalette::Active, QPalette::Highlight, QRgb(0x507F1F));
+ palette.setColor(QPalette::Inactive, QPalette::Highlight, QRgb(0xA6BE8E));
+ palette.setColor(QPalette::Disabled, QPalette::Highlight, QRgb(0xC3D5B4));
+
+ palette.setColor(QPalette::Active, QPalette::HighlightedText, QRgb(0xFFFFFF));
+ palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QRgb(0x252528));
+ palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QRgb(0x8C8C92));
+
+ palette.setColor(QPalette::All, QPalette::Light, QRgb(0xF9F9F9));
+ palette.setColor(QPalette::All, QPalette::Midlight, QRgb(0xE9E9EB));
+ palette.setColor(QPalette::All, QPalette::Mid, QRgb(0xC9C9CF));
+ palette.setColor(QPalette::All, QPalette::Dark, QRgb(0xBBBBC2));
+ palette.setColor(QPalette::All, QPalette::Shadow, QRgb(0x6C6D79));
+
+ palette.setColor(QPalette::All, QPalette::Link, QRgb(0x4B7B19));
+ palette.setColor(QPalette::Disabled, QPalette::Link, QRgb(0x4F6935));
+ palette.setColor(QPalette::All, QPalette::LinkVisited, QRgb(0x507826));
+ palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QRgb(0x506935));
+
+ return palette;
+}
+
+QString LightStyle::getAppStyleSheet() const
+{
+ QFile extStylesheetFile(QStringLiteral(":/styles/light/lightstyle.qss"));
+ if (extStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return extStylesheetFile.readAll();
+ }
+ qWarning("Failed to load light theme stylesheet.");
+ return {};
+}
+
+void LightStyle::polish(QWidget* widget)
+{
+ if (qobject_cast<QMainWindow*>(widget) || qobject_cast<QDialog*>(widget) || qobject_cast<QMenuBar*>(widget)
+ || qobject_cast<QToolBar*>(widget)) {
+ auto palette = widget->palette();
+#if defined(Q_OS_MACOS)
+ if (!osUtils->isDarkMode()) {
+ // Let the Cocoa platform plugin draw its own background
+ palette.setColor(QPalette::All, QPalette::Window, Qt::transparent);
+ } else {
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xD6D6D6));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xF6F6F6));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xD4D4D4));
+ }
+#elif defined(Q_OS_WIN)
+ palette.setColor(QPalette::All, QPalette::Window, QRgb(0xFFFFFF));
+#else
+ palette.setColor(QPalette::Active, QPalette::Window, QRgb(0xEFF0F1));
+ palette.setColor(QPalette::Inactive, QPalette::Window, QRgb(0xEFF0F1));
+ palette.setColor(QPalette::Disabled, QPalette::Window, QRgb(0xE1E2E4));
+#endif
+
+ widget->setPalette(palette);
+ }
+}
diff --git a/src/gui/styles/light/LightStyle.h b/src/gui/styles/light/LightStyle.h
new file mode 100644
index 000000000..d2d4f48a3
--- /dev/null
+++ b/src/gui/styles/light/LightStyle.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef KEEPASSXC_LIGHTSTYLE_H
+#define KEEPASSXC_LIGHTSTYLE_H
+
+#include "gui/styles/base/BaseStyle.h"
+#include <QApplication>
+
+class LightStyle : public BaseStyle
+{
+ Q_OBJECT
+
+public:
+ QPalette standardPalette() const override;
+
+ using BaseStyle::polish;
+ void polish(QWidget* widget) override;
+
+protected:
+ QString getAppStyleSheet() const override;
+};
+
+#endif // KEEPASSXC_LIGHTSTYLE_H
diff --git a/src/gui/styles/light/lightstyle.qss b/src/gui/styles/light/lightstyle.qss
new file mode 100644
index 000000000..079907d15
--- /dev/null
+++ b/src/gui/styles/light/lightstyle.qss
@@ -0,0 +1,18 @@
+DatabaseWidget:!active, GroupView:!active,
+EntryPreviewWidget QLineEdit:!active, EntryPreviewWidget QTextEdit:!active {
+ background-color: #FCFCFC;
+}
+
+DatabaseWidget:disabled, GroupView:disabled,
+EntryPreviewWidget QLineEdit:disabled, EntryPreviewWidget QTextEdit:disabled {
+ background-color: #EDEDED;
+}
+
+QGroupBox::title {
+ color: #4B7B19;
+}
+
+QToolTip {
+ color: #F9F9F9;
+ background-color: #4D7F1A;
+}
diff --git a/src/gui/styles/styles.qrc b/src/gui/styles/styles.qrc
new file mode 100644
index 000000000..c8e9057dc
--- /dev/null
+++ b/src/gui/styles/styles.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+ <qresource prefix="/styles">
+ <file>base/basestyle.qss</file>
+ <file>dark/darkstyle.qss</file>
+ <file>light/lightstyle.qss</file>
+ </qresource>
+</RCC>
diff --git a/src/gui/widgets/ElidedLabel.cpp b/src/gui/widgets/ElidedLabel.cpp
index 749f075c8..5e71fbceb 100644
--- a/src/gui/widgets/ElidedLabel.cpp
+++ b/src/gui/widgets/ElidedLabel.cpp
@@ -56,8 +56,9 @@ QString ElidedLabel::url() const
void ElidedLabel::setElideMode(Qt::TextElideMode elideMode)
{
- if (m_elideMode == elideMode)
+ if (m_elideMode == elideMode) {
return;
+ }
if (m_elideMode != Qt::ElideNone) {
setWordWrap(false);
@@ -69,8 +70,9 @@ void ElidedLabel::setElideMode(Qt::TextElideMode elideMode)
void ElidedLabel::setRawText(const QString& elidedText)
{
- if (m_rawText == elidedText)
+ if (m_rawText == elidedText) {
return;
+ }
m_rawText = elidedText;
emit rawTextChanged(m_rawText);
@@ -78,8 +80,9 @@ void ElidedLabel::setRawText(const QString& elidedText)
void ElidedLabel::setUrl(const QString& url)
{
- if (m_url == url)
+ if (m_url == url) {
return;
+ }
m_url = url;
emit urlChanged(m_url);
diff --git a/src/gui/widgets/PopupHelpWidget.cpp b/src/gui/widgets/PopupHelpWidget.cpp
index 269c31c5b..2a604dce9 100644
--- a/src/gui/widgets/PopupHelpWidget.cpp
+++ b/src/gui/widgets/PopupHelpWidget.cpp
@@ -23,27 +23,22 @@
PopupHelpWidget::PopupHelpWidget(QWidget* parent)
: QFrame(parent)
- , m_parentWindow(parent->window())
, m_appWindow(getMainWindow())
, m_offset({0, 0})
, m_corner(Qt::BottomLeftCorner)
{
Q_ASSERT(parent);
-#ifdef Q_OS_MACOS
- setWindowFlags(Qt::FramelessWindowHint | Qt::Drawer);
-#else
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);
-#endif
hide();
m_appWindow->installEventFilter(this);
- parent->installEventFilter(this);
+ parentWidget()->installEventFilter(this);
}
PopupHelpWidget::~PopupHelpWidget()
{
- m_parentWindow->removeEventFilter(this);
+ m_appWindow->removeEventFilter(this);
parentWidget()->removeEventFilter(this);
}
@@ -65,10 +60,10 @@ void PopupHelpWidget::setPosition(Qt::Corner corner)
bool PopupHelpWidget::eventFilter(QObject* obj, QEvent* event)
{
- if (obj == parentWidget() && event->type() == QEvent::FocusOut) {
- hide();
- } else if (obj == m_appWindow && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
- if (isVisible()) {
+ if (isVisible()) {
+ if (obj == parentWidget() && event->type() == QEvent::FocusOut && qApp->focusWindow() != windowHandle()) {
+ hide();
+ } else if (obj == m_appWindow && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) {
alignWithParent();
}
}
@@ -83,21 +78,22 @@ void PopupHelpWidget::showEvent(QShowEvent* event)
void PopupHelpWidget::alignWithParent()
{
- QPoint pos;
+ QPoint pos = m_offset;
switch (m_corner) {
case Qt::TopLeftCorner:
- pos = parentWidget()->geometry().topLeft() + m_offset - QPoint(0, height());
+ pos += QPoint(0, -height());
break;
case Qt::TopRightCorner:
- pos = parentWidget()->geometry().topRight() + m_offset - QPoint(width(), height());
+ pos += QPoint(parentWidget()->width(), -height());
break;
case Qt::BottomRightCorner:
- pos = parentWidget()->geometry().bottomRight() + m_offset - QPoint(width(), 0);
+ pos += QPoint(parentWidget()->width(), parentWidget()->height());
break;
+ case Qt::BottomLeftCorner:
default:
- pos = parentWidget()->geometry().bottomLeft() + m_offset;
+ pos += QPoint(0, parentWidget()->height());
break;
}
- move(m_parentWindow->mapToGlobal(pos));
-} \ No newline at end of file
+ move(parentWidget()->mapToGlobal(pos));
+}
diff --git a/src/gui/widgets/PopupHelpWidget.h b/src/gui/widgets/PopupHelpWidget.h
index 3c02ccc1a..353121c6a 100644
--- a/src/gui/widgets/PopupHelpWidget.h
+++ b/src/gui/widgets/PopupHelpWidget.h
@@ -37,7 +37,6 @@ protected:
private:
void alignWithParent();
- QPointer<QWidget> m_parentWindow;
QPointer<QWidget> m_appWindow;
QPoint m_offset;
diff --git a/src/gui/wizard/NewDatabaseWizard.cpp b/src/gui/wizard/NewDatabaseWizard.cpp
index eaadc53ff..45ac8e46d 100644
--- a/src/gui/wizard/NewDatabaseWizard.cpp
+++ b/src/gui/wizard/NewDatabaseWizard.cpp
@@ -16,16 +16,18 @@
*/
#include "NewDatabaseWizard.h"
+#include "NewDatabaseWizardPageDatabaseKey.h"
#include "NewDatabaseWizardPageEncryption.h"
-#include "NewDatabaseWizardPageMasterKey.h"
#include "NewDatabaseWizardPageMetaData.h"
#include "core/Database.h"
-#include "core/FilePath.h"
#include "core/Global.h"
#include "core/Group.h"
+#include "core/Resources.h"
#include "format/KeePass2.h"
+#include <QFrame>
+#include <QPalette>
#include <QVBoxLayout>
NewDatabaseWizard::NewDatabaseWizard(QWidget* parent)
@@ -39,7 +41,7 @@ NewDatabaseWizard::NewDatabaseWizard(QWidget* parent)
// clang-format off
m_pages << new NewDatabaseWizardPageMetaData()
<< new NewDatabaseWizardPageEncryption()
- << new NewDatabaseWizardPageMasterKey();
+ << new NewDatabaseWizardPageDatabaseKey();
// clang-format on
for (const auto& page : asConst(m_pages)) {
@@ -48,7 +50,21 @@ NewDatabaseWizard::NewDatabaseWizard(QWidget* parent)
setWindowTitle(tr("Create a new KeePassXC database..."));
- setPixmap(QWizard::BackgroundPixmap, QPixmap(filePath()->dataPath("wizard/background-pixmap.png")));
+ Q_INIT_RESOURCE(wizard);
+ setPixmap(QWizard::BackgroundPixmap, QPixmap(":/wizard/background-pixmap.png"));
+
+ // Fix MacStyle QWizard page frame too bright in dark mode (QTBUG-70346, QTBUG-71696)
+ QPalette defaultPalette;
+ auto windowColor = defaultPalette.color(QPalette::Window);
+ windowColor.setAlpha(153);
+ auto baseColor = defaultPalette.color(QPalette::Base);
+ baseColor.setAlpha(153);
+
+ auto* pageFrame = findChildren<QFrame*>()[0];
+ auto framePalette = pageFrame->palette();
+ framePalette.setBrush(QPalette::Window, windowColor.lighter(120));
+ framePalette.setBrush(QPalette::Base, baseColor.lighter(120));
+ pageFrame->setPalette(framePalette);
}
NewDatabaseWizard::~NewDatabaseWizard()
@@ -57,11 +73,7 @@ NewDatabaseWizard::~NewDatabaseWizard()
bool NewDatabaseWizard::validateCurrentPage()
{
- bool ok = m_pages[currentId()]->validatePage();
- if (ok && currentId() == m_pages.size() - 1) {
- m_db->setInitialized(true);
- }
- return ok;
+ return m_pages[currentId()]->validatePage();
}
/**
diff --git a/src/gui/wizard/NewDatabaseWizardPage.ui b/src/gui/wizard/NewDatabaseWizardPage.ui
index 6b69e85b5..e920b26ed 100644
--- a/src/gui/wizard/NewDatabaseWizardPage.ui
+++ b/src/gui/wizard/NewDatabaseWizardPage.ui
@@ -12,7 +12,7 @@
<string>WizardPage</string>
</property>
<property name="title">
- <string>En&amp;cryption Settings</string>
+ <string>Encryption Settings</string>
</property>
<property name="subTitle">
<string>Here you can adjust the database encryption settings. Don't worry, you can change them later in the database settings.</string>
diff --git a/src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp b/src/gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
index 130560e27..3180400c0 100644
--- a/src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp
+++ b/src/gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
@@ -15,27 +15,27 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#include "NewDatabaseWizardPageMasterKey.h"
-#include "gui/dbsettings/DatabaseSettingsWidgetMasterKey.h"
+#include "NewDatabaseWizardPageDatabaseKey.h"
+#include "gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.h"
#include <QApplication>
-NewDatabaseWizardPageMasterKey::NewDatabaseWizardPageMasterKey(QWidget* parent)
+NewDatabaseWizardPageDatabaseKey::NewDatabaseWizardPageDatabaseKey(QWidget* parent)
: NewDatabaseWizardPage(parent)
{
- setPageWidget(new DatabaseSettingsWidgetMasterKey());
+ setPageWidget(new DatabaseSettingsWidgetDatabaseKey());
- setTitle(tr("Database Master Key"));
- setSubTitle(tr("A master key known only to you protects your database."));
+ setTitle(tr("Database Credentials"));
+ setSubTitle(tr("A set of credentials known only to you that protects your database."));
connect(pageWidget(), SIGNAL(sizeChanged()), SLOT(updateWindowSize()));
}
-NewDatabaseWizardPageMasterKey::~NewDatabaseWizardPageMasterKey()
+NewDatabaseWizardPageDatabaseKey::~NewDatabaseWizardPageDatabaseKey()
{
}
-void NewDatabaseWizardPageMasterKey::updateWindowSize()
+void NewDatabaseWizardPageDatabaseKey::updateWindowSize()
{
// ugly workaround for QWizard not managing to react to size changes automatically
- QApplication::activeWindow()->adjustSize();
+ window()->adjustSize();
}
diff --git a/src/gui/wizard/NewDatabaseWizardPageMasterKey.h b/src/gui/wizard/NewDatabaseWizardPageDatabaseKey.h
index 3b5072846..e0fb5349e 100644
--- a/src/gui/wizard/NewDatabaseWizardPageMasterKey.h
+++ b/src/gui/wizard/NewDatabaseWizardPageDatabaseKey.h
@@ -15,22 +15,22 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef KEEPASSXC_NEWDATABASEWIZARDPAGEMASTERKEY_H
-#define KEEPASSXC_NEWDATABASEWIZARDPAGEMASTERKEY_H
+#ifndef KEEPASSXC_NEWDATABASEWIZARDPAGEDATABASEKEY_H
+#define KEEPASSXC_NEWDATABASEWIZARDPAGEDATABASEKEY_H
#include "NewDatabaseWizardPage.h"
-class NewDatabaseWizardPageMasterKey : public NewDatabaseWizardPage
+class NewDatabaseWizardPageDatabaseKey : public NewDatabaseWizardPage
{
Q_OBJECT
public:
- explicit NewDatabaseWizardPageMasterKey(QWidget* parent = nullptr);
- Q_DISABLE_COPY(NewDatabaseWizardPageMasterKey);
- ~NewDatabaseWizardPageMasterKey() override;
+ explicit NewDatabaseWizardPageDatabaseKey(QWidget* parent = nullptr);
+ Q_DISABLE_COPY(NewDatabaseWizardPageDatabaseKey);
+ ~NewDatabaseWizardPageDatabaseKey() override;
private slots:
void updateWindowSize();
};
-#endif // KEEPASSXC_NEWDATABASEWIZARDPAGEMASTERKEY_H
+#endif // KEEPASSXC_NEWDATABASEWIZARDPAGEDATABASEKEY_H