From 2d44c7e4629b0d3c2d35924b1f432e869af51539 Mon Sep 17 00:00:00 2001 From: James Ring Date: Mon, 21 Oct 2019 15:25:46 -0700 Subject: [PATCH 001/215] Fix unused variable error when building without WITH_XC_YUBIKEY. --- src/cli/Utils.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index a45967917e..9988b60f9a 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -177,7 +177,9 @@ namespace Utils outputDescriptor)); compositeKey->addChallengeResponseKey(key); } -#endif +#else + Q_UNUSED(yubiKeySlot); +#endif // WITH_XC_YUBIKEY auto db = QSharedPointer::create(); QString error; From a8c10cda9184893f9003f29285f4b2abb8512fcd Mon Sep 17 00:00:00 2001 From: Constantin Date: Thu, 10 Oct 2019 22:27:00 +0200 Subject: [PATCH 002/215] Update QUICKSTART.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improve style, insert »it« in sentence. --- docs/QUICKSTART.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 1a2cd3dd42..3e366bbfe7 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -49,7 +49,7 @@ Sharing allows you to share a subset of your credentials with others and vice ve ### Enable Sharing -To use sharing, you need to enable for the application. +To use sharing, you need to enable it for the application. 1. Go to Tools → Settings. 1. Select the category KeeShare. From eb75985aa60ba6bead7fbe10a7c8b20e8fbd2678 Mon Sep 17 00:00:00 2001 From: Sergei Zyubin Date: Wed, 30 Oct 2019 10:37:17 +0100 Subject: [PATCH 003/215] INSTALL.md: Fix broken link for MacOS --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index e82fb53b17..345a4198ee 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -35,7 +35,7 @@ Prepare the Building Environment * [Building Environment on Linux](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Linux) * [Building Environment on Windows](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Windows) -* [Building Environment on MacOS](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-OS-X) +* [Building Environment on MacOS](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-macOS) Build Steps =========== From 09d7b5db311cd488f02415b746b6683780c49d5d Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Fri, 1 Nov 2019 22:51:45 -0400 Subject: [PATCH 004/215] Create FUNDING.yml --- .github/FUNDING.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..b7f0bb955b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +github: droidmonkey +patreon: keepassxc +liberapay: keepassxc +custom: ["https://keepassxc.org/donate"] From 74381dc11553b256681e4f2f0ef230c96e99c5f4 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Tue, 5 Nov 2019 07:58:11 +0100 Subject: [PATCH 005/215] Add additional maintainer --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b7f0bb955b..71d4a5e1a1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ -github: droidmonkey +github: ["droidmonkey", "phoerious"] patreon: keepassxc liberapay: keepassxc custom: ["https://keepassxc.org/donate"] From 39af47fbf9d386a2068745004fc0e39b30c2bc6e Mon Sep 17 00:00:00 2001 From: Sergey Vilgelm Date: Fri, 15 Nov 2019 09:37:14 -0600 Subject: [PATCH 006/215] Add a new line after in Analyze command Adding a new line after the message "Evaluating database entries against HIBP file, this will take a while..." helps to separate a report and the comment. --- src/cli/Analyze.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/Analyze.cpp b/src/cli/Analyze.cpp index 3e6edcebfb..7fc00c8eff 100644 --- a/src/cli/Analyze.cpp +++ b/src/cli/Analyze.cpp @@ -55,7 +55,8 @@ int Analyze::executeWithDatabase(QSharedPointer database, QSharedPoint return EXIT_FAILURE; } - outputTextStream << QObject::tr("Evaluating database entries against HIBP file, this will take a while..."); + outputTextStream << QObject::tr("Evaluating database entries against HIBP file, this will take a while...") + << endl; QList> findings; QString error; From 56a5a129c651b55e08d71ff55bd3b2ab4a695f23 Mon Sep 17 00:00:00 2001 From: Balazs Gyurak Date: Sun, 17 Nov 2019 19:33:28 +0000 Subject: [PATCH 007/215] Correctly initialize standalone PW generator mode --- src/gui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0d53d88a85..6452c051bb 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -885,8 +885,8 @@ void MainWindow::switchToPasswordGen(bool enabled) if (enabled) { m_ui->passwordGeneratorWidget->loadSettings(); m_ui->passwordGeneratorWidget->regeneratePassword(); - m_ui->passwordGeneratorWidget->setStandaloneMode(true); m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); } else { m_ui->passwordGeneratorWidget->saveSettings(); switchToDatabases(); From dc42d5dda663d03bec7faaf80c037280879d13de Mon Sep 17 00:00:00 2001 From: Balazs Gyurak Date: Sun, 17 Nov 2019 13:03:05 +0000 Subject: [PATCH 008/215] Release database before exiting CLI interactive mode --- src/cli/keepassxc-cli.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index e16be9b296..98cc6be06a 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -149,6 +149,7 @@ void enterInteractiveMode(const QStringList& arguments) prompt += "> "; command = reader->readLine(prompt); if (reader->isFinished()) { + currentDatabase->releaseData(); return; } @@ -162,6 +163,7 @@ void enterInteractiveMode(const QStringList& arguments) errorTextStream << QObject::tr("Unknown command %1").arg(args[0]) << "\n"; continue; } else if (cmd->name == "quit" || cmd->name == "exit") { + currentDatabase->releaseData(); return; } From cb28329f144ac9a3b022fcadd493677e5b6ba927 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sat, 23 Nov 2019 13:28:50 +0000 Subject: [PATCH 009/215] Fix typos in various .md files --- .../release-preview-bug-report.md | 2 +- docs/KEYBINDS.md | 64 +++++++++---------- docs/QUICKSTART.md | 4 +- src/fdosecrets/README.md | 2 +- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release-preview-bug-report.md b/.github/ISSUE_TEMPLATE/release-preview-bug-report.md index 25b720168e..a22d1fa693 100644 --- a/.github/ISSUE_TEMPLATE/release-preview-bug-report.md +++ b/.github/ISSUE_TEMPLATE/release-preview-bug-report.md @@ -1,6 +1,6 @@ --- name: Release Preview Bug report -about: report a bug with a release preview (eg, 2.4.0-beta1) +about: report a bug with a release preview (e.g., 2.4.0-beta1) title: "[PRE-RELEASE] " labels: PRE-RELEASE BUG assignees: droidmonkey diff --git a/docs/KEYBINDS.md b/docs/KEYBINDS.md index 94e8b3daf1..968df20377 100644 --- a/docs/KEYBINDS.md +++ b/docs/KEYBINDS.md @@ -1,35 +1,31 @@ -# List of Keyboard Shortcuts for KeepassXC - -Actions | Keyboard Shortcuts ----------------------------|---------------------------- -New Database | Ctrl + Shift + N -Open Database | Ctrl + O -Save Database | Ctrl + S -Save Database As | Ctrl + Shift + S -Close Database | Ctrl + W -Lock Databases | Ctrl + L -Quit | Ctrl + Q -New Entry | Ctrl + N -Edit Entry | Ctrl + E -Delete Entry | Ctrl + D -Clone Entry | Ctrl + K -Show TOTP | Ctrl + Shift + T -Copy TOTP | Ctrl + T -Copy Username | Ctrl + B -Copy Password | Ctrl + C -Trigger AutoType | Ctrl + Shift - V -Open Url | Ctrl + Shift - U -Copy Url | Ctrl + U -Show Minimized | Ctrl + M -Hide Window | Ctrl + Shift - M -Select Next Database Tab | Ctrl + Tab *OR* Ctrl + PGDN -Select Previous Datase Tab | Ctrl + Shift + Tab *OR* Ctrl + PGUP -Toggle Passwords Hidden | Ctrl + Shift + C -Toggle Usernames Hidden | Ctrl + Shift + B -Focus Search | Ctrl + F -Clear Search | ESC -Show Keyboard Shortcuts | Ctrl + / - - - +# List of Keyboard Shortcuts for KeePassXC +Actions | Keyboard Shortcuts +-----------------------------|---------------------------- +New Database | Ctrl + Shift + N +Open Database | Ctrl + O +Save Database | Ctrl + S +Save Database As | Ctrl + Shift + S +Close Database | Ctrl + W +Lock Databases | Ctrl + L +Quit | Ctrl + Q +New Entry | Ctrl + N +Edit Entry | Ctrl + E +Delete Entry | Ctrl + D +Clone Entry | Ctrl + K +Show TOTP | Ctrl + Shift + T +Copy TOTP | Ctrl + T +Copy Username | Ctrl + B +Copy Password | Ctrl + C +Trigger AutoType | Ctrl + Shift - V +Open URL | Ctrl + Shift - U +Copy URL | Ctrl + U +Show Minimized | Ctrl + M +Hide Window | Ctrl + Shift - M +Select Next Database Tab | Ctrl + Tab *OR* Ctrl + PGDN +Select Previous Database Tab | Ctrl + Shift + Tab *OR* Ctrl + PGUP +Toggle Passwords Hidden | Ctrl + Shift + C +Toggle Usernames Hidden | Ctrl + Shift + B +Focus Search | Ctrl + F +Clear Search | ESC +Show Keyboard Shortcuts | Ctrl + / diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 3e366bbfe7..f1668ace89 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -120,14 +120,14 @@ There is a simple overview of shared groups to keep track of your data. ## Technical Details and Limitations of Sharing -Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC. Since the merge algorithm uses the history of entries to prevent data loss, this history must be enabled and have a sufficient size. Furthermore, the merge algorithm is location independend, therefore it does not matter if entries are moved outside of an import group. These entries will be updated none the less. Moving entries outside of export groups will prevent a further export of the entry, but it will not ensure that the already shared data will be removed from any client. +Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC. Since the merge algorithm uses the history of entries to prevent data loss, this history must be enabled and have a sufficient size. Furthermore, the merge algorithm is location independent, therefore it does not matter if entries are moved outside of an import group. These entries will be updated none the less. Moving entries outside of export groups will prevent a further export of the entry, but it will not ensure that the already shared data will be removed from any client. KeeShare uses a custom certification mechanism to ensure that the source of the data is the expected one. This ensures that the data was exported by the signer but it is not possible to detect if someone replaced the data with an older version from a valid signer. To prevent this, the container could be placed at a location which is only writeable for valid signers. ## Using Auto Open The Auto Open feature automatically loads and unlocks additional databases when you unlock your main database. -In order to use this functionnality, do the following: +In order to use this functionality, do the following: 1. Create a group called **AutoOpen** at the root of your main database. 1. In this group, create a new entry for each database that should be opened automatically: diff --git a/src/fdosecrets/README.md b/src/fdosecrets/README.md index 22278860c8..bd28754a1f 100644 --- a/src/fdosecrets/README.md +++ b/src/fdosecrets/README.md @@ -9,7 +9,7 @@ can connect and access the exposed database in KeePassXC. ## Configurable settings * The user can specify if a database is exposed on DBus, and which group is exposed. -* Whether to show desktop notification is shown when an entry is retrived. +* Whether to show desktop notification is shown when an entry is retrieved. * Whether to skip confirmation for entries deleted from DBus ## Implemented Attributes on Item Object From 0423bbe16861e798ad8da0d82392146428affc1f Mon Sep 17 00:00:00 2001 From: Carlo Teubner <435950+c4rlo@users.noreply.github.com> Date: Sat, 23 Nov 2019 15:39:03 +0000 Subject: [PATCH 010/215] INSTALL.md: fix broken wiki link --- INSTALL.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 345a4198ee..f86bd2b20d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,9 +2,7 @@ Build and Install KeePassXC ================= This document will guide you through the steps to build and install KeePassXC from source. -You can visit the online version of this document at the following link: - -https://github.com/keepassxreboot/keepassx/wiki/Install-Instruction-from-Source +For more information, see also the [_Building KeePassXC_](https://github.com/keepassxreboot/keepassxc/wiki/Building-KeePassXC) page on the wiki. The [KeePassXC QuickStart](./docs/QUICKSTART.md) gets you started using KeePassXC on your Windows, Mac, or Linux computer using the pre-built binaries. From 7b95867378ae515075e9d3df0653e6d52573253e Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Fri, 29 Nov 2019 13:45:14 -0500 Subject: [PATCH 011/215] Code format fixes --- src/browser/BrowserService.cpp | 6 +++--- src/cli/Analyze.cpp | 3 +-- src/core/Database.h | 2 +- src/gui/DatabaseOpenWidget.cpp | 8 +++++--- src/gui/DatabaseTabWidget.cpp | 3 ++- src/gui/dbsettings/DatabaseSettingsWidget.cpp | 1 - src/gui/masterkey/KeyFileEditWidget.h | 1 - tests/TestBrowser.cpp | 1 - tests/TestCli.cpp | 2 +- tests/TestFdoSecrets.cpp | 4 ++-- tests/util/TemporaryFile.cpp | 2 +- 11 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 392d28c438..c00229fd8e 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -606,8 +606,7 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& // Search for additional URL's starting with KP2A_URL if (entry->attributes()->keys().contains(ADDITIONAL_URL)) { for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) - && handleURL(entry->attributes()->value(key), domain, url)) { + if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), domain, url)) { entries.append(entry); continue; } @@ -1029,7 +1028,8 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, } // Match scheme - if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty() && entryQUrl.scheme().compare(qUrl.scheme()) != 0) { + if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty() + && entryQUrl.scheme().compare(qUrl.scheme()) != 0) { return false; } diff --git a/src/cli/Analyze.cpp b/src/cli/Analyze.cpp index 7fc00c8eff..6095e988b3 100644 --- a/src/cli/Analyze.cpp +++ b/src/cli/Analyze.cpp @@ -55,8 +55,7 @@ int Analyze::executeWithDatabase(QSharedPointer database, QSharedPoint return EXIT_FAILURE; } - outputTextStream << QObject::tr("Evaluating database entries against HIBP file, this will take a while...") - << endl; + outputTextStream << QObject::tr("Evaluating database entries against HIBP file, this will take a while...") << endl; QList> findings; QString error; diff --git a/src/core/Database.h b/src/core/Database.h index 9c99299459..d5f2092b21 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -29,8 +29,8 @@ #include "crypto/kdf/AesKdf.h" #include "crypto/kdf/Kdf.h" #include "format/KeePass2.h" -#include "keys/PasswordKey.h" #include "keys/CompositeKey.h" +#include "keys/PasswordKey.h" class Entry; enum class EntryReferenceType; diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index a409aadc36..f90fd37d1f 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -370,9 +370,11 @@ void DatabaseOpenWidget::browseKeyFile() QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters); if (QFileInfo(filename).canonicalFilePath() == QFileInfo(m_filename).canonicalFilePath()) { - MessageBox::warning(this, tr("Cannot use database file as key file"), - tr("You cannot use your database file as a key file.\nIf you do not have a key file, please leave the field empty."), - MessageBox::Button::Ok); + MessageBox::warning(this, + tr("Cannot use database file as key file"), + tr("You cannot use your database file as a key file.\nIf you do not have a key file, " + "please leave the field empty."), + MessageBox::Button::Ok); filename = ""; } diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 9cbfa8fd7b..c807264171 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -166,7 +166,8 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath, for (int i = 0, c = count(); i < c; ++i) { auto* dbWidget = databaseWidgetFromIndex(i); Q_ASSERT(dbWidget); - if (dbWidget && dbWidget->database()->canonicalFilePath().compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) { + if (dbWidget + && dbWidget->database()->canonicalFilePath().compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) { dbWidget->performUnlockDatabase(password, keyfile); if (!inBackground) { // switch to existing tab if file is already open diff --git a/src/gui/dbsettings/DatabaseSettingsWidget.cpp b/src/gui/dbsettings/DatabaseSettingsWidget.cpp index 224c4e5655..c7f7f400df 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidget.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidget.cpp @@ -48,4 +48,3 @@ const QSharedPointer DatabaseSettingsWidget::getDatabase() const { return m_db; } - diff --git a/src/gui/masterkey/KeyFileEditWidget.h b/src/gui/masterkey/KeyFileEditWidget.h index 7d5868e88a..dd414e133f 100644 --- a/src/gui/masterkey/KeyFileEditWidget.h +++ b/src/gui/masterkey/KeyFileEditWidget.h @@ -32,7 +32,6 @@ class KeyFileEditWidget : public KeyComponentWidget { Q_OBJECT - public: explicit KeyFileEditWidget(DatabaseSettingsWidget* parent); Q_DISABLE_COPY(KeyFileEditWidget); diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index bb3318d07a..818dfaebdf 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -400,7 +400,6 @@ void TestBrowser::testSortEntries() QCOMPARE(result[2]->url(), QString("https://github.com/login")); QCOMPARE(result[3]->username(), QString("User 3")); QCOMPARE(result[3]->url(), QString("github.com/login")); - } void TestBrowser::testGetDatabaseGroups() diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 8a9ab50cea..9a2756eac2 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -23,13 +23,13 @@ #include "core/Global.h" #include "core/Tools.h" #include "crypto/Crypto.h" -#include "keys/drivers/YubiKey.h" #include "format/Kdbx3Reader.h" #include "format/Kdbx3Writer.h" #include "format/Kdbx4Reader.h" #include "format/Kdbx4Writer.h" #include "format/KdbxXmlReader.h" #include "format/KeePass2.h" +#include "keys/drivers/YubiKey.h" #include "cli/Add.h" #include "cli/AddGroup.h" diff --git a/tests/TestFdoSecrets.cpp b/tests/TestFdoSecrets.cpp index 6994f60abf..30d8da5c7d 100644 --- a/tests/TestFdoSecrets.cpp +++ b/tests/TestFdoSecrets.cpp @@ -21,9 +21,9 @@ #include "core/EntrySearcher.h" #include "fdosecrets/GcryptMPI.h" -#include "fdosecrets/objects/SessionCipher.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/SessionCipher.h" #include "crypto/Crypto.h" @@ -96,8 +96,8 @@ void TestFdoSecrets::testDhIetf1024Sha256Aes128CbcPkcs7() void TestFdoSecrets::testCrazyAttributeKey() { - using FdoSecrets::Item; using FdoSecrets::Collection; + using FdoSecrets::Item; const QScopedPointer root(new Group()); const QScopedPointer e1(new Entry()); diff --git a/tests/util/TemporaryFile.cpp b/tests/util/TemporaryFile.cpp index 19622faedf..3b1e3a589c 100644 --- a/tests/util/TemporaryFile.cpp +++ b/tests/util/TemporaryFile.cpp @@ -71,7 +71,7 @@ bool TemporaryFile::copyFromFile(const QString& otherFileName) } QByteArray data; - while(!(data = otherFile.read(1024)).isEmpty()) { + while (!(data = otherFile.read(1024)).isEmpty()) { write(data); } From ae471bea14c8e05bcf368168649399b116a69992 Mon Sep 17 00:00:00 2001 From: Lars Wendler Date: Tue, 7 Jan 2020 17:44:08 -0500 Subject: [PATCH 012/215] CMakeLists.txt: Do not unconditionally use ccache This causes build failures in Gentoo because we don't allow access to ccache files if ccache is not enabled for build. Fix this by adding a WITH_CCACHE cmake option and change behavior so that cmake fails if WITH_CCACHE is enabled but ccache program cannot be found. Gentoo-bug: https://bugs.gentoo.org/704560 Signed-off-by: Lars Wendler --- CMakeLists.txt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2f9b5bfe8..1c5746c594 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,13 +27,6 @@ string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) -# Use the Compiler Cache (ccache) if it is installed -# (install with: sudo apt get ccache) -find_program (CCACHE_FOUND ccache) -if (CCACHE_FOUND) - set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) -endif (CCACHE_FOUND) - # Support Visual Studio Code include(CMakeToolsHelpers OPTIONAL) include(FeatureSummary) @@ -48,6 +41,7 @@ option(WITH_DEV_BUILD "Use only for development. Disables/warns about deprecated option(WITH_ASAN "Enable address sanitizer checks (Linux / macOS only)" OFF) option(WITH_COVERAGE "Use to build with coverage tests (GCC only)." OFF) option(WITH_APP_BUNDLE "Enable Application Bundle for macOS" ON) +option(WITH_CCACHE "Use ccache for build" OFF) set(WITH_XC_ALL OFF CACHE BOOL "Build in all available plugins") @@ -65,6 +59,17 @@ if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() +if(WITH_CCACHE) + # Use the Compiler Cache (ccache) program + # (install with: sudo apt get ccache) + find_program (CCACHE_FOUND ccache) + if(CCACHE_FOUND) + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) + else() + message(FATAL_ERROR "ccache requested but cannot be found.") + endif() +endif() + if(WITH_XC_ALL) # Enable all options (except update check) set(WITH_XC_AUTOTYPE ON) From edea88b5358d9b4ea7028c8bcd55176f4682ed38 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Tue, 7 Jan 2020 16:34:40 -0500 Subject: [PATCH 013/215] Use SHA256 Digest for Code Signing on Windows --- release-tool | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release-tool b/release-tool index 6d217ca9db..f6b8c8e275 100755 --- a/release-tool +++ b/release-tool @@ -1286,8 +1286,8 @@ appsign() { # osslsigncode does not succeed at signing MSI files at this time... logInfo "Signing file '${f}' using Microsoft signtool..." - signtool sign -f "${key}" -p "${password}" -d "KeePassXC" \ - -t "http://timestamp.comodoca.com/authenticode" "${f}" + signtool sign -f "${key}" -p "${password}" -d "KeePassXC" -td sha256 \ + -fd sha256 -tr "http://timestamp.comodoca.com/authenticode" "${f}" if [ 0 -ne $? ]; then exitError "Signing failed!" From 04d6d675a51338659ebb8ef1b82ea12a4f8dfb71 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Tue, 7 Jan 2020 17:56:34 -0500 Subject: [PATCH 014/215] Only use bare minimum settings for portable version * Fixes #4103 * This ini file ensures the portable distribution populates with the default settings from the code and not from outdated ini file. --- share/keepassxc.ini | 72 +++++++-------------------------------------- 1 file changed, 10 insertions(+), 62 deletions(-) diff --git a/share/keepassxc.ini b/share/keepassxc.ini index d40572659e..ab450d485e 100644 --- a/share/keepassxc.ini +++ b/share/keepassxc.ini @@ -1,62 +1,10 @@ -[General] -RememberLastDatabases=true -RememberLastKeyFiles=true -OpenPreviousDatabasesOnStartup=true -AutoSaveAfterEveryChange=false -AutoSaveOnExit=false -AutoReloadOnChange=true -HideWindowOnCopy=false -MinimizeOnCopy=true -DropToBackgroundOnCopy=false -MinimizeOnOpenUrl=false -UseGroupIconOnEntryCreation=true -IgnoreGroupExpansion=false -AutoTypeEntryTitleMatch=true -GlobalAutoTypeKey=0 -GlobalAutoTypeModifiers=0 -LastOpenedDatabases=@Invalid() - -[GUI] -Language=system -ShowTrayIcon=false -DarkTrayIcon=false -MinimizeToTray=false -MinimizeOnClose=false -MinimizeOnStartup=false -MonospaceNotes=false -MainWindowGeometry="@ByteArray(\x1\xd9\xd0\xcb\0\x2\0\0\0\0\x2(\0\0\0\xbd\0\0\x5W\0\0\x3;\0\0\x2\x30\0\0\0\xdc\0\0\x5O\0\0\x3\x33\0\0\0\0\0\0\0\0\a\x80)" -SplitterState=@Invalid() -EntryListColumnSizes=@Invalid() -EntrySearchColumnSizes=@Invalid() - -[security] -autotypeask=true -clearclipboard=true -clearclipboardtimeout=10 -lockdatabaseidle=false -lockdatabaseidlesec=240 -lockdatabaseminimize=false -lockdatabasescreenlock=true -passwordscleartext=false -passwordemptynodots=true -passwordsrepeat=false - -[Http] -Enabled=false -ShowNotification=true -BestMatchOnly=false -UnlockDatabase=true -MatchUrlScheme=true -SortByUsername=false -Port=19455 -AlwaysAllowAccess=false -AlwaysAllowUpdate=false -SearchInAllDatabases=false -SupportKphFields=true -generator\LowerCase=true -generator\UpperCase=true -generator\Numbers=true -generator\SpecialChars=false -generator\ExcludeAlike=true -generator\EnsureEvery=true -generator\Length=16 +[General] +UpdateCheckMessageShown=false +LastActiveDatabase=@Invalid() +LastOpenedDatabases=@Invalid() +HideWindowOnCopy=false +MinimizeOnCopy=true + +[GUI] +HideUsernames=false +HidePasswords=true From 3fdafc6d25e85050976e0cc645db579086db3f45 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Tue, 7 Jan 2020 18:19:02 -0500 Subject: [PATCH 015/215] Prevent crash if Auto-Type performed on new entry * Check that entry's group is not nullptr * Fixes #3967 --- src/autotype/AutoType.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 299299b8c8..80a2268ec3 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -588,12 +588,12 @@ QList AutoType::createActionFromTemplate(const QString& tmpl, c QList AutoType::autoTypeSequences(const Entry* entry, const QString& windowTitle) { QList sequenceList; + const Group* group = entry->group(); - if (!entry->autoTypeEnabled()) { + if (!group || !entry->autoTypeEnabled()) { return sequenceList; } - const Group* group = entry->group(); do { if (group->autoTypeEnabled() == Group::Disable) { return sequenceList; From 36f92b7649b8afa040d2630621f429201e591d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= Date: Thu, 26 Dec 2019 21:32:12 +0100 Subject: [PATCH 016/215] Replace application icons with Material Design icons. Use the following to run KeePassXC with the icons from the source code, ignoring the operating system's Qt icon theme: ``` KEEPASSXC_IGNORE_ICON_THEME=1 keepassxc ``` The patch further adds a script `makeicons.sh` that re-creates KeePassXC icons from the Material Design icon set and can be used for easily updating icons in the future. Instructions are in the script. Fixes #475 --- COPYING | 138 +- .../128x128/apps/keepassxc-dark.png | Bin 5053 -> 0 bytes .../128x128/apps/keepassxc-locked.png | Bin 6965 -> 0 bytes .../128x128/apps/keepassxc-unlocked.png | Bin 7594 -> 0 bytes .../application/128x128/apps/keepassxc.png | Bin 7594 -> 0 bytes .../preferences-system-network-sharing.png | Bin 13769 -> 0 bytes .../mimetypes/application-x-keepassxc.png | Bin 6605 -> 0 bytes .../16x16/actions/application-exit.png | Bin 851 -> 0 bytes .../application/16x16/actions/auto-type.png | Bin 860 -> 0 bytes .../application/16x16/actions/configure.png | Bin 606 -> 0 bytes .../16x16/actions/database-change-key.png | Bin 646 -> 0 bytes .../16x16/actions/database-lock.png | Bin 454 -> 0 bytes .../16x16/actions/document-close.png | Bin 613 -> 0 bytes .../16x16/actions/document-edit.png | Bin 665 -> 0 bytes .../16x16/actions/document-new.png | Bin 509 -> 0 bytes .../16x16/actions/document-open.png | Bin 649 -> 0 bytes .../16x16/actions/document-save-as.png | Bin 773 -> 0 bytes .../16x16/actions/document-save.png | Bin 500 -> 0 bytes .../actions/edit-clear-locationbar-ltr.png | Bin 804 -> 0 bytes .../actions/edit-clear-locationbar-rtl.png | Bin 571 -> 0 bytes .../application/16x16/actions/entry-clone.png | Bin 735 -> 0 bytes .../16x16/actions/entry-delete.png | Bin 713 -> 0 bytes .../application/16x16/actions/entry-edit.png | Bin 797 -> 0 bytes .../application/16x16/actions/entry-new.png | Bin 832 -> 0 bytes .../16x16/actions/favicon-download.png | Bin 754 -> 0 bytes .../16x16/actions/group-delete.png | Bin 627 -> 0 bytes .../application/16x16/actions/group-edit.png | Bin 608 -> 0 bytes .../16x16/actions/group-empty-trash.png | Bin 880 -> 0 bytes .../application/16x16/actions/group-new.png | Bin 601 -> 0 bytes .../application/16x16/actions/help-about.png | Bin 704 -> 0 bytes .../16x16/actions/message-close.png | Bin 301 -> 0 bytes .../application/16x16/actions/paperclip.png | Bin 478 -> 0 bytes .../16x16/actions/password-copy.png | Bin 661 -> 0 bytes .../16x16/actions/password-generate.png | Bin 903 -> 0 bytes .../16x16/actions/password-generator.png | Bin 444 -> 0 bytes .../16x16/actions/password-show-off.png | Bin 738 -> 0 bytes .../16x16/actions/password-show-on.png | Bin 763 -> 0 bytes .../application/16x16/actions/system-help.png | Bin 866 -> 0 bytes .../16x16/actions/system-search.png | Bin 740 -> 0 bytes .../application/16x16/actions/url-copy.png | Bin 801 -> 0 bytes .../16x16/actions/username-copy.png | Bin 763 -> 0 bytes .../application/16x16/apps/keepassxc-dark.png | Bin 492 -> 0 bytes .../16x16/apps/keepassxc-locked.png | Bin 718 -> 0 bytes .../16x16/apps/keepassxc-unlocked.png | Bin 715 -> 0 bytes .../application/16x16/apps/keepassxc.png | Bin 715 -> 0 bytes .../mimetypes/application-x-keepassxc.png | Bin 627 -> 0 bytes .../application/22x22/actions/auto-type.png | Bin 1252 -> 0 bytes .../application/22x22/actions/chronometer.png | Bin 1579 -> 0 bytes .../22x22/actions/database-change-key.png | Bin 984 -> 0 bytes .../22x22/actions/database-lock.png | Bin 583 -> 0 bytes .../22x22/actions/dialog-close.png | Bin 1169 -> 0 bytes .../application/22x22/actions/dialog-ok.png | Bin 702 -> 0 bytes .../22x22/actions/document-new.png | Bin 1213 -> 0 bytes .../22x22/actions/document-open.png | Bin 1125 -> 0 bytes .../22x22/actions/document-save.png | Bin 820 -> 0 bytes .../application/22x22/actions/entry-clone.png | Bin 1157 -> 0 bytes .../22x22/actions/entry-delete.png | Bin 1114 -> 0 bytes .../application/22x22/actions/entry-edit.png | Bin 1188 -> 0 bytes .../application/22x22/actions/entry-new.png | Bin 1324 -> 0 bytes .../22x22/actions/favicon-download.png | Bin 1047 -> 0 bytes .../22x22/actions/group-empty-trash.png | Bin 1297 -> 0 bytes .../application/22x22/actions/help-about.png | Bin 1113 -> 0 bytes .../22x22/actions/message-close.png | Bin 384 -> 0 bytes .../application/22x22/actions/paperclip.png | Bin 574 -> 0 bytes .../22x22/actions/password-copy.png | Bin 960 -> 0 bytes .../22x22/actions/password-generate.png | Bin 1369 -> 0 bytes .../22x22/actions/password-generator.png | Bin 620 -> 0 bytes .../application/22x22/actions/system-help.png | Bin 1186 -> 0 bytes .../22x22/actions/system-search.png | Bin 1302 -> 0 bytes .../application/22x22/actions/url-copy.png | Bin 1221 -> 0 bytes .../22x22/actions/username-copy.png | Bin 1099 -> 0 bytes .../mimetypes/application-x-keepassxc.png | Bin 898 -> 0 bytes .../application/22x22/status/dialog-error.png | Bin 968 -> 0 bytes .../22x22/status/dialog-information.png | Bin 1084 -> 0 bytes .../22x22/status/dialog-warning.png | Bin 779 -> 0 bytes .../application/24x24/apps/keepassxc-dark.png | Bin 792 -> 0 bytes .../24x24/apps/keepassxc-locked.png | Bin 1163 -> 0 bytes .../24x24/apps/keepassxc-unlocked.png | Bin 1160 -> 0 bytes .../application/24x24/apps/keepassxc.png | Bin 1160 -> 0 bytes .../256x256/apps/keepassxc-dark.png | Bin 11605 -> 0 bytes .../256x256/apps/keepassxc-locked.png | Bin 15385 -> 0 bytes .../256x256/apps/keepassxc-unlocked.png | Bin 16827 -> 0 bytes .../application/256x256/apps/keepassxc.png | Bin 16827 -> 0 bytes .../32x32/actions/application-exit.png | Bin 1804 -> 0 bytes .../application/32x32/actions/auto-type.png | Bin 1896 -> 0 bytes .../application/32x32/actions/chronometer.png | Bin 7142 -> 0 bytes .../application/32x32/actions/configure.png | Bin 1458 -> 0 bytes .../32x32/actions/database-change-key.png | Bin 1627 -> 0 bytes .../32x32/actions/database-lock.png | Bin 1090 -> 0 bytes .../32x32/actions/dialog-close.png | Bin 2054 -> 0 bytes .../application/32x32/actions/dialog-ok.png | Bin 1246 -> 0 bytes .../32x32/actions/document-close.png | Bin 1592 -> 0 bytes .../32x32/actions/document-edit.png | Bin 1802 -> 0 bytes .../32x32/actions/document-new.png | Bin 1470 -> 0 bytes .../32x32/actions/document-open.png | Bin 1798 -> 0 bytes .../32x32/actions/document-properties.png | Bin 1342 -> 0 bytes .../32x32/actions/document-save.png | Bin 1299 -> 0 bytes .../actions/edit-clear-locationbar-ltr.png | Bin 1935 -> 0 bytes .../actions/edit-clear-locationbar-rtl.png | Bin 1340 -> 0 bytes .../application/32x32/actions/entry-clone.png | Bin 1949 -> 0 bytes .../32x32/actions/entry-delete.png | Bin 2156 -> 0 bytes .../application/32x32/actions/entry-edit.png | Bin 2021 -> 0 bytes .../application/32x32/actions/entry-new.png | Bin 2256 -> 0 bytes .../32x32/actions/favicon-download.png | Bin 1591 -> 0 bytes .../32x32/actions/group-empty-trash.png | Bin 2162 -> 0 bytes .../application/32x32/actions/help-about.png | Bin 1839 -> 0 bytes .../application/32x32/actions/key-enter.png | Bin 760 -> 0 bytes .../application/32x32/actions/paperclip.png | Bin 1014 -> 0 bytes .../32x32/actions/password-copy.png | Bin 1431 -> 0 bytes .../32x32/actions/password-generate.png | Bin 2457 -> 0 bytes .../32x32/actions/password-generator.png | Bin 1666 -> 0 bytes .../32x32/actions/password-show-off.png | Bin 1849 -> 0 bytes .../32x32/actions/password-show-on.png | Bin 2089 -> 0 bytes .../application/32x32/actions/statistics.png | Bin 1323 -> 0 bytes .../application/32x32/actions/system-help.png | Bin 2141 -> 0 bytes .../32x32/actions/system-search.png | Bin 2023 -> 0 bytes .../application/32x32/actions/url-copy.png | Bin 2002 -> 0 bytes .../32x32/actions/username-copy.png | Bin 1722 -> 0 bytes .../32x32/actions/view-history.png | Bin 1566 -> 0 bytes .../32x32/apps/internet-web-browser.png | Bin 1559 -> 0 bytes .../application/32x32/apps/keepassxc-dark.png | Bin 1063 -> 0 bytes .../32x32/apps/keepassxc-locked.png | Bin 1574 -> 0 bytes .../32x32/apps/keepassxc-unlocked.png | Bin 1591 -> 0 bytes .../application/32x32/apps/keepassxc.png | Bin 1591 -> 0 bytes .../32x32/apps/preferences-desktop-icons.png | Bin 2264 -> 0 bytes .../32x32/apps/utilities-terminal.png | Bin 999 -> 0 bytes .../32x32/categories/preferences-other.png | Bin 1997 -> 0 bytes .../mimetypes/application-x-keepassxc.png | Bin 1370 -> 0 bytes .../32x32/status/security-high.png | Bin 1625 -> 0 bytes .../application/48x48/apps/keepassxc-dark.png | Bin 1661 -> 0 bytes .../48x48/apps/keepassxc-locked.png | Bin 2398 -> 0 bytes .../48x48/apps/keepassxc-unlocked.png | Bin 2440 -> 0 bytes .../application/48x48/apps/keepassxc.png | Bin 2440 -> 0 bytes .../application/64x64/apps/keepassxc-dark.png | Bin 2394 -> 0 bytes .../64x64/apps/keepassxc-locked.png | Bin 3321 -> 0 bytes .../64x64/apps/keepassxc-unlocked.png | Bin 3458 -> 0 bytes .../application/64x64/apps/keepassxc.png | Bin 3458 -> 0 bytes .../mimetypes/application-x-keepassxc.png | Bin 3122 -> 0 bytes .../scalable/actions/application-exit.svg | 1 + .../scalable/actions/auto-type.svg | 1 + .../scalable/actions/bugreport.svg | 1 + .../scalable/actions/chronometer.svg | 1 + .../scalable/actions/configure.svg | 1 + .../scalable/actions/database-change-key.svg | 1 + .../scalable/actions/database-lock.svg | 1 + .../scalable/actions/database-merge.svg | 1 + .../scalable/actions/dialog-close.svg | 1 + .../scalable/actions/dialog-ok.svg | 1 + .../scalable/actions/document-close.svg | 1 + .../scalable/actions/document-edit.svg | 1 + .../scalable/actions/document-new.svg | 1 + .../scalable/actions/document-open.svg | 1 + .../scalable/actions/document-properties.svg | 1 + .../scalable/actions/document-save-as.svg | 1 + .../scalable/actions/document-save.svg | 1 + .../application/scalable/actions/donate.svg | 1 + .../actions/edit-clear-locationbar-ltr.svg | 1 + .../actions/edit-clear-locationbar-rtl.svg | 1 + .../scalable/actions/entry-clone.svg | 1 + .../scalable/actions/entry-delete.svg | 1 + .../scalable/actions/entry-edit.svg | 1 + .../scalable/actions/entry-new.svg | 1 + .../scalable/actions/favicon-download.svg | 1 + .../scalable/actions/getting-started.svg | 1 + .../scalable/actions/group-delete.svg | 1 + .../scalable/actions/group-edit.svg | 1 + .../scalable/actions/group-empty-trash.svg | 1 + .../scalable/actions/group-new.svg | 1 + .../scalable/actions/help-about.svg | 1 + .../scalable/actions/key-enter.svg | 1 + .../scalable/actions/keyboard-shortcuts.svg | 1 + .../scalable/actions/message-close.svg | 1 + .../scalable/actions/object-locked.svg | 15 +- .../scalable/actions/object-unlocked.svg | 16 +- .../scalable/actions/paperclip.svg | 1 + .../scalable/actions/password-copy.svg | 1 + .../scalable/actions/password-generate.svg | 1 + .../scalable/actions/password-generator.svg | 1 + .../scalable/actions/password-show-off.svg | 1 + .../scalable/actions/password-show-on.svg | 1 + .../actions/sort-alphabetical-ascending.svg | 1 + .../actions/sort-alphabetical-descending.svg | 1 + .../scalable/actions/statistics.svg | 1 + .../scalable/actions/system-help.svg | 1 + .../scalable/actions/system-search.svg | 1 + .../actions/system-software-update.svg | 1 + .../application/scalable/actions/url-copy.svg | 1 + .../scalable/actions/user-guide.svg | 1 + .../scalable/actions/username-copy.svg | 1 + .../scalable/actions/view-history.svg | 1 + .../application/scalable/actions/web.svg | 1 + .../application/scalable/apps/freedesktop.svg | 4 +- .../scalable/apps/internet-web-browser.svg | 1 + .../apps/preferences-desktop-icons.svg | 1 + .../preferences-system-network-sharing.svg | 1 + .../scalable/apps/utilities-terminal.svg | 1 + .../scalable/categories/preferences-other.svg | 1 + .../scalable/status/dialog-error.svg | 1 + .../scalable/status/dialog-information.svg | 1 + .../scalable/status/dialog-warning.svg | 1 + .../scalable/status/security-high.svg | 1 + share/icons/svg/application-exit.svg | 2 - share/icons/svg/application-x-keepassxc.svg | 2 - share/icons/svg/auto-type.png | Bin 7352 -> 0 bytes share/icons/svg/configure.svg | 2 - share/icons/svg/dialog-close.svg | 238 --- share/icons/svg/dialog-error.svg | 474 ----- share/icons/svg/dialog-information.svg | 370 ---- share/icons/svg/dialog-ok.svg | 390 ----- share/icons/svg/dialog-warning.svg | 383 ----- share/icons/svg/document-close.svg | 426 ----- share/icons/svg/document-edit.svg | 634 ------- share/icons/svg/document-new.svg | 477 ------ share/icons/svg/document-open.svg | 2 - share/icons/svg/document-properties.svg | 601 ------- share/icons/svg/document-save-as.svg | 2 - share/icons/svg/document-save.svg | 2 - .../icons/svg/edit-clear-locationbar-ltr.svg | 391 ----- .../icons/svg/edit-clear-locationbar-rtl.svg | 380 ----- share/icons/svg/internet-web-browser.svg | 4 - share/icons/svg/key-enter.svg | 265 --- share/icons/svg/message-close.svg | 41 - share/icons/svg/paperclip.svg | 108 -- share/icons/svg/password-copy.svg | 2 - share/icons/svg/password-generator.svg | 568 ------ share/icons/svg/preferences-desktop-icons.svg | 14 - share/icons/svg/preferences-other.svg | 1012 ----------- share/icons/svg/security-high.svg | 380 ----- share/icons/svg/system-search.svg | 2 - share/icons/svg/url-copy.svg | 2 - share/icons/svg/username-copy.svg | 2 - share/icons/svg/utilities-terminal.svg | 1517 ----------------- share/icons/svg/view-history.svg | 753 -------- src/core/FilePath.cpp | 2 +- src/gui/LineEdit.cpp | 13 +- src/gui/MainWindow.cpp | 10 + src/gui/MainWindow.ui | 4 +- utils/makeicons.sh | 160 ++ 238 files changed, 306 insertions(+), 9562 deletions(-) delete mode 100644 share/icons/application/128x128/apps/keepassxc-dark.png delete mode 100644 share/icons/application/128x128/apps/keepassxc-locked.png delete mode 100644 share/icons/application/128x128/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/128x128/apps/keepassxc.png delete mode 100644 share/icons/application/128x128/apps/preferences-system-network-sharing.png delete mode 100644 share/icons/application/128x128/mimetypes/application-x-keepassxc.png delete mode 100644 share/icons/application/16x16/actions/application-exit.png delete mode 100644 share/icons/application/16x16/actions/auto-type.png delete mode 100644 share/icons/application/16x16/actions/configure.png delete mode 100644 share/icons/application/16x16/actions/database-change-key.png delete mode 100644 share/icons/application/16x16/actions/database-lock.png delete mode 100644 share/icons/application/16x16/actions/document-close.png delete mode 100644 share/icons/application/16x16/actions/document-edit.png delete mode 100644 share/icons/application/16x16/actions/document-new.png delete mode 100644 share/icons/application/16x16/actions/document-open.png delete mode 100644 share/icons/application/16x16/actions/document-save-as.png delete mode 100644 share/icons/application/16x16/actions/document-save.png delete mode 100644 share/icons/application/16x16/actions/edit-clear-locationbar-ltr.png delete mode 100644 share/icons/application/16x16/actions/edit-clear-locationbar-rtl.png delete mode 100644 share/icons/application/16x16/actions/entry-clone.png delete mode 100644 share/icons/application/16x16/actions/entry-delete.png delete mode 100644 share/icons/application/16x16/actions/entry-edit.png delete mode 100644 share/icons/application/16x16/actions/entry-new.png delete mode 100644 share/icons/application/16x16/actions/favicon-download.png delete mode 100644 share/icons/application/16x16/actions/group-delete.png delete mode 100644 share/icons/application/16x16/actions/group-edit.png delete mode 100644 share/icons/application/16x16/actions/group-empty-trash.png delete mode 100644 share/icons/application/16x16/actions/group-new.png delete mode 100644 share/icons/application/16x16/actions/help-about.png delete mode 100644 share/icons/application/16x16/actions/message-close.png delete mode 100644 share/icons/application/16x16/actions/paperclip.png delete mode 100644 share/icons/application/16x16/actions/password-copy.png delete mode 100644 share/icons/application/16x16/actions/password-generate.png delete mode 100644 share/icons/application/16x16/actions/password-generator.png delete mode 100644 share/icons/application/16x16/actions/password-show-off.png delete mode 100644 share/icons/application/16x16/actions/password-show-on.png delete mode 100644 share/icons/application/16x16/actions/system-help.png delete mode 100644 share/icons/application/16x16/actions/system-search.png delete mode 100644 share/icons/application/16x16/actions/url-copy.png delete mode 100644 share/icons/application/16x16/actions/username-copy.png delete mode 100644 share/icons/application/16x16/apps/keepassxc-dark.png delete mode 100644 share/icons/application/16x16/apps/keepassxc-locked.png delete mode 100644 share/icons/application/16x16/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/16x16/apps/keepassxc.png delete mode 100644 share/icons/application/16x16/mimetypes/application-x-keepassxc.png delete mode 100644 share/icons/application/22x22/actions/auto-type.png delete mode 100644 share/icons/application/22x22/actions/chronometer.png delete mode 100644 share/icons/application/22x22/actions/database-change-key.png delete mode 100644 share/icons/application/22x22/actions/database-lock.png delete mode 100644 share/icons/application/22x22/actions/dialog-close.png delete mode 100644 share/icons/application/22x22/actions/dialog-ok.png delete mode 100644 share/icons/application/22x22/actions/document-new.png delete mode 100644 share/icons/application/22x22/actions/document-open.png delete mode 100644 share/icons/application/22x22/actions/document-save.png delete mode 100644 share/icons/application/22x22/actions/entry-clone.png delete mode 100644 share/icons/application/22x22/actions/entry-delete.png delete mode 100644 share/icons/application/22x22/actions/entry-edit.png delete mode 100644 share/icons/application/22x22/actions/entry-new.png delete mode 100644 share/icons/application/22x22/actions/favicon-download.png delete mode 100644 share/icons/application/22x22/actions/group-empty-trash.png delete mode 100644 share/icons/application/22x22/actions/help-about.png delete mode 100644 share/icons/application/22x22/actions/message-close.png delete mode 100644 share/icons/application/22x22/actions/paperclip.png delete mode 100644 share/icons/application/22x22/actions/password-copy.png delete mode 100644 share/icons/application/22x22/actions/password-generate.png delete mode 100644 share/icons/application/22x22/actions/password-generator.png delete mode 100644 share/icons/application/22x22/actions/system-help.png delete mode 100644 share/icons/application/22x22/actions/system-search.png delete mode 100644 share/icons/application/22x22/actions/url-copy.png delete mode 100644 share/icons/application/22x22/actions/username-copy.png delete mode 100644 share/icons/application/22x22/mimetypes/application-x-keepassxc.png delete mode 100644 share/icons/application/22x22/status/dialog-error.png delete mode 100644 share/icons/application/22x22/status/dialog-information.png delete mode 100644 share/icons/application/22x22/status/dialog-warning.png delete mode 100644 share/icons/application/24x24/apps/keepassxc-dark.png delete mode 100644 share/icons/application/24x24/apps/keepassxc-locked.png delete mode 100644 share/icons/application/24x24/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/24x24/apps/keepassxc.png delete mode 100644 share/icons/application/256x256/apps/keepassxc-dark.png delete mode 100644 share/icons/application/256x256/apps/keepassxc-locked.png delete mode 100644 share/icons/application/256x256/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/256x256/apps/keepassxc.png delete mode 100644 share/icons/application/32x32/actions/application-exit.png delete mode 100644 share/icons/application/32x32/actions/auto-type.png delete mode 100644 share/icons/application/32x32/actions/chronometer.png delete mode 100644 share/icons/application/32x32/actions/configure.png delete mode 100644 share/icons/application/32x32/actions/database-change-key.png delete mode 100644 share/icons/application/32x32/actions/database-lock.png delete mode 100644 share/icons/application/32x32/actions/dialog-close.png delete mode 100644 share/icons/application/32x32/actions/dialog-ok.png delete mode 100644 share/icons/application/32x32/actions/document-close.png delete mode 100644 share/icons/application/32x32/actions/document-edit.png delete mode 100644 share/icons/application/32x32/actions/document-new.png delete mode 100644 share/icons/application/32x32/actions/document-open.png delete mode 100644 share/icons/application/32x32/actions/document-properties.png delete mode 100644 share/icons/application/32x32/actions/document-save.png delete mode 100644 share/icons/application/32x32/actions/edit-clear-locationbar-ltr.png delete mode 100644 share/icons/application/32x32/actions/edit-clear-locationbar-rtl.png delete mode 100644 share/icons/application/32x32/actions/entry-clone.png delete mode 100644 share/icons/application/32x32/actions/entry-delete.png delete mode 100644 share/icons/application/32x32/actions/entry-edit.png delete mode 100644 share/icons/application/32x32/actions/entry-new.png delete mode 100644 share/icons/application/32x32/actions/favicon-download.png delete mode 100644 share/icons/application/32x32/actions/group-empty-trash.png delete mode 100644 share/icons/application/32x32/actions/help-about.png delete mode 100644 share/icons/application/32x32/actions/key-enter.png delete mode 100644 share/icons/application/32x32/actions/paperclip.png delete mode 100644 share/icons/application/32x32/actions/password-copy.png delete mode 100644 share/icons/application/32x32/actions/password-generate.png delete mode 100644 share/icons/application/32x32/actions/password-generator.png delete mode 100644 share/icons/application/32x32/actions/password-show-off.png delete mode 100644 share/icons/application/32x32/actions/password-show-on.png delete mode 100644 share/icons/application/32x32/actions/statistics.png delete mode 100644 share/icons/application/32x32/actions/system-help.png delete mode 100644 share/icons/application/32x32/actions/system-search.png delete mode 100644 share/icons/application/32x32/actions/url-copy.png delete mode 100644 share/icons/application/32x32/actions/username-copy.png delete mode 100644 share/icons/application/32x32/actions/view-history.png delete mode 100644 share/icons/application/32x32/apps/internet-web-browser.png delete mode 100644 share/icons/application/32x32/apps/keepassxc-dark.png delete mode 100644 share/icons/application/32x32/apps/keepassxc-locked.png delete mode 100644 share/icons/application/32x32/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/32x32/apps/keepassxc.png delete mode 100644 share/icons/application/32x32/apps/preferences-desktop-icons.png delete mode 100644 share/icons/application/32x32/apps/utilities-terminal.png delete mode 100644 share/icons/application/32x32/categories/preferences-other.png delete mode 100644 share/icons/application/32x32/mimetypes/application-x-keepassxc.png delete mode 100644 share/icons/application/32x32/status/security-high.png delete mode 100644 share/icons/application/48x48/apps/keepassxc-dark.png delete mode 100644 share/icons/application/48x48/apps/keepassxc-locked.png delete mode 100644 share/icons/application/48x48/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/48x48/apps/keepassxc.png delete mode 100644 share/icons/application/64x64/apps/keepassxc-dark.png delete mode 100644 share/icons/application/64x64/apps/keepassxc-locked.png delete mode 100644 share/icons/application/64x64/apps/keepassxc-unlocked.png delete mode 100644 share/icons/application/64x64/apps/keepassxc.png delete mode 100644 share/icons/application/64x64/mimetypes/application-x-keepassxc.png create mode 100644 share/icons/application/scalable/actions/application-exit.svg create mode 100644 share/icons/application/scalable/actions/auto-type.svg create mode 100644 share/icons/application/scalable/actions/bugreport.svg create mode 100644 share/icons/application/scalable/actions/chronometer.svg create mode 100644 share/icons/application/scalable/actions/configure.svg create mode 100644 share/icons/application/scalable/actions/database-change-key.svg create mode 100644 share/icons/application/scalable/actions/database-lock.svg create mode 100644 share/icons/application/scalable/actions/database-merge.svg create mode 100644 share/icons/application/scalable/actions/dialog-close.svg create mode 100644 share/icons/application/scalable/actions/dialog-ok.svg create mode 100644 share/icons/application/scalable/actions/document-close.svg create mode 100644 share/icons/application/scalable/actions/document-edit.svg create mode 100644 share/icons/application/scalable/actions/document-new.svg create mode 100644 share/icons/application/scalable/actions/document-open.svg create mode 100644 share/icons/application/scalable/actions/document-properties.svg create mode 100644 share/icons/application/scalable/actions/document-save-as.svg create mode 100644 share/icons/application/scalable/actions/document-save.svg create mode 100644 share/icons/application/scalable/actions/donate.svg create mode 100644 share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg create mode 100644 share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg create mode 100644 share/icons/application/scalable/actions/entry-clone.svg create mode 100644 share/icons/application/scalable/actions/entry-delete.svg create mode 100644 share/icons/application/scalable/actions/entry-edit.svg create mode 100644 share/icons/application/scalable/actions/entry-new.svg create mode 100644 share/icons/application/scalable/actions/favicon-download.svg create mode 100644 share/icons/application/scalable/actions/getting-started.svg create mode 100644 share/icons/application/scalable/actions/group-delete.svg create mode 100644 share/icons/application/scalable/actions/group-edit.svg create mode 100644 share/icons/application/scalable/actions/group-empty-trash.svg create mode 100644 share/icons/application/scalable/actions/group-new.svg create mode 100644 share/icons/application/scalable/actions/help-about.svg create mode 100644 share/icons/application/scalable/actions/key-enter.svg create mode 100644 share/icons/application/scalable/actions/keyboard-shortcuts.svg create mode 100644 share/icons/application/scalable/actions/message-close.svg create mode 100644 share/icons/application/scalable/actions/paperclip.svg create mode 100644 share/icons/application/scalable/actions/password-copy.svg create mode 100644 share/icons/application/scalable/actions/password-generate.svg create mode 100644 share/icons/application/scalable/actions/password-generator.svg create mode 100644 share/icons/application/scalable/actions/password-show-off.svg create mode 100644 share/icons/application/scalable/actions/password-show-on.svg create mode 100644 share/icons/application/scalable/actions/sort-alphabetical-ascending.svg create mode 100644 share/icons/application/scalable/actions/sort-alphabetical-descending.svg create mode 100644 share/icons/application/scalable/actions/statistics.svg create mode 100644 share/icons/application/scalable/actions/system-help.svg create mode 100644 share/icons/application/scalable/actions/system-search.svg create mode 100644 share/icons/application/scalable/actions/system-software-update.svg create mode 100644 share/icons/application/scalable/actions/url-copy.svg create mode 100644 share/icons/application/scalable/actions/user-guide.svg create mode 100644 share/icons/application/scalable/actions/username-copy.svg create mode 100644 share/icons/application/scalable/actions/view-history.svg create mode 100644 share/icons/application/scalable/actions/web.svg create mode 100644 share/icons/application/scalable/apps/internet-web-browser.svg create mode 100644 share/icons/application/scalable/apps/preferences-desktop-icons.svg create mode 100644 share/icons/application/scalable/apps/preferences-system-network-sharing.svg create mode 100644 share/icons/application/scalable/apps/utilities-terminal.svg create mode 100644 share/icons/application/scalable/categories/preferences-other.svg create mode 100644 share/icons/application/scalable/status/dialog-error.svg create mode 100644 share/icons/application/scalable/status/dialog-information.svg create mode 100644 share/icons/application/scalable/status/dialog-warning.svg create mode 100644 share/icons/application/scalable/status/security-high.svg delete mode 100644 share/icons/svg/application-exit.svg delete mode 100644 share/icons/svg/application-x-keepassxc.svg delete mode 100644 share/icons/svg/auto-type.png delete mode 100644 share/icons/svg/configure.svg delete mode 100644 share/icons/svg/dialog-close.svg delete mode 100644 share/icons/svg/dialog-error.svg delete mode 100644 share/icons/svg/dialog-information.svg delete mode 100644 share/icons/svg/dialog-ok.svg delete mode 100644 share/icons/svg/dialog-warning.svg delete mode 100644 share/icons/svg/document-close.svg delete mode 100644 share/icons/svg/document-edit.svg delete mode 100644 share/icons/svg/document-new.svg delete mode 100644 share/icons/svg/document-open.svg delete mode 100644 share/icons/svg/document-properties.svg delete mode 100644 share/icons/svg/document-save-as.svg delete mode 100644 share/icons/svg/document-save.svg delete mode 100644 share/icons/svg/edit-clear-locationbar-ltr.svg delete mode 100644 share/icons/svg/edit-clear-locationbar-rtl.svg delete mode 100644 share/icons/svg/internet-web-browser.svg delete mode 100644 share/icons/svg/key-enter.svg delete mode 100644 share/icons/svg/message-close.svg delete mode 100644 share/icons/svg/paperclip.svg delete mode 100644 share/icons/svg/password-copy.svg delete mode 100644 share/icons/svg/password-generator.svg delete mode 100644 share/icons/svg/preferences-desktop-icons.svg delete mode 100644 share/icons/svg/preferences-other.svg delete mode 100644 share/icons/svg/security-high.svg delete mode 100644 share/icons/svg/system-search.svg delete mode 100644 share/icons/svg/url-copy.svg delete mode 100644 share/icons/svg/username-copy.svg delete mode 100644 share/icons/svg/utilities-terminal.svg delete mode 100644 share/icons/svg/view-history.svg create mode 100644 utils/makeicons.sh diff --git a/COPYING b/COPYING index fe7d02f3eb..ee8c24f211 100644 --- a/COPYING +++ b/COPYING @@ -55,28 +55,15 @@ Files: cmake/GenerateProductVersion.cmake Copyright: 2015 halex2005 License: MIT -Files: share/icons/application/*/apps/keepassxc.png - share/icons/application/scalable/apps/keepassxc.svg - share/icons/application/*/apps/keepassxc-dark.png +Files: share/icons/application/scalable/apps/keepassxc.svg share/icons/application/scalable/apps/keepassxc-dark.svg - share/icons/application/*/apps/keepassxc-locked.png share/icons/application/scalable/apps/keepassxc-locked.svg - share/icons/application/*/apps/keepassxc-unlocked.png share/icons/application/scalable/apps/keepassxc-unlocked.svg - share/icons/application/*/mimetypes/application-x-keepassxc.png share/icons/application/scalable/mimetypes/application-x-keepassxc.svg Copyright: 2016, Lorenzo Stella License: LGPL-2 -Files: share/icons/application/*/actions/auto-type.png - share/icons/application/*/actions/database-change-key.png - share/icons/application/*/actions/entry-clone.png - share/icons/application/*/actions/entry-edit.png - share/icons/application/*/actions/entry-new.png - share/icons/application/*/actions/group-empty-trash.png - share/icons/application/*/actions/help-about.png - share/icons/application/*/actions/password-generate.png - share/icons/database/C00_Password.png +Files: share/icons/database/C00_Password.png share/icons/database/C01_Package_Network.png share/icons/database/C02_MessageBox_Warning.png share/icons/database/C03_Server.png @@ -142,60 +129,65 @@ Copyright: 2003-2004, David Vignoni License: LGPL-2.1 Comment: from Nuvola icon theme -Files: share/icons/application/*/actions/entry-delete.png - share/icons/application/*/actions/group-delete.png - share/icons/application/*/actions/group-edit.png - share/icons/application/*/actions/group-new.png -Copyright: 2003-2004, David Vignoni - 2012, Felix Geyer -License: LGPL-2.1 -Comment: based on Nuvola icon theme - -Files: share/icons/application/*/actions/favicon-download.png -Copyright: 2003-2004, David Vignoni - 2018, Kyle Kneitinger -License: LGPL-2.1 -Comment: based on Nuvola icon theme - -Files: share/icons/application/*/actions/application-exit.png - share/icons/application/*/actions/chronometer.png - share/icons/application/*/actions/configure.png - share/icons/application/*/actions/database-lock.png - share/icons/application/*/actions/dialog-close.png - share/icons/application/*/actions/dialog-ok.png - share/icons/application/*/actions/document-close.png - share/icons/application/*/actions/document-edit.png - share/icons/application/*/actions/document-new.png - share/icons/application/*/actions/document-open.png - share/icons/application/*/actions/document-properties.png - share/icons/application/*/actions/document-save.png - share/icons/application/*/actions/document-save-as.png - share/icons/application/*/actions/edit-clear-locationbar-ltr.png - share/icons/application/*/actions/edit-clear-locationbar-rtl.png - share/icons/application/*/actions/key-enter.png - share/icons/application/*/actions/password-generator.png - share/icons/application/*/actions/password-copy.png - share/icons/application/*/actions/password-show-*.png - share/icons/application/*/actions/system-search.png - share/icons/application/*/actions/username-copy.png - share/icons/application/*/actions/view-history.png - share/icons/application/*/apps/internet-web-browser.png - share/icons/application/*/apps/preferences-desktop-icons.png - share/icons/application/*/apps/utilities-terminal.png - share/icons/application/*/categories/preferences-other.png - share/icons/application/*/status/dialog-error.png - share/icons/application/*/status/dialog-information.png - share/icons/application/*/status/dialog-warning.png - share/icons/application/*/status/security-high.png - share/icons/svg/*.svg -Copyright: 2007, Nuno Pinheiro - 2007, David Vignoni - 2007, David Miller - 2007, Johann Ollivier Lapeyre - 2007, Kenneth Wimer - 2007, Riccardo Iaconelli -License: LGPL-3+ -Comment: from Oxygen icon theme (http://www.oxygen-icons.org/) +Files: share/icons/application/scalable/categories/preferences-other.svg + share/icons/application/scalable/apps/keepassxc-dark.svg + share/icons/application/scalable/apps/preferences-system-network-sharing.svg + share/icons/application/scalable/apps/utilities-terminal.svg + share/icons/application/scalable/apps/keepassxc-locked.svg + share/icons/application/scalable/apps/keepassxc-unlocked.svg + share/icons/application/scalable/apps/keepassxc.svg + share/icons/application/scalable/apps/freedesktop.svg + share/icons/application/scalable/apps/internet-web-browser.svg + share/icons/application/scalable/apps/preferences-desktop-icons.svg + share/icons/application/scalable/status/dialog-information.svg + share/icons/application/scalable/status/dialog-warning.svg + share/icons/application/scalable/status/dialog-error.svg + share/icons/application/scalable/status/security-high.svg + share/icons/application/scalable/mimetypes/application-x-keepassxc.svg + share/icons/application/scalable/actions/document-close.svg + share/icons/application/scalable/actions/application-exit.svg + share/icons/application/scalable/actions/database-change-key.svg + share/icons/application/scalable/actions/group-new.svg + share/icons/application/scalable/actions/document-properties.svg + share/icons/application/scalable/actions/group-empty-trash.svg + share/icons/application/scalable/actions/statistics.svg + share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg + share/icons/application/scalable/actions/entry-delete.svg + share/icons/application/scalable/actions/entry-clone.svg + share/icons/application/scalable/actions/entry-edit.svg + share/icons/application/scalable/actions/password-generator.svg + share/icons/application/scalable/actions/dialog-ok.svg + share/icons/application/scalable/actions/chronometer.svg + share/icons/application/scalable/actions/document-new.svg + share/icons/application/scalable/actions/view-history.svg + share/icons/application/scalable/actions/group-delete.svg + share/icons/application/scalable/actions/dialog-close.svg + share/icons/application/scalable/actions/group-edit.svg + share/icons/application/scalable/actions/document-save.svg + share/icons/application/scalable/actions/password-show-on.svg + share/icons/application/scalable/actions/message-close.svg + share/icons/application/scalable/actions/entry-new.svg + share/icons/application/scalable/actions/url-copy.svg + share/icons/application/scalable/actions/username-copy.svg + share/icons/application/scalable/actions/auto-type.svg + share/icons/application/scalable/actions/password-show-off.svg + share/icons/application/scalable/actions/paperclip.svg + share/icons/application/scalable/actions/configure.svg + share/icons/application/scalable/actions/database-lock.svg + share/icons/application/scalable/actions/password-copy.svg + share/icons/application/scalable/actions/system-help.svg + share/icons/application/scalable/actions/help-about.svg + share/icons/application/scalable/actions/system-search.svg + share/icons/application/scalable/actions/key-enter.svg + share/icons/application/scalable/actions/document-edit.svg + share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg + share/icons/application/scalable/actions/password-generate.svg + share/icons/application/scalable/actions/favicon-download.svg + share/icons/application/scalable/actions/document-open.svg + share/icons/application/scalable/actions/document-save-as.svg +Copyright: 2019 Austin Andrews +License: SIL OPEN FONT LICENSE Version 1.1 +Comment: Taken from Material Design icon set (https://github.com/templarian/MaterialDesign/) Files: share/icons/database/C62_Tux.png share/icons/database/C63_Feather.png @@ -245,11 +237,3 @@ License: MIT Files: share/icons/application/scalable/apps/freedesktop.svg Copyright: GPL-2+ Comment: from Freedesktop.org website - -Files: share/icons/application/32x32/actions/statistics.png -Copyright: Icon made by Freepik from https://www.flaticon.com/free-icon/bars-chart_265733 - -Files: share/icons/application/scalable/actions/object-locked.svg - share/icons/application/scalable/actions/object-unlocked.svg -License: LGPL-3 -Comment: from Breeze icon theme (https://github.com/KDE/breeze-icons) diff --git a/share/icons/application/128x128/apps/keepassxc-dark.png b/share/icons/application/128x128/apps/keepassxc-dark.png deleted file mode 100644 index d2cc1d5803b77fb440186cb79002c338050cc549..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5053 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 Hu0mjf7g704 diff --git a/share/icons/application/128x128/apps/keepassxc-unlocked.png b/share/icons/application/128x128/apps/keepassxc-unlocked.png deleted file mode 100644 index 30a8202613abd703ce6d5c6fbc15c9253fca5791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7594 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 M07*qoM6N<$g7_bQ4FCWD diff --git a/share/icons/application/128x128/apps/keepassxc.png b/share/icons/application/128x128/apps/keepassxc.png deleted file mode 100644 index 30a8202613abd703ce6d5c6fbc15c9253fca5791..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7594 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 M07*qoM6N<$g7_bQ4FCWD diff --git a/share/icons/application/128x128/apps/preferences-system-network-sharing.png b/share/icons/application/128x128/apps/preferences-system-network-sharing.png deleted file mode 100644 index 88a251701cef7e2899410293cc7bc7c9a8ac0b4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13769 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 diff --git a/share/icons/application/128x128/mimetypes/application-x-keepassxc.png b/share/icons/application/128x128/mimetypes/application-x-keepassxc.png deleted file mode 100644 index dac32fed487bd527338592c65cb859502f375dd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6605 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 diff --git a/share/icons/application/16x16/actions/database-change-key.png b/share/icons/application/16x16/actions/database-change-key.png deleted file mode 100644 index 186c07728f3688ef5bdc37787db587b2a0cdaf7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 646 zcmV;10(t$3P)@nHnVCN?}swyS?YMm}#1!7J&!O_W3=(_vIK>$i9mZ#S~Np|-v) zXZN;_7N1ToUE8{F{)5ZkA+GC2)6>%>KxYRd!($|O3NC9eZqR>vk>i5{xgYVNXFXR( zcQpzri7KjU9LLew**EWjJ1#)q2`&*I5BT zK%+QN2lxbGf=n)ljUrSP5ka-IB9;nZX@z&2dBA}81!QNFjn^;nrHOQ77HcgcQnS{| zAHeb^51v0Ej}HkKR^IM0KR?9DD`SZDYm!y9Y*S_6!2YL?Om5}DNkb61be_FLQ&%@p z5R{b#5&gFTKp|jiIZqL*7z3eSB=CGhN)KmkZG{>G02f+QjX1cVYS7=>QmrnPI@VSJ zl7PVX?Nzq1Wq^V$|9iD|wM97?Vn6`9p%9ROy11dSKFg6@1AZcgR&^HCccQX%rDSH#i!t2~y2LQ>{*xqnKVLf+Pn)kd{d}Np%Sx34y3- zgNNdxMD%y4Ccph&5QNnAXX^(a?(w_t`@HvfJPrWY3RS1gW?Kk_LUBow5>~5qk@Y^! zelocy9*<{hwHj1aMY&u?yi`bkG}VHI-Sw$^*)zMr6-or5=_vxo8fS{L1X?ypLaT)nQS)O zWEm}ig`L~y^Qo<8b-UeiI-O=A%eR7HFj#ICh@vQ`QYmdgLY%{uGKq`baU8Fk5pfw1 zbqyf7W#Y@qM+am!5iAQFWM`4SHy2Ih-R6z4?22tPCPXruF z+X56(coS52IXt8(a{nsR=Rx{P|4Pt$je9Ew#8wAzA`YVv9mCC~#x+v4 wvnNY*6aB;xVJ1ch6EVo%0ntTt5N&MUD`}U6V0Jos=l}o!07*qoM6N<$f-exq761SM diff --git a/share/icons/application/16x16/actions/document-close.png b/share/icons/application/16x16/actions/document-close.png deleted file mode 100644 index fcfaa46c0ca186de3509e50e9c09b334332dfeac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 613 zcmV-r0-F7aP)JsbWki7u@X3hPvb*KUYmxc%CI#VTUV#??)VTMA0XrJ5*XmXtg6ad zuT&}!MG?j`f*bWn(9VE*Y@b$W|uA^G5;#WS8_|z0Oj`^Sn zJJ?-}LVOxUHM@hiCw+KC^wkw`s2es-6P9HmwYG*=Y#EzJefV&OuziV;4EgZVPqoWL z7hQqQAl`O2T(8%i>o?2G*qEHei{W9sJ`%uYFo^R+w-|TW74Qf39=OqHII(RTxm*tE zblORQ?G|GuJl}!AK*KOltJNH!BWO08Xti2SI+>)TIZ*qN4O-9y)nQ3oQBrMRiC}Ne?Z8Nl9P_D~bd?=qxj<>2BxzMdkU-*=GNAIzBR?{8ZM&@_$TQ&UJOky0Xr zKnejuAcO!yEDVz>p3hy}Xo+S| znwty7Vxs1uC|w1}a9Jz^;WIGy2QKLC+C5CTp0qPd&7)il8SPcE*V_ON|N zE`xzWGBT1$Nl97htOA4(pe+(~Ht^ut7oK*EP^oJdwy7MiXT0nCot zv3Iym?OK$I~`&gfUki#<&D}dwW@Lj0aHH1Qfr(A2M@ruLH|TN&Bxe4E-3V zy1E*Z$%LXPT(Vf+0$mZn^rVv+S2yXKTx1qyGaP-0oCDby6Te|HnehAlSSzi4O*Y#( zAP@n(c-f9?c!ZLYt87lo!C;6Y7!2a^cu*7tYo)cXzOMclFuvvv3JMBIHCO1b(1au= zE&_9LO;00000NkvXXu0mjfJ4`Rs diff --git a/share/icons/application/16x16/actions/document-new.png b/share/icons/application/16x16/actions/document-new.png deleted file mode 100644 index a8e07a5e952b196b4333b5980a4c3a0bfe53abbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 509 zcmVHoyVLx#0WW?a-mRwVHnUn9vq*X=Galb z?Fj8quh&s577+@Cu)MOOFC-EXw!n`cBm9P1tp-igu$W5eOY`%ijBL7hz-OYQffh1< z<})8^YGU6snfh(`aGh$ED4%}|uY1@e}cAJ`Trv*1E00000NkvXXu0mjfM(5~< diff --git a/share/icons/application/16x16/actions/document-open.png b/share/icons/application/16x16/actions/document-open.png deleted file mode 100644 index 0dba17b6708b3082972383a998e00b7764563f1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 649 zcmV;40(Sk0P)R2->*FD2P_kvW-!@FoK}JAP6E`wk^nTQ=kOW zrWB(nvKEa&q>l4p>Zo(QpQpvWQ=<96%Yk!vp67h<3JDyb7cgu^HJ8gxx~{wapBh}g z_j02B{D5wU`9eWEJ39uq;bLp6)sr2^jsoDiF2)!H2*Z$?>yn?I)~Ttf{?nIk^zZGy zfYV?hR%|Bw_3526U1nvujJLi9fHCO=B8V|q$04833kc61+#>olMqBFv+S`w@yjbo8 zFxB7ltggBVQ56wMH3Y|6eBTFPXVWeo-o3`y&>)p^g+l&AVj6lb%d0DFhKS%e4%tiw zfTreV4jpM@eCR$uCZ6Ma$xf{wc+}QI02DwKBZ6@ZMuc;HeT{jA4vB(@APlK2tx+u%5`o&f%VMcSu~=rV zSjLDjTU_9CA>eRVH-aKa(p^LHiWPYMev)sWK90)8fhT(N`c=x6MN}0;Fh(#a-=~*| zf`~XB&jI3eN7-y6Z-yUZ#cAZ-#7{imN77SJ_lUK#YSG5wZ<7^+;SIz`|c= z-I7;~;~)rjeqotc<6qD?WFsUc*5hW_Opck^61KHvHz$sF5rqM=flw!H%*7{UGiQgBTHr6+#3HnEJp90U#(gRf~vN3v^g$={ZXfoJww`sbLyb>z^Y| jE-6WKB8sByXdM3oi!38fGZ8S&00000NkvXXu0mjfiS{A= diff --git a/share/icons/application/16x16/actions/document-save-as.png b/share/icons/application/16x16/actions/document-save-as.png deleted file mode 100644 index f0f278941b76c7515699aa9fce395ab2a4c9ccca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 773 zcmV+g1N!`lP)IsJ<#~Djzl-O6MgSxJg{C!XRgZnv z!!EPAt+%jH{rg=$aj%d|Lb==zOG?3 zdCPcmHi39!G`fgD?i>xF+MU2mOWKgYGQ&dIf`trz`_32tAbt_)?!JD2{u_y;3NegV z1=D^wl&hzUNfY~1&8b5IshD3<%DayP^!?&dR}21*By#L=gsDSmtyR%}+`_KfeK_on z;ewRu$rKm4c=@)U+T$14-*||D#}zDcnb2r7)U6BQ@(~@G&N2=(cyT$4kTQy3`ZNLO z+(LSvzvlM6P6nQCXU`TVdcB_N#bfC{9l?r~%UQo^8@IZj$-PoW5ll-;q989HS3wr9 zZmz(yJ5LVJvybM%$uS6%Ct!%ylR0~i+$SMr6hY#YspMy8a`%jbQ`r@giYp#i8o7+CEvZcIlkbWlkYVD*lHbBxGg?#T{zl00000NkvXXu0mjf DCbDj} diff --git a/share/icons/application/16x16/actions/document-save.png b/share/icons/application/16x16/actions/document-save.png deleted file mode 100644 index 59a0e255a182724094a6c5796d1d817462677378..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 zcmVziB3qNZc3gRX0! zX<`iWK0Me5!|178U0?SSz_Fdgz~BH(PEA9r9}=KYGzbwF^_LfCP$(1^u13FR#yZ4Lr4fmBNX&TsCOSBR>FF2tEhkJDb%v^ijw3+aimR&~>b6eFu@ zd%rjcIB-GT*;xB?{ZaFsL@tx~p93{&4t$>`cK7!n9*;-%@f(l=4$GM(>W7Dh)ExLC zx>v8$L_VK~lcQsBTnDzZStaUVDg~vN5|z+zI+g$t-Sa$}V2pun+hAE1n5L;#Lz(*8lqU3iFE#v{){e q6~J?;L^XZLx<+cU`aOcli(NeFE9&4L_}qA=X1Kx1WPCiI&eAno_o&k9De5lB&!%Q;yl-RxBD=RBJ zn#KlHTU$e4D3utHLUDcEY&OH<;$r6b_!y6kjbXdpj;E)m@yW>vURqkp5`LhfLO$ej zb)fqC`kx?G)m5lmUY^wJ^{}qWR0<9b4jA^mv9ZB;yi$C z_1l*-fNBCDkw~6tG#ap4t$1^DlZ}DX)6<+@Utee1+S;I{rG;@iomkhT1EEm(hJ;?q z0dTooMAP(yNF-uEa3Yb&1!gjtoE{k&0fj=r=yW>JYPF2PV8|$yN+6-p96&)q0Shuo zuDJ@fx3}T&@DO%)cj4&h2zGXM*vvqD7#@#@`FuX)=jXp?JDCF@4;J7#VHrsIm@R#E z^%M95K3H2@V~9XHon{0A0VaNsmzVdNgyo%+D3wW3QBl#9szwR1_&{c5h;0dLc+uQ^8|)4%?s0cPe=1FJfOGG7ea=Cm# zrBZb#lSxE6Mg8-VIUEiY3O&dz4|@s=3pu}aB;$%~HXEm@Nk@}PD55EY(LfnZ21Vq#KJRRNL`65=c@EWEtDKye@! z$OdX*V`F1yXXoJH;Njr`N(l%Eh=>S_h=@o^N&*d6R8mw@Qc_n}(>KsFG&D3aHZ(Uk zx3aPVT48T*?+64=_Aai@uCA``?(SYd;O*hKCSzGM5rg9ncsJ#y^WvGW(sU%7JS-o1MdA3l8k{Q1k5FJHZS_4e)C z4<9~!{`~pNmoLA5{rdg;_n$w1O1-|X0)~Y|NswPKgS@<+p1plYNNVbiQ>U&xefspn zhaW%w{^eS^W)@J*Tu&Fr5DCe-2bsl~7#Ua(%+I~OE%$a=boGDb7F&n+rp@&iT@$+) z;(a1(ZC-qDo4bu)KF77t&F7VJQe4U7-ql`LzxDraQqZ@wX4t1T%X1oEzj~#%OAmv} zKMxIwIg_3gzbedP{BT8VwQk{#dFzhP@oP8lyR_i`JoBetE1zF_+^kxDWctsD)Tw$9s^581A z+04g$d$%769IB{Omow>!vBk(_Wd3m>v({tySMg7v`KWi$dFEtePPDUP3DN;zPqRPj3f>@#5rJyt&9yeJnKDXC{6RoGIz>A2C&LaLTj8nB?SeU6$og6^9w-Pmt$6+=}FqZ75kyp)fFlAPI%8q-&}Vl$4cwU^G%#OcHpWLemOJrIQ$a(!KKj<>#}s zus$F!&AJJ4GYp^<5E!75VDMtC*(^Q?1OmG~9zRSb8KOv#&1&E{4xA+8`}FkU$h+vB z<@oe1@rRldR+8YSzCh1rK-$_uC>EK+XcX?1m6f(Aio%N`htkq*Fc=JwBoky=LO!3z z$FbPdhv@i?>{_my(=}b_32y>L5JEu+sgfkFgHlf5c}|pN$)PB=dPQ;ATrL*`L4>NR z=+7JDf)E167&gYqz!-zB7p!)>^{&HV3wXWmfZy*&EH;V0 zzE@RLl0T7#h8EcE?%%-Q(e*qyjzZIP!EU$S_xU_csgybtkI!8N5Zn4Q{Q^)8_pfp{ RG4TKZ002ovPDHLkV1oN+RPO)) diff --git a/share/icons/application/16x16/actions/entry-delete.png b/share/icons/application/16x16/actions/entry-delete.png deleted file mode 100644 index d8f784c755a7db0a36fc670ac1a9e6e6fc0175ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 713 zcmV;)0yh1LP)Px#32;bRa{vGf6951U69E94oEQKA0&Yn}K~y*qrNK)`(*Xbn;Geqbd~NfAE>@{z zP8<>ySV=yhh7Lj#bHMmn5`zt|-Zr@DO!UP_qy#n-eXyrsZUy zIi#uoUj+spGK;>6R}5@z}^TUJIo9Ku*d1+kG@rLthoXe4`k1)EmONsWf=?iQRT6Ym5Blw++lAOycrCzl&$ zv|6&cnQUsJBR7|~C6ZFCRp9QX?W|68T+#3CJ02`Qod|44n$!zFG=zs|kILm72??Q9 zspR{Z7`6lj<>MC}5Y|z&WtR3Vk|xfNo#*pQdS~(`|5FW-kvX&B;hX@3VGj>lq*9JY zMG21dbUsih3XutTT}=t>>MRNRQl*fzIY$qZG?Bc{(MQD`j*AoK0AX4x!j%x?;#+(jIS2bL7Pr;QQ(Dgyi#W6J>m#d}>p5A%* z6Fnf-1Aa$<=#zdpV?Km&4uHadh{DuXT5ra(#MiqLf6D+Q)vo5af_Q8}Hs(Vi+tcfYbqROEv#piqNZbj2>zuz<3EJ00000NkvXXu0mjfo+LCc diff --git a/share/icons/application/16x16/actions/entry-edit.png b/share/icons/application/16x16/actions/entry-edit.png deleted file mode 100644 index c6f04a4942bde7b4473a804d6f3787bab5969a18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 797 zcmV+&1LFLNP))VHn3h=Ny|ksO=~_wL>n$j1?9lV=}qTSUAqvoVkQ1MMKO|mbsUiKU_wW%L-X5 z*RcuBrj6yATVv|PT50#8IkvAq&hJR_pQk?czJGi_&(r%p0Q3S<0Vl8+NCL(Jb^xqE zCa}+7f3Rc5?HcQZ!&R8WqX9_%had?^1cHbM@{I5vhL11WSNzr|SCLQlNrx+m15ymZ z;y(oQ4D5^na)G^A!;U9jtZLWwPf}KKTX(2EE`?OU1-QZFHU}+CgyA!RIffvd7(;N- zk$tkRp|VlRt(Jz4U3&n4rT9nRXzRJdd&8;nqpt7S)1z-L=oykW6HI==A#?KT2c^&3 zr4uit_|(FOK=5yY$83(T$QrE1s_Bw;NWP=`cz=u5NA^2yV+4`0SGHHzcPXz*O2~NA z1h!!WY6(PKOAq&6cK3O|g5Z-K?`BEbEa{Y5UaZ%miO#Zu()Mrr??_`de3QjG+4VaN za3s!Jqc=9yo(<`^yEC%BtIaNHw4{~#(Xv(-E|qB8Wy$7|=$~;ckTV4iIirG$TT84` ztws85mYR@!W_312sIF$5-aoI_u2Wtyu%Hx7A^m_@V17Wb2rSuA=EAS{EYd6dG@zfK z(ZwTEwP|aKt{z+7?F{Xkod9@%RX`fXD;mELlr@C>>fLm9KSX+hRF9sO4pR9ghdRAs z`ce_2+$oMSC#I;t-Yf_R0=wXXn4Boiu7DeiG1a#^Fv?#@Q&22%;@IT z*52uEu($x083KTYnT_H2yS^a@FL0t^6_8{sry z#a}D|R>IWG2##M#=969`J?*1|v0-A&~|s!3?#oo}V{00000NkvXXu0mjf?M!%X diff --git a/share/icons/application/16x16/actions/entry-new.png b/share/icons/application/16x16/actions/entry-new.png deleted file mode 100644 index b4ff1d8ebcbd08725a388da90e5d4fda7719854a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 832 zcmV-G1Hb%McSLw@KKjQIsi_0|W z+yr$dhV8+K8a0uqFD_wB8a`VF7YsHD&eHemU?1hNv1 zUo@8W`P8ku>eORhQdi)#Mmwj~Q)mSb;0lQ>f%wHhK43Qi%f~^EmE5NJH~v+#+V#73 z4Sp|2N2$V#lD4tT3MB-wGCVBr_?GN}CRf(k6>+g&Wy2(vjENlO9sgTi+q|d0zN%79 ze}OVCrYrvFG#S<>UBEr=CrX0WlaH+W@QHj*Np%|P+pbeTyI$l>h~5Z*7C(?POt4RGw57BUsQ6s)&^&Xw6I$OfuZup=a@B+(> zp$OlWs0?x%GC8u_&gIsnTtBk~|6n@`bRhrjSiS7C>Cn0<547Q5PENGHB#Ml6wA{uIka24aE?w@`@1ot+oHd8JyW$Q`mdh!986< zx*Dw=pN{>n7{UO^c!z*1bifhZ=N?$dKx-a7-y|BEJz(H{-}w!%R-VFa)1YYp0000< KMNUMnLSTaF!-!r0 diff --git a/share/icons/application/16x16/actions/favicon-download.png b/share/icons/application/16x16/actions/favicon-download.png deleted file mode 100644 index 3426fb937e6bdb6e3d7408797b1d2094d227d990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 754 zcmVYnE*P$gutJiM3QBU5kff3o2$o>sWt?AU%v(CQ|C|<@n%bflUJiWc z@bG>wN3^!KE|!-Ed`d}_0;LpEN~BaMwRr;c_kUPvYHG>{=2@2!2vqnINsWP_5rhx` z>mZ4AK}rQcpsbW&uzHVWSp|tiViN$+`SufIMGa(RYFGv&QZWBh5}8U79!oO$Ey1a= zC!DS=WxEWe6t=BVT&R(g4z>aD6hwR)rfuRT6)Q^;P169i698aY8hbM!->=BClT5@+ zJcb~i5-jKH%!M_Cwzk@?2~0!av!OCalB>J8KV}%6T%fb{9C3dJ@n4cxMCg0qAyJjp z&RnvK;b`+lf@FM^!Pk%J9qr+EOB-!>Z!qf$oD+*s5CdQpm|+FTFQaGa5W~?sz>ERl zIFr2SxJLf|TuwHeCp7Yo@SKBnI1gd$jO|)Nq`=x(;q%}C&Qus@Dh%(xti4M2kZuZe zb0;8f2vS>1ymILx-GQ?8(OOgEQHMTb7;_hZtBI{8LRIhx!OCisQcO4@u0Fg)#JhmU zBd~S>Bz?MO9zW3T&L-X%Yd=qIINh7Z_r*!Hx@G)+o!#U)W(!jjHO~F%9?f-A7>15% z>KKMjc19-M%}>dCUI0;v$K)^DIz&5SF{dSTf$_+7QD0yGEG;eVpi&CgbvKf@u1jy| zYtg<&UuJfx7l2eu^6}pEqVsa>4AJ?nKZyij|I1cc$<4Y)jq>tPvnr^q8OQGr^1sqc kiqB>pt?0q)E!=+h8^a|QwMfN#>i_@%07*qoM6N<$f>3l#?*IS* diff --git a/share/icons/application/16x16/actions/group-delete.png b/share/icons/application/16x16/actions/group-delete.png deleted file mode 100644 index c5ca349fb7ae6a41ba2f9a653f44115f1b97edeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 627 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 s@=5*=56ybS*Re7qbEUFVdQ&MBb@07|`V1^@s6 diff --git a/share/icons/application/16x16/actions/paperclip.png b/share/icons/application/16x16/actions/paperclip.png deleted file mode 100644 index b84d865d0fb15fa85a536b287a783e716293f29f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmV<40U`d0P)MzCV_|S* zE^l&Yo9;Xs00049NklZ~&j5in;?*%#omCxG(G_AaF%X1Fi!@I@a_Qd3`=5?p zX`E6fh@qf5^2nvTZzgY?xZ(EeW7ji7F%%R`eDiDS)=2YE>+Ve-|3vp|7~WX znQ$9uf=bV_o2&0h_A1T1_1_LvLD0-6KsRVBs5B_Fs43{oeEi=PRYBCucmKWSc(O1s zhBB$JIL>+Z-w#zmO7n-`^A382x<~_I=&Xb9e?>K;hGfv*iy!|Sxz)73as827AOGyW zm==Vhppdn+eAVHL&!7E%_WR=V6^C=n3t3SV{AXxjsj{j_EA1=kD^07gYJi6%01}@N U24|`+&;S4c07*qoM6N<$g5Of%%>V!Z diff --git a/share/icons/application/16x16/actions/password-copy.png b/share/icons/application/16x16/actions/password-copy.png deleted file mode 100644 index 4f0f502a0da6364971335da7df9292dcbc510b62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 661 zcmV;G0&4wp@7o1iX9oD4zTuau9OTLKPPkt&|3$R-`ridv|=Z$!xZkK6v{cGw=J}_ueeS z($)uq!B)51-J-laolf8O`~7DWL!561OlQO4@M){ndQz!eNUGH;Z2Kc?x7)JQ>BL|R zX(5}Jt1ss~sn_ebg(jfRcmB?1*ywK1oq<}thWC4Wtl4aGLkPmM)HQ)=n(#a?;hG?d zqQ>f_RTzdr&QY^0_`a`-7H8y~XSpWJ6wslpRLbckNl0CF;R|4?SQ_phI@vUU@_>$jF8P*e?t5SuB@@1tMgkI5H!$A>5qfhky; z|2+JR{crnVTx6=W!UY-2M$nQ~fmtY^SS+GcDnSUDfpzzM%n0%%YBCMb_CHWcd6wWj z(+kw#{8dVj$b``|A`C+W^mIx=K)E_gk_2k2nm~1q<9Ip3U3$TT$wBAjkoAcVz*Q@9w?} vwzu!S$b<81T_<0|X)|!T6DT zlkH1WQ5?s==iXg6r`^0$TP<^mxy(tL^hi>^sO&+K$g=)`tn7smWiP5XQpAYp35uWy zdV)v;qoh_=ln`m5Hpxnxb2?M!w&rqf?LKsNt#|dR4xGae&iQ?Q-}C#PLli~9zYOwU z09y!~mz=_HuoPu%%3U^hiT|tbYu|^FHfN{j5*v-S=>C!%`3<;QVRddhS(?S0X95W+ zfFeQ(bfK>JT4QH-^dJTNlBvp-;|mM8B^s-M=7W~t{B@>eK2u=$TrJ(^=++qentI^% zYDGqpWk`g`F-o8*@()>gaohXU(c#~v0v_y{CE9Ne*RR}R$*;WPwE{6r_7iA+P?VnX zn0Q?}HNSLEli_>9;P}4CB^A(eEIpi&7XqKoa(YWxqow)?JCz{00qP>YN@6g3)~F*fALNPLO^_pzFqP%llqjCX_+DlT8IQ z(70$4jEjTpR{^y($NZ~GHw$wzO-L2=h=~e@M|`+^;c9*0b#o1CHh-5Tuk#ehjs)>gW+hQ)4>s~>@g8Z_wlDxQKqody-QYG4C9+58&6wc^C;Nt+M zr;S6FqYx8fsB0*)sern=^I?NQ5K~imt;sYaA?~(Jom1##L!*O82n|BxZbeCj0j!B2 z(%lYwBaeZg=9CJs*)A%Dg$p4`3au)EVF-=GaJmO0fzH-1FPXS&bb zzqYJ)adpp`*7LdhW>@#tIG3w{ojdpb;hQm#I1XA0D6rp6qQ??51a4kY)%<%7TUWLL d5a>G_`3ay3dJ!pe^%ejC002ovPDHLkV1ij2ss{i7 diff --git a/share/icons/application/16x16/actions/password-generator.png b/share/icons/application/16x16/actions/password-generator.png deleted file mode 100644 index 1fd64960e2d0a5c27a78efca0350742091addc03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 444 zcmV;t0YmMzCV_|S* zE^l&Yo9;Xs0003yNkl3?VyDLQj0LSs)x##JeF86`^IG#}q5|fmLk{Isfu_-dp z6=D)LMN)+P0Y-zr0)xk-*krrOq^#~&$Kny6kG>;zmfp``>n|9E*2T-+@*&F)Q$1Mv zsV!bSTYukXhi$f4qwrYUsW06JljS2;D6-5R^USfz7`Y3XiX@B8V1zN27^I77 z3S>yEC=xHZ1cWrvL?aEPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L0B*Vf0B*Vg*50uf00007bV*G`2iXY{ z02>=Ewir|Z00Lr3L_t(I%dJvfXwp#>cakxeLeQ&FFtnF3h+srLjOamFOnPy|d>CvA zn~3-@EFxq~5>eEN2!6ZGiAsKK`MDT`NXVE&V{=So5k82CQrctiKb>nNCX-%v;BxOh zAHREkzk6u3t26-U?pCXHJQj<+h{xm4MIsT4_^Q?F{{eVB-Y6EOu^7BwFN8uNNTpH` z4u|#q4_s;s3kzf0+uPq{GMR01a`Fj_#o|s(Ogt8g#YVH){A+i2_ZZVgh5~3bnka+8 zxIHAylFQ|xKp>DO7o}49Z*+Ni`5A6w{l5A6`8$h?i{k@bzOvixCUU0J>3^I|CUZn6 z6yEroVPA4$^1#RVK49YHpm1<&RtsMTuN+uMU^Gzx`60q{2{{NArvGRo)k89e}- z%~rxTsb;g;?cgmIKAo9?si`S&BM_O3dc6+CViAl+BcPBUdc|vNYqaU<>AN@sP^na? z!^1;J@9%?fZVpyfR^VMY3_CkJ&~CS((P%&>lj(vO3w0LL1V$t1K|EvVIMaB_0e)dLq%o<1n%b*)yb#L;b8EEZT_Unl&jPNxHeUb$TE zYGN{(z6%6`M;s1k_-IR|(ub#~r*HE4yf=|Zy!UuKQ54lCkw`=szNXjfX;)&u0qxIV U%lF9yP5=M^07*qoM6N<$g2_KfHUIzs diff --git a/share/icons/application/16x16/actions/password-show-on.png b/share/icons/application/16x16/actions/password-show-on.png deleted file mode 100644 index a6b89cdabda072a58328ec578fc4159e89c7eb37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 763 zcmVPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L0B*Vf0B*Vg*50uf00007bV*G`2iXY{ z023CQ-tOT500MhSL_t(I%dJvfXp&(RR;gHPM$9{J3P!{(HKDm?po!REZr!+%UNq)p z41dZ*rKU-^>2zuu3bF;#ppmU)(PRW8AvPnnp++$xw&BI7;mq2ii{N{DzhbgvtDBvh z^Pa!wyyxLzFb>iH{PvMZq#+iIY2xvCm&s(RqjQBq;r{?4k*LP)cF!1%Ms#*|qPx2r zcDo&cK)}Cy!JgLJ+god~SUwAd!Wf&)zQ|-UPZKW|2n0cuO7$}o3N4a0=Dz@v#GTxy zTql)ECxgM@0v$LUj=!rCiKK(L+TA%Gk9Vr2rKO?jM4QLs3DIvBi}feT;c%E@vG~|d zlhtZ9-!PfV{r&wg=yV|Q7TIXsskgVc-|Xw_YubzbI;YdQNR!v2)lp90eOPV>p;UHJ zIs!B_IGC%iuRlRI%H?u%^^HeeF4u}iqrt2BH<-%(z*z1(#?oaxSt#ME)(r9^%IEWa zYHV!e&=<&Lva^+Guh;w1U@%~Hbp?~NA8;pC#^g*2w+4q`O6Fl5evItWB8Voaa5%h1 zMrsJDhbsU!o9!JD4^=Le5TAaFrh!K=XnP=UJ_r88C0xIkLSbzU*=!bs4@9HU7nNbH zR?Fb?`DX|^^7$Nl`V?xZ)I$3Y#^OZV`O9m_z^toETqmU1R@VmDiyK5zD}0pF*-U*5vkM6v#QBFqENIs zTtmZ~SB!TdR=eQ|k5k)QUr{U;vAn#DiHQmL{r)csh2lJy%iUkv9c}XF_xYuPOlEF8 tkw`ov-%jfF`j}d+R*~`{0>U^D`vtNpJg`tELazV-002ovPDHLkV1l?FVZHzW diff --git a/share/icons/application/16x16/actions/system-help.png b/share/icons/application/16x16/actions/system-help.png deleted file mode 100644 index adb2d8e2106f0d2abbf658e9f0a741aaa48e49f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 866 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 O0000 zK~y-6T~%95R96(;XXeh6dC7Z_hdioj5DnDETGS9zn@IhsHBGc1HpaAQVj5#wNHh^_ znx=ke?S~La?S~(<(a_pPq9y&1VoZ$SgCY-Mh8fb z?ylI`GiR4>-MaODBof&X35Q!hudADCX=!OTnM@5Da8pwgojZ3<1HEwJLV9Uw={|?u z@kLHf&f5lqftHq*h;x+;1|u1bMw*|WpAG~AE#v<2t9Ga3gZ%aDTbZiRYRrAwDI(2Ex@R&1%*(wLEvaUd3t(eUsvO-)Ynq%4!Fq)4aJklssY!3 zRqv{-th`ZDQu5xjr%$;%H;3Nbw27otic&DmnwCbwt#~}H$pWU-|Gc04;}cY~XAcX* zx!WBbN1L0kwBs>VVCowh8kp5;t=n9_`GnW&q0-_F1art>GBN(@6%+z3Wihl67mIrH=RKV4n?yJN?=9lsYA z77F%48m`mpxj1A)Q4!~gN+9{h}*dc6#6t03DeyCJXs-y8C?M< zD1~m=SzWDdj@8$b&>ZZDFEbWqahOjFfdJVZP6`A9eYLf=c8s(xQA(zYL;z*s4;!b~me?l`a*PZ*qd&G0}b)G8t&4;KBlou`>y=hFk;?CP9M1p+Odn#+fi~ zaB%RKhmRh9+w-U=BwiG4ZEd7Kefl&#cEFa&@E0bwpm zW|NbXl!zzT^z=0K_4VE8ebW2o%^NqTg#^fgHg4J4wn5$v_+u~D*F{A|Rb^#m+JkAB zZql;8T5CZ*^7(uLpWpWlO8>F_*Y+U2cC7;ISC$UTgc*sD6N1?D+}FZK>SU5JrX0vCK#*9WKso~j9YmP dQ0jHye*tZ@=tVu1VV3{^002ovPDHLkV1mEI^gaLp diff --git a/share/icons/application/22x22/actions/database-change-key.png b/share/icons/application/22x22/actions/database-change-key.png deleted file mode 100644 index 7bf8d05d332c930a57b0b816403d9b1e758a9619..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 984 zcmV;}11J26P)b zl}%_|RTze!bMBp=B$G^QotflDCy3yIbMHBK@$lZ~ zeBZski12?5AK+P~hVI(UPlKbX%PD14+*xBIrg%MKZmzahe#u;Y@$&TaT+~xdK}son z-)DAqb`31B_lYOjzWX`B+!W@%>Ll4?nRsCgb8VCZPi5Ys@3wTrqbb>1Yx4R055R>6 zcuNcW`nLly_DP?lZdYaU^!!0nu(!DaYkAi}c+-MY&kQi6G7)B8_!>dmCL{ zUE4yAQ@sqUIA9^#Utsdpqo8*J3806oq`as3hj3|B7XNez#naA+ln z4&V+!Yk^Sub>Nj%&fdsEtO)c%;WYStmRLs~)FSz8g~7c;luKo-4FKW9Yx@wmSmDbL z4l?m_C*xa3!7qX?8_pkE;N<)QIXlVSg9DgK73TujM#s(x*4i*wLFU{NZ)Y#^&DKI- zAQDv^Kh{C(z3=k)!{caAVT?s7hpwp*1U6GqTbwxbDW_h4lwG<5#0h@XBQ-pLACFPF zRcsuZ6G6lUo<%@hb1-oJ$`VIRiCvK`fCOz?tr#k0gE6wMA~rg1lDOWd*DK5)`xd$X z8I&|CL{Rl90xAGOpg^g*3fD9-iB|6H--B{3pj^F%*AlWWsYu{h#BJ_a1HNLQqb=A7 z3Tr`}t5t#vRl`QdZZq)J0&gDqgr}dMq1z>cr=oNS+O+dr^TWf3^v-b-Bmo3JqDZvH zNVLT<5!C=mqYBpCwzS0YJTLk;umEw7$I`>Z@IhBBxj##k&iqEUyo|G!ps{KlCe$1U z-S&rdWHOmkA+RR+?vyMaefS0szS848a&}R3`12F*AO3j%_Z#1=$#B3z2)qI$0R`D} z7ukRCCH>uv;?l`jx%}F{0RH3f0lp6ZP*;J8`sR!O2>uI9W>@NmJFr~<0000v-H3w zMc_!0M9I22H<#V_#$s1o4tCwmgJE{x$9ywy<{J_bz4A0>jNp)ai8eO|0MVYZ38+AZU~(PTG-) zxvmRi3=Sf$*Xx;>ZQHpr%_VkSHxqLl2c>kpzIZ0PpBMP{^$I^WF2b;aD6(Ks0W+gk z0bG}HtIlZN-r??}Cs?i(?mhTO|CQVEiQw_WOZc9~FHf-b3C0jA7Nc4L92=-N1&uob z>*OhxYg0_e%m83OWB?G9A`mf@5-1hl5dnYz&;Y3Tf-uJ>0*Dxh7#PD~7Fd>mNK!?V z99|q4lfQ`}_08dd@%5DC803K1|Gni!_|2qt0{ zz8J%NgoKy?F=*U}MS~<7`LM|{7&_FARiLu2t!>xa_wBv++;jZ>0ssI2!1pP3vm>St zxs17l3r{i+00001000nv!tR)Fw~e1XX=-dtjnj3$`ZSjSAOOH99h*5(;Xwc1 zy><)_E4H@_0Duf>nwoh1b^m_l71bn}t8?TiZvq5>yT~8pcO4A%4-DA;@I#_F2!SBT zQe?=;kYyRTdj7nBzVwo-SvIrEzVGn?M1ZiLkupP@J390o{EXWCN1`pgRu(2Smak~7 zOi31}G_Fos`eJjgXAp6f+Hoat%RWT zv7Du)DdqkhgrH~lt@pNldIl);2CZxI{ytan=^viZf9q!fz}jNfZ%2-}cJi#w)kJeP zRi9&_!3^1Dc1DN`bmbA}2r(hfluHc*AD{P|ZyYgs=^}tEP5t64_vu+Z<+?2mg|1Y- zi?~dTJfTE)gpi{iLrRvArLQ08qEc|!jk_SA5uO4ns<-gC%y2N zmG_YmG5;FBMk@ev_p>Jz*G7~l%}DChrdW7~c$qTUB}Sj% zIO+ia8;5C8xHl>-cXozbn_)udQM6B%N(p-i^Q z`9JgAv-}YYNC5%>00Q#p+0TvlaA*^|i);?W3G+)_I>%pL;7rO4>c{{f0000WAcr=T jQ2;bi!#bKs0RZ6tXpXFZPvdJR00000NkvXXu0mjf4DKof diff --git a/share/icons/application/22x22/actions/dialog-ok.png b/share/icons/application/22x22/actions/dialog-ok.png deleted file mode 100644 index bb27eea88b85f4cd9a20cd411bccadf683c05ef3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 702 zcmV;v0zv(WP)dNopcHc)IO zK1nuEVI@68CqP0jM@lzTYdBP2IaYK!RA4(+WY<^O0 zk5h7$RB?+{a+_6jfmd^hS96(Ibdgzipjvj5UV4*WdYN8(onCvMUw4pSd7xiJVqv&o*g%%#NHtgabl0000tbW%=J00IOB1_=ra4kRZzKt)VeUT1K4eSwdh zo}i(uw7$Q_#?8*&;o{=t=jrb5@$~lj`uqF*{r>(cIwUUu006d0L_t&-(_>(uAK*~d z=E4+Z*Q}}2<3tx_(CNi}WI!~&*KLai~O!53Z{eQKLDtud*o zHhpPh+r*YWG`3nF`l4v5peaxdtgWdr;)a-#`nL-er0l}7yY9}+?#|rayE{9~g6&OC zzCCA7zWbeX&%H}9SABiG+}POYVtnC$Rz!(lvURf7vC;8fd3pI8-ePafVq$SVvK$%E zWBPISm`&pwFf#MywzjsNot>Q@>AIea4|Ph zq+$5)5uBUoPcB85_8@~R7V)v8$KL$l(4ilbiUL`dvP5>1BniQ25FJ#9yp$KG8@iFvGcXLDan%K82+J7kvx!ex?%{$X_TN89EA^;TV+LcZV#L|EUk2pt0IXTkt7u6QQ}cA(i;v4}Z~X%gd>MNvK{7s%476pKg^$f|>N$-UTH@gdvd z?PgVF)ey*i@HmqcnV|+%7O@}@4L8ZMO3e*~5g!#QahE`GCh1J_I8M*ML)SvzBNSZ# zM>;+KE`H!~)uj-gV9C}BLET?nBt ze`u!SY&9H9OGt2 zw_sP-F6`^xhuyF5MpJVWD!diCHDxQ;1I%iLxrx31b`;An3}NDCB9%%(JYC$UHN>zf zH3?WA^F21nnIPq4EFOy?6bd064r6*|2C;ZtHS>QJ22Rxp22m}Me7tWtMd}VYb zkbNp;xv&-14vbl%ZB>wn^A?aO{YfH0m6cvBEc`QezQ6zd@iE_Bkyut%2GMu>RM_5h zvhPFo4Y^_@l_aIpDfs>Vu@hgNcza;r=R4wi-G(|uqtU{{bMPA`N^rSca5|mja5zLs z+1ZuMw?p{+o0w=!O-=psX zmcpaC$M;7r4}S9C!NWNbZ)n(vKp=>xmYM%E6|?y?(mb)wM=g<54FANfl!#SUuID2c bUX%MT^i>C}(?M3800000NkvXXu0mjf^gcO< diff --git a/share/icons/application/22x22/actions/document-open.png b/share/icons/application/22x22/actions/document-open.png deleted file mode 100644 index dff45686b5ae6d96002ae6838c016035953322ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1125 zcmV-r1e*JaP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L011Qu011Qvs^VjL00007bV*G`2ipY# z3m6qOSCZra00ZSoL_t(IjfIrYi)B_Kr3I`zh!K! zw;;|TfSDnHh~S(fgg~>|#D~Ch6BENHKl${+`=@?e+^}tT6jRKQS}k1u?7bKE1c2u9 zBC0W=3Yb-`$VwpR@ZKY7Zq2}eOgy^>Oou+dbdxPl>;d4?#h;%(Jo(Fz186MYqFofX zG46tr7<=!jWf@tP0g(0eF)%pD#J;_}xK%m#?OF16i^lRI`FvdiK)&!VE*3oyGebmr z?7A{!eSM6Mj#j`~Mt^@lYlnt8^x8oN9{ifA?|xu;VUFwIwZ%{x39M>ezW zp-R_oCbz}6?#MtO%lhp4)vJHWk*V*mJ-K6iLo7=%qe?p+@;*?nH>fWZJyc%I29t`lckmFhf-Gh5pV`7>9S2w6M`9(YD5)W z+Sq#|iAl%6r*SC|4Qf&#Ja?N>n45S)yRX1ZaRu05LH$DpsOtd(x%@gp{EI zwOE}CMwfBV3{{F+^)1asDo~SZy`w7x%F2zHK@>v_f&;vBI59d}rR&peC{@ZPAW27` z_Yf(y)DyFu8z zejVf69>Z&-D|h$ieFE;L8PbK-(t2369<5>@ec%c+gr!Dd|1CB|&A<#N4o`K!fJ~qT z^a~UUeGYQ196$A0vXFKC!kb+4~+|1r)#&c#i$!?+;C>@x~?#}tmfBth> zrj&xCNGU3AT)WKne-va(VCV z`*%eR9`a$STCGBBcNflIx(X+bAA=E=jcNulxB==m7QY-0tuLQJ2m$pE^~JGbDJ9gl zYfw2;fglV~AcMF;5pxP?+BBIUl+bK8HCReDv4VUK5l90NG@Z7f{;4&Wmp3o0pa`X7 zDJvx*p?I~jss~^wA#F3bb#WyDAj=7$Yh`MQ1^Fw`TT-hP3z<|*aGDx*;(iDpciM9) zzOq(?5T|c!kb1C$V)~%L*ydgcm(LX^YNOJqHu7bp-1R4lwH5LoibFx21K4T)m`m|m z>C_mkIdP9^6p0@wjTE0Ws@cV=>L{!DJ|Yw{VG587O5BpGP+u#peqJ z_`3Hkws^RSUCymkNa1Ep?lD)p0*EC|CrnBsdWU1$E3x7GSb0OU4rT@3c-X+Xwf>51!2|@+rg+ zt%}8dJ4m|0`;aaoZ2a=^h~+!YYGA$ySSZ56?Np4Iqma6Lvr)tjDgNPqiFtw~$`0IE z_Ond}t+p6?eJRe@7NEN)AHOb~Wm_aD4f7i3lb2Y&1{rbrGV+RcBPDK>jA+|>F;7j$ zcW_H9KUctV#evb$DKt0t!eFS!sWW+S+TX?8^;hsi%iEY}48?>w6yujeFlKrSV|Aez zs|~{~LkLd2=68c-6}V2U@b%M{oUMfMVj~6yhcGuci`m(E^!43Aett3fI^V?dPrg{Z z>yO2I+p&0`anB!r{m5p2*~o$@~Zx zFBoxqWCSjk3)9onc=&Jz6;yRx%$ap_VMT3a1xZ@&h+y$^$fPF6%K+SsrEDI}>x3P@6SQ`$qS7yiQ)7l5@yj&0@5ps#qq|c*J7y5zj0TB>Bi>DRQMUwUVtxs#NLKDpgu-PR^IV>Gc)p=;&radeGVF z@Z8>Ds0E*|{xF)CKvHS)syHYpnB*)KxA?LA36F!Z+jxkIir=YFXuheaI0vKAgsWFO zS&^Nro2-bB&(K9j$9wj#h~2|2XSej=mOcrx7S`|XIY2B<=1U}>e5TcwvyN$J*pZd> z#rNz(<47dQ>&5P|YgZ5*IKX?F*i#dhI_v+my6o)y$%KT=uUMe{?)AvXnCJNq@+|lp XU?@I3^GE9d00000NkvXXu0mjfL8mRw diff --git a/share/icons/application/22x22/actions/entry-delete.png b/share/icons/application/22x22/actions/entry-delete.png deleted file mode 100644 index 9ad1885cb188a4cd2a131ccbcddd7e0e1c729f17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1114 zcmV-g1f~0lP)Px#32;bRa{vGf6951U69E94oEQKA1OG`xK~y*qwZVH#6a)ar@!!F5ASY)WzB(vL zPUUqTPxDQG_?VV++Q5#LjWRKtTROFLmUDu|QfxDslZhI+{;)yQDb6$<3hE#fbzm4& z>O>wp-g1cCH{1Hdx&DA2pO1zj&14#OI3}j>WNd8fW~QrLfR@W5I%sJcD7XhH8by6941)@%^k{4+jr2ke3Gq1$eWvsB}8nCe6~bTvGUk z2-~@LLah7dOEVWr2K|jqfr0aHgoV+ap3cKHYv{_zpe{C+T~ex^^@ilLG1Ppfw=$i! zs&$;$wwMjckw3j^@L%t+DnNh9lqUWV9o^d$5Wr1?feMKmW;2Zr2Up_bAMKFh^|ZH* zJ|8#ipA7CLz-Ay9ns-oBlu5=5VW&n$YErh<8g(fl;l9UW;djaVi^0HklZpC~Bl&g0 z1aJ4edEA^kx8;!07_UF2Ny#TN!}>}$j_&y^$-lnkdUQb#Y)r%Inl_~{&o)}a;P#M97(K|bJ5k(btd&fSrd8(dr z*kFje=5)4}I~=F?OIBq?o9^~sw8Aw5+&003Kxh{{5-Bt+~V|Pn1=6MF8OOPQl*5im-x0*;{z3f3c)Qw zqo7grlp1Ke76Jp2)E`e`(?nZ&y5!JcxTN{{3)K3cNN`e`xUpni?fZIWNP6Df* z3ocBPLNpl5B>VgR+Php@yLy;*Z4KAWys5f>uRwT?iN34CecgiBJ%I-$$z&&m|Ll_C z6%AFoWOPYkZ%J8SCFwpjf?Cm4W}@d#7y~tld^vkuw@VtUf9hSM)*en#`}d3GefW*= z)En^B1#x-nOSt5-Q2)jTW8vMrwnK?k({^SeFU?~mVJ&v(zB=lLRuM-knQ z6$moo|Ia)iuPeMlJ}U5uE~++rk9i4r5Kab?fLXw3ARO?0038XeHPBql-kV}p?fPFvm$o&=;Bc%p$P08@eCU4K31hUNilL(`WzZ(Y78Z9OYxe<}4D_}b@P z@E{-qNOik$JTL>8X2dAK#6lq7K-VyA@_}#4T++Ua(#8rYCcF3w`1F+oeSl0L#eidB zU<~-pFyI(qig9y4Y`}UT7i;MDCB@E0mEM##RY*AnH(Jb=0b{yqoWj462DA6sqnbCx zMg5v$4cIslMoa+H6a)7sz5_OIcz#_`b#rCCRB%qpJKUzg(70ut1?vVU!sCUpp|!f7 zB{jV**QrH1kT+AWMm=?QJWPBM@Mk8l-hcMO)0eL|Nre^C>Jn+t$PGsTOE=ioQCH99 z5q`&N_7B$8q9IbPUDCUfwn=~GYHRwah6(V3FR?^Kf4$?pOWJ!*dihf+GWn+(@a^y3 zH=$&C6r{ZBn<&J1cX_D4RDwOJ$t8i|lD?DKCdqpQ2ReVv3+bj*q zt`#0b5_&eIBuKNEW|uAvZqsjlwPn1;nbR+1SM_(%k_JnPleAdUW?ebz&>Q(q9lR`! zUoC~&C5v_5`wtr*Nsu4ub5|FFBqwS+@295naH-x~YWI{BCTWt?7N@VarKsf0U5$QA zGS8IgUswk8`cK&IZCY5m#J63QKGGSaYe;_})giT-RR4RBnrlN;ynC5+ze*qMDObSo z162_Gv^UTnm<$Z<2`2&RkaIfEOS)v1enzT5`US~_8ZV)mL!QcT?AETG-)qb33pCjy zaPv50n6iw`;V`DRTUR6lb3Isbd?ji>ZNTM7*OBg`8|79_8oO9K*FUHGx5IRG&rrP? z*7tA{M5F_AjL33_Q;j}O2W$|#GJ+pZu0rj{P<0_{`~kI3nattqkMa564k=Fq785OOQ%X!wIwM zuLVmr&*)_Sa4iT5+?xU+8Ae$ofscV@8gQoD?l2aUDG5StFgnM|C;O+qwNAMg(6-fnJ&J``FJw9Dv;o?-{dz`MuasoJh-22WtHSHC(f(wJK1D7KAAY zQ)Fh(Meh%cFls%|NYFD$a>oU{lHkdBnEu&R8gv}B9z+%E0%f;&zB1=wFG%O}G}fql z>Ob z)oD;wbr{F-@4Z~PL@ctnu$Y+S!hjlbQwDcML`(z)y{sZ21UV%xp<`K-mXunJTA9X7 znKRAkIE{_Tlx2~c8KE$R0!fVv5-Ki;qUing;$9HVx1O2boafc&Ip_TU=lLUv$B_rF znLvW$J{)i}7$7_t#t(-HgCR1+>>6u15V|O#BXm(hN9c>QIy!6DSj!JYhPW_c@PqKv zKsqoFNCM^n&jZhSyWwH!2g$x*=^rqqB=u&atyT@TYW-!aQiH8pjka1%UYC9c%TxVH z^1Tl`1DQZ7FxP=50ExgXAl%<*hok%gVm8lit=B6xAL*dB>dTw&s<5_DyphM+zJ8MKqw~PHJmAR#w<=O}rLsu--iX&S%aGsW4CbX_-nfwAwLavXA)0q| zii)o#DYr3Bakb+#wtAGpjz-9Ldx%WSgYJ>$dtMS)9dM?}SlsZ*FsUU$(n?7irRKL( zvnNkk{Q}E;hcLMB+JIxlO>;Ax1t)`L zUtZwt^5wnT-i|PzzWC#KNv}xSCaF}72lJHE$8VSKAUxeyc`$&ux^O5*h5Z6{ z`iEk6DKhUUsGg>c%2rxkw7gb(oMHhXWSi)Mi~M`+ZrmdhZDNtakr}Z2ZXwvQ3cXfY*dLpY1s0EuAn( z_0Ce0t8^9V4w5`2g-aTzx@`eUh7v0c2Xt%se}b|z5wYDVh)as6*tq3`_e zFhD%85XgS0LHEqHZ`H-0jpyoD@z`#Df!a?S)L!SVqpJtVigEh?V1Qu9*vxS>c({r6 z{#x{jhf$@GH0yhGcON?So|hK7m`gGr1p_z%q1cb7nU8=w>l7f%K>nr>F1;B@<;(8u i%rY`Hy%T<1hv|QR$svVO8m&Po4d~+cG7E=;K`HzD`*#6{0dJKsfMwF@O-72`5U^t}d!JNg@Fd0$d+5wkExxuxCAz zGd}ZEF3E(?UsIo;m#wb@v#&6Pm7Jx>S&EYj zQw)y|b2)$YK4SF)`05Pf$@kVoArLrrhMw1+qp$Y_fq==kUkz~l{Wp;J?qGJmg4(ea zN0)$0Ou(L-8`1g+h8FjbaxOAweM9M(Bu0lh)b%9a4fS*I)Yptz0Y3fsP10$Hd21S3 zXCejbQwl^hAP{2PnkbUjt}=1)0-Gw}*jrEY#bPvyU-p~8( z+z7s#C+R>kmF4$|Q!AT9lO$-A)x|LcZpl3<3TqlSZ^g=5#k>pDfYy*T1woSy9IrDK zo~QWr$MZm;2TJ=uYt3WvPG*g{vcm3l<7Wc+zQXg=D$yT)KF{ILkD|~_re>%y%UjNe zB=YvU7sb~4PJcFcawUpVQLQz;?=vdLX?!L`!$vs(^=(1wcGkBf`m)VSr z=%(sfn{agf;&HAit{k)%?U9++NiH4*Jkj3X-WLvs?=O8Blu{_A08mPia~*kh@y{)> z7aBtKvB)aX)E{|%>Y19({W952Iy(fY<&rjlXmPzULZN`!vgO3?mmg?rZMy}4@e2t~ zzkBYoGd8&o+dj*GfRL>%$J#%9UO#oDTN`(6KLc#%e?#-OIJY(R0yo`g%iq_I>~^;P R1cU$p002ovPDHLkV1jaO)EWQ) diff --git a/share/icons/application/22x22/actions/group-empty-trash.png b/share/icons/application/22x22/actions/group-empty-trash.png deleted file mode 100644 index ed0bb2b7266a51c21a1aea0e0b82161ce94c47d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1297 zcmV+s1@8KZP)^({XQ3@SSWGlw|W;}p8WFU{k=jd#sBe)g+aV^ z77!4p0x35X3z;4L-S#s@+g#n1wW>nXvT>BgUb{2fYPF{tqh~I)y!c1%oF2i*1WX(S zm9P->0T=}&a{Zd+rH^-RslL`-=<4r80>@35n-59Ch^&xwq~pOS8RQ5+46)4gn8?E0z@7 zRvMh{f0Ab0=K0N4n!e)PZx>m)Vin~|g)o+E9~$7*okjY43wVLzorXcZZoUJ^a{c7@)e9V1kl5{uSN zi6jM0rL67ISXP3GJ`2awiCPX5jfg*M0d}E?)m^|=9df=;tr>DpSqK#BvFiqg0P-1- z3c?gjAxYgCydYt)w+ln-;KprcB1Jots8k`*?icHk0&9yCYk&{}Aq1KR&b&)2RM`0< zjvJsQ0X=4b+o;iO1q4Az5KGj5Raz;9E+CQ$O@NXxUTZS%r)1*{jaiq0T#BY!l#5xE zERY0kR+ke(6*HAe7RUbgRw#5$hbX!KAx;Hmp$|JF=mZ{795EMYlyZ5pT?UD?Y0HqT z?xB>qM8^ZLlv3A~q>wc*biu6K;quKUhKlL760*91Y3A|#gxcLUf$%U*ogh)P6Mcz_ zMUEv%$+gKg3Pru?Gcq=V=lTdGK$w_9BaR?RbVA1?nD@!+8dW<1nLZwzvrMBI3BJ4H zkVr);1v$&Yiw&gGF?5}l*P+>%LrY`I#S-Ow0bOQ@VheOb-1qSac=VPt-Ht#AjWkvG zsm@r#z>fru8&dZSnsJHwFhdf>7znbaNhEDTQ2@`0NrHF@_=m3#eRAa3&1l`qe#!-% zylv13Ew0bNncEpgY8LgDz{>Sf%vaE5N}LL6v5j;4I$ZwpGRnWQQ#PfvA75)FZYsT diff --git a/share/icons/application/22x22/actions/help-about.png b/share/icons/application/22x22/actions/help-about.png deleted file mode 100644 index 4fefbf4c282cd8b8090e3a14794faaafb9140aee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1113 zcmV-f1g86mP)q$gGR5;7k z)lZ03RUE+a@44^Z_s@BwKAjwKBYaBS{36v`wJ}H5uimpbU&4Xcx2zDuWh< z5kX=tY7x~&#TFF^p*2ya*&n8HM&Fw^bKjr2@7{CoJ%3ufwCt6wT=ZN1&f&xF_v6Po z!dlDq2#4zw;&R1e3TO2*0X<++q&r)K!%WR2tBN~{%xks^}Ns!ftz zvYgV$O@k!yfqz=JeIqyDDB=`ispv>eC^bPRp&od=_Hmn6K58%<2-38B){&Y>D#B)! z{>n-mks0fO`%1<2lm>X?c#J=vVzs6^7oj7chwgMaaPJ~s+&jSSYaC`8F`-mMQWHo^ zwR!=Y7r4a*l^!^k&6RLmPM!^T`?G*rGoj!K>VeC2L$iHDiOsk4Q5ne7?kJ=*bfT16 zqe0M_C)EbwdAT0g8iQ6EH={T+8S&PMCgT$kew5+pHvx0a7)(m64D*qq-s~_{pCyU{ zgw+_OLEGM~h{L#pipe?_9E~3&935|S{3{>7m6FK_z!JrZi_>#l@_ka3fHhZOE#ssI z4%@-`Xm*xzp94t>Rs)(ShzTMESTF`SZlyotg&5C|9K}@TuA)e6jrAn^sbweIW%?d*GWBdk>Q~d#Ig&=F)G%8 z!5H;Nd$NlrA^HBr5TwY72_NCU+zVTj)#i~i$39nO5izu~454Inw8V3JD=b=&!;oQY z+aSC5tcN5ax3ElB^bwx^+Mv@XKs0-w&C0=UCX)LLrT$k(w%p-%+#=lgLr4@>fyociXj5)qI7p1b*jlk z5n`rBJTWn6b^0`T?_Fv9nsBTj)kUP-FWlV2*c-fpa78A)t1)U34ryqhi2*ST= fqJM|~H-FymIsaMYWsi`t00000NkvXXu0mjfUE&gW diff --git a/share/icons/application/22x22/actions/message-close.png b/share/icons/application/22x22/actions/message-close.png deleted file mode 100644 index f0850594fabc1972fb90909ba0f9f1fc27c07234..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 384 zcmV-`0e}99P)MzCV_|S* zE^l&Yo9;Xs00030Nklb;z7)HOXAQr+1RZoHx(SsCd?|=}Tsf23)Y8gq_ z(w&63^XrhvvD1X6Z?gSA$)D}tk^=*t*%D(g#6-*qR0mzwq*O^*(XFaO_B3(i!By}1 z;udE{Wq>#S;y@>t*Upmh6vM%em-!8(R_zfwx*?=z^@KpY*y1;j5@W8#Y$7oxv-O$S zdfSE)A;uY%Y{W+;WiI_zA(pd^6Q&X%C##bug?z14(tpN?S|9LL%>4j1tACLIPhvpQ z>QOHAC9!Jt+>5+7I?hrPyP}aKMx1Y|pA7xx-!-c5Uh@}WmiDM67Z%*r|HFV85hpS- ePHdRcck4fHJlG`bpYsC%0000S->P)0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs0005KNklZ~&p1?}b!X*gn>VoFRouW zUwgJ>Ut(n_8y>|4K|4>r`o7~_?dl!pUjEp0Dk=z%;*9!NUzZ*T4Dyum4w`%L)wh6p zJc^U1fBzRXFOBs-gE6b`ydVEPr{hr^GxNuPkC_m*>CE5%?PlUp96Iyce|x9`t(ian zo8nU(H1pGc%bB6fAhyEH&;RvjT3NdA<5287^UZ&gnd(zz{r@wF&3yY`ZRVU44kb7g zd(V9K-(cpgFJe>vGw{rO{$Cl$M|L-gV$YfPfmT2GFEW#Xl^sYc0{J)yID)yau8R+K3O~*dRZa(%oYLgm}Z$A@fXxs#bhHhc|;WJ-a4yzRE z1Nkm9afb8#|1PtPx%9)e@-)MZxvXa0|L=)Qadg|A@70?e6d*hcg_2Eoz6Z47P@I@@ z?%9dQ?zOI}p6qt2j5>K}J&yX*knv~caKRLcRAt^;1Zyv-C0h>+sMd1%K8UO$Q M07*qoM6N<$f(&;JegFUf diff --git a/share/icons/application/22x22/actions/password-copy.png b/share/icons/application/22x22/actions/password-copy.png deleted file mode 100644 index 739652646d6f7c2570e21e46579689bc2240cf98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 960 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 diff --git a/share/icons/application/22x22/actions/username-copy.png b/share/icons/application/22x22/actions/username-copy.png deleted file mode 100644 index 81e6f5f443ca33e56fb44919a5145a1df91ec0df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1099 zcmV-R1ho5!P)vCzKsp=hu-8-uY8)~3bQwz;%}Ha5&OGcjozVkToc z*W{eDmk*OkXOc;Y3l<0VK78w6|GsJ54GpLO$^dQ0)9yNVS@Fgjp~(4j*C%|eJ_NX} zWen&*Q%_IN`zOBqVDsQ`ph$_rp~G7(Ew|SJ+S=OoE(JHAI??gdJG8dwehFvwy3_*Q!{dw{A~~%<)37i|CQU9~_434%PZ5j67Zqw5p!7F07o1q8Soe55 z`2+q*oHcb+S3kn!Trs~~zRv#r2U9;}KvFug!D)FqKvPpwVe__a2e&`Jqw7?6Z=TN| zq-y;px(@tE`C}XL-3-!svYVH8y@tng0L!vg+|!Huc*~ZiT?Njf$b3w-&Q2!^l(I1l z2R^@_Xn2-DFo?sUqm<3`bco}}PV&;uS2%n2Tu#+WsZ?i{ZS9GKlMW>|>2%~dsIRM| zax_RoLjw*Y4<$B&!WNs*%nThJM^;jtvcfWPD`k_J!9bAyGrv+3i!tW+69@(t=S0~6 zOiWBH!fe~dvMfx~ylcgs$n52KJYg$Q2!ayD=-3!H2L`YJZ*MQ7e~*zlpPWy!X6+in z;V>g3Bl!J(hK7dl`Fup9Q3F69zVXKk;qc_Tf2O9t`!^g86y)ctWHP~D!5}|7U0fNw zie=7IR9Hw=Wd+YYyP2-e<9y@!it+JrOw+{Wa#2xH!F{Er($jPLPax}x)q$Q9jxG@#%e^e%=bq0J01$BBCw5#B;!vQiuqaWy+rR_9Oc~`{H#V zlwQ1?Pm|N>ELzPo6bgm%jvoEK|M1~&KLV!G(4}AvcsT9R|9CiWrQ6S(_zzzX<237> RFuec(002ovPDHLkV1i>w2i^bx diff --git a/share/icons/application/22x22/mimetypes/application-x-keepassxc.png b/share/icons/application/22x22/mimetypes/application-x-keepassxc.png deleted file mode 100644 index 22734c82d77464b4e554f137e3d2360fe26df099..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 898 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

g&W`&+>Pedm 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 diff --git a/share/icons/application/256x256/apps/keepassxc-unlocked.png b/share/icons/application/256x256/apps/keepassxc-unlocked.png deleted file mode 100644 index 03485c64eb21ed5a7406257ff8ba331706e56bd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16827 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 diff --git a/share/icons/application/256x256/apps/keepassxc.png b/share/icons/application/256x256/apps/keepassxc.png deleted file mode 100644 index 03485c64eb21ed5a7406257ff8ba331706e56bd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16827 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 diff --git a/share/icons/application/32x32/actions/application-exit.png b/share/icons/application/32x32/actions/application-exit.png deleted file mode 100644 index d7be16865a375c833b7c2b65e0d69512885d5917..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1804 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 diff --git a/share/icons/application/32x32/actions/configure.png b/share/icons/application/32x32/actions/configure.png deleted file mode 100644 index 073f87ae955808b506f5de2fe00c93b89bb7e4e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1458 zcmV;j1x@;iP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L02{9W02{9XUK)`c00007bV*G`2iXAx z11Tj&CV-`aXsewfs1yM~#VX1u7e#c6PK5x5a19|? z5CTC26`~k5gj~dMkznwyt*s_1MFp|8RcvY1sZL8&RIDN`N=JfJ9&F1+;p-;NTl<*5 zXJ%i{d^_KsJ$t_0V`Ia>^6+@iP9`f!xcKG8hUP2Hk1k#Qy8Z0A`uYQh_HiAZ9PMnF zT#Eu1I_dSg#({wW3=I##XzanmM?d3-8`p92lx{?rohcz0V?o%i&(Evs?KR@%%a<5_ zHEa$*p_Gq=1P6E7+1b?)CcuPaDu4;Kd-oW-f9uAJ7cVe4IDiN3_d)wrWZq;BWGu$g zKX=}oxsq*L@#JY2p7%XBXQ8d_4jjiiF17hT7AH0)O|pF}u3fuIsW(y<-5>Bci;QG7e=aw%gvXJr3&e4OAxj?{qgYnGj(5i zdwT_0Gk`buXlrgZG@eC%ejaY$xrNTh9eCFB439fIpslUJros*NEiK>NFtq-Jipuf< z@(H&Z`;@6}?&q5thrVsO0%?g9tJqQCCM4o%_fvBKO&88XvaJ}cM1Jr7T?7RMwUL{< zwfI@|{4!0a=B1`Wke&gJRtvuc0cdD!Fb8m_?KY&+?JyWxp;9S_$!8PU7^|?mzc+1M zL*uzyPrABr>hx*Mp63t!Nj)NBRwI#@f{u&Q{g`NcO6Cj(z&^b-sWP-RTuwPrWO;-aerd7m+IekyN{y1eYkij^zi>E&rAHf11>R8OIh z-y=vcV{KND#$O~?$c&NfNCXB3!hgO$e0+V7n4E~pDs?v%9GhVFOmMR{7H(K3lS})# zTrMIaBVbB@$l?%ixExegSM)nOJEvRvy!FX7HP!4-jvf6YZA}^k0s)eek`Nse4Q?El znu+=^H#gT}!Z|TQe(=zNq%STu4HgvSqoAMwxw*MW&qxPfkcwSuk!;%nUJ4H)k%$I}O(@=44Dn_$YP1@}td24eE|7`m zX1gvgM@H3-#H2(dCnv*Hcv-?M2(yLI=}$l^+x{!zK4fC1x7{I=Y*wmDN8%E=#`KIe z{d*7WMG8L!@d@!bq5F(v_FSKXgF`A|->~|n)17+k)>I7n`udofh-rN0e3X&)U~Ogw z)HGav_kGKs?(Xh|wC~7>%CVy&qN+6Iqn6fby4x|_I8K?WOhMC)e$a1@Um5Ma#n>!A zY0|`Cb;Zu1on=ZBk$q@b=s3Buq=f<)o;Az2miFFd`ady=`g_Bk+Un72P36tX-Re%6 zV#koD=ky~?z4d>o9@)IvvuEj}V%QxYgfBOYA3uHv?YS}fkF%`50Smj0XobC}UH||9 M07*qoM6N<$f)Hu05&!@I diff --git a/share/icons/application/32x32/actions/database-change-key.png b/share/icons/application/32x32/actions/database-change-key.png deleted file mode 100644 index 1eaab8c8ed005ee1096882c4664d5e75a45b65b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1627 zcmV-h2Bi6kP)2gZV9$)xFmbRXtrj)6*sp z@WQ35P8W6l=YRg^oLjarhM#lU^@|-s;J~ilK7K1Kp#z1ljnGIu6$s_mgr@_em6ElJ z8~n@1JT-gw-H0H#D2K)v_U+rZ6aXNLdG3937e%MXpc4X_HcVFx!f{BZ^7z#Z+F8TP z&rDr)=;JS6^nL&A#Q?A@3n4^n1_Bu9?`M5>6|fe_8cu#F*t;i7s>A1zT>;%&Z-H8x zs}FpaIC`r3+KN@Hz86Y~pK>gOz!*chT>ii7y3YYqO8{uNcHICuQjwts@4jX8z@DQV z|KLMDdh>A}xZMM1Eo%lx*>dYI?)cRUf5rE_1!~*<3L!Xk>J&S7?z|j$bO`_(Zd$u0 zW-*d0HurL1_ipCTpW&|it^^I(U98*G&Gg>yP@YFJl|lyr27@u7!&Vz$aBz@9p)k}k z1-T?!R<8ymU;syR=PzrVIC_w-Zj1h*ELa7ggY0E1NI54MIX29Oq3iLLf;P^LF$e*k z=b^P$Edi8@Y|7>9(E}FHJI2Nb$G}+u;+hDZ1Gy6GugG%v{m)pr@j9w=DvUDjG{y+V zQ0NlaHfXI)O90h*%C1XaqDei`6QU!!iI$V0yEnu5=qM`-lKG^J94_8Fb;Hv{@l-@cDW(fHw*eSNR3;b z!%pQHSlZl!PYChpm`4ehx|5jhGKfF&07gZjS_8|_kngsOnkZWTBWS+K3JwwGlfR&1}1oZ;&7IKSzU;S(aNKFhXAXKd0b2rL%L4I`SO zt+Ua%7YilY7JzJY;j+f3pl%I(pYxN7AgfTGS~7AMx8t}5a#`AIz{JLltN7zPFLCQ1hRBKx zm}IDQAq1tOO(8E(zG#tuV$-nMrnPC`h{`}I3fhL@<0Jg@^e_*v8vDm2YC*kQQe` zQ-8l`3BZW~2;ik&_D|%n0^ArkyncxR Z{0NlxUF=87d^G?7002ovPDHLkV1iKm@>l=> diff --git a/share/icons/application/32x32/actions/database-lock.png b/share/icons/application/32x32/actions/database-lock.png deleted file mode 100644 index e2c996e6c67a80bfb9bf759f828fcc7c8ad9e249..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1090 zcmV-I1ikx-P)7Op}$-%-|;-}W;UC>#u$S_q3~&AV`C+g$-EdJAAfsV1o-0O z;v>g#9&~hcxV2ghc6N3^N(tTF-3DuGYa!zC*x1-ZT(8n3z?YVm-k|Y;VzCI@+uMK& zfKm!b0s0@MQVGH^grT7!SY2IxKRi6#pM(TYVjYb?Lqh}W>+8ml>$+dj=v{o(gZDkV zySsN$i7)N#?Jzet2VGrVeap+s&r(rfet!N2dixUN?&tG)@O}U3=;-JqzB;;*&Cbp~ zLF1F8L|9VnZ%CH*x%h0R=@4Mt zdnwkGbgiwe_m45YPXgCXan7MqsZeh8ra|BmdP5!omf5S5)(atC2SH$Rg~&He0OfBd zZ!pR9$jC_kr1cXM6M0l1pb|wgZK+fNg#eQ;adSbXOn*pJ--#AsOtI@FCeX;*|A#;$ zA!vlav5n~a@|eDO!Gp~o3i7{PD3k!o%B zfzB&e;M?b~aQ*gk)-Zv~SBp^F{0OqJ4trGz`2vGt!GUtgfoe^F(hNAxf^0^D?`g<* z5?b0q$aQKs*Qr2Uya)RHFuYs%a4Z4-_w2Wj(lH431jrDxTL<9qD)>$fa$X27*LaVE z3ICMxP!)l@sSyfOLI0cs7NxM>39k%f##Jb7wW!KP=RREWb$0(M~p$7>!DS7{Pp(H6d+ z7fLz!oFf^Io)a{FFDN{#D1=QaU?sO8si|tTmd0x#0wcFoM&cTcp0g-4lm`GOeQ)rS zn~*vL^2~9-fSe;$2%NKhukBQftJY))4h?(_#ybLj_!zGV^qF97*&IBdUxH>VX(EdT%j diff --git a/share/icons/application/32x32/actions/dialog-close.png b/share/icons/application/32x32/actions/dialog-close.png deleted file mode 100644 index 82f6adb21bccfe0967b23e485f1b1fd74044a3ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2054 zcmV+h2>JJkP)Wq`&=0Y87XAzvm6{PRu zy@8LWTwX>Q8A=@;(mYq6oYXD~O*GJ=+U6Tg?*0UWh|VB5v+x(J>ENN1t8#Sbxs`8w zn`P_Q>tDN8*45=Du_(+PI%MRBKXhpKZX*W{sG&}^&8Hq=#}8;bjo|c>yLm^2ujef3 z4#7olf4kRj*`nOjBOrLSh={SLp0e+O2aJu3m_w8AjIrf&OnZgk)snk;cg{Dv$j&L1 zT=u~aT6*P`0)n%PXjZG9zyE$me)>~WXz=7Qe(zpp{F2~Tg-@`qmw)S`qc2UZx#bqU zYgS7VCj|>)L`1Y8Iw>N?NC?`kmKPs-$dMObFoinbzLT3!cv)~txQTP##E-gI)rSkO zzuvOnx`wQ+&?ZTxAxe`}5X6GmMw?jBZWo%ZmKcI0Mp>z(Rjqnq=T4*3(&JKy z(Y(Yd$y)9#v#J}t{r&nbyF{aU+~oL>o@=kS{P*8$V)B@|nPb}ZF|jc&HpUf=agCV~ z6UT=X152&?$Vbc-g~=mF#JM>uuD;q5bRyxNJ2=x}L zsr2<(($k|0W%~NrhQz$Yf@Bq2%5--iPZJ%>x>ReUih*TTZoJW=rI)|i>Oa};@bO_2 zlf$ZWBaR+Fq&WXVSAOi%l2X}AD*gT5aOuVZ%^eZ0!tJ}pGEjsVb z*I9SR7ll>py!A6*mX<3P9ee&cyZ(5y-o|mgVMgQNevQM2WT>Eo3fCeDNbcabFXV|O z=)*b7S#mMi64IVbwOsbxk*M$5o89o|_X#05lcPU-*2C}rT^EH3uhC|_t~nL8CaKq% z#yG0{(>69INGhyIks?7_A*9Gi(w3fl&Se)=9s1WV`S!;@B*y4;4)1%`Lt8dDKOb^_ z|ExT(NxP%0o3x80B}ImmR<0e*=3Se!ydYf4^61)@yeB z!43K*Y-2>qX+H#+E!xpl6Gy@q3_+Nk}y3;qeeu)43GST6Nrfg zt=+sh%0FgEP_${EpmVlq&$gQkJ^qNPpYOHj&J8-|M&$*~DXJ5M8ofDF|M{Q;|FBiN zHsk4g-l6%zzsts`4kMBxrSTYFLLD(!5YVxnH*V*<3B3(85j#i+Nc+n2fo@4LF7Kk; zNm?N-6EZ@e-KO3k?R<^;@nO|Lrk^8JDWil#eDPL3i76DpDJlB5^S-xoZe$?By*^uj&?#Uk>n&LLgJ*NK~W_%C`CHZi4N3waR(px9zVtm3NH&@EkTYIxAV>$ z*`6^_!yM{pppC*QDKg~9(TNHM`R-kOdOrs+gSKB1oL+)5mR`*2k8{hVYz}laP)8GO z3ld}~Ax8-lJp0f5-Mu`Dm0a+rY+TLyUe=W9FDwX7YR+=-I6r=z z-Ctu5MlgjY3a1gAU4jgqSb`obc`dzHaBhiogz2XkZZU%?OkoyH6o}3s`1J*($dDmJ kYC(Y(T4$L2D*J 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�=3$KwGoWyUTjuLbkI0R=#_ z|5FqNRaMb-onj(y7Np7KlfDp%%$T?3bGh$o0a>Fj1nLGZrNnWZc{PAg)Zz$a0=t3b zvqA`gZQD4GLn@WRG)-*V#5)1}s!H*y9YLf+yUs^E$3t2H| zK?(=(=pNb8dGOpPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01ejw01ejxLMWSf00007bV*G`2ipY# z76LTg?MOlZ00xFhL_t(o!=;y9h+S6|$A4?>eLs>(GDE&ZTWug%O^uod1!*K2G?e-v zl8#?cUqz%3g5rxx5eh*OjQAuizFEYGB8sSq5hGMd15Jww{m6tS!6cnzCNp>D&fI&? z*?al0_qpfvPAVyG=5o&2XPv$NYyJOg?L9(c?A^P!GdDN48));^!y5OJpXZMpIkI~4 zkr2aKQi6E-@RtrEbUc6|h zPM!L-_x|XiLx=7;6Z)m6{@i@-R}RLJo7JQ@0BQa?Ra&hUiwg@fJu_oJf99DJ+qZ8& zx_|%vdjg@q{O%)s@u|O_{M?g&vqJ)btF?{VpRf4nda3J?KREJj2G%lvD$Q&nP2x@))F zU?yv8eSY}0!#w?`*VwV=gS4Fo0TrPX^Zz<`;aflg5Jm&$7y4kuMlq^lW*f?-0$K@G zHBs(cEfB$gH8Z|)_yL}L_Bs5{_u-uq0_>db%I%x~(Ygwpqa3a?8jUtoLUlE>H*v12 zIOlNA(Q38WvSrKI)_YI8(_!=GDY~1c_|EKm>0LX|aIlV*Ll##DtSsHMu?EbH_0fQ` z9No3{E2n0*Va_>6x7%fUdK!RErvpH@+l8%L*|BREWpN)r`T8_p{n1~Tx&H&WXcU9> zFa}VL3d&+gEJqu9Y!!8KZiHYMF>6wA&eQE~V&|@1Jb&&SMNxtoqL~&;x|rU!jbD8K z^L*iZCz!eKA+T5jh+#x1M#QqHg{O*CK%!BElBHgs<=z@aSrTHz%!naShCo@Al);E0 zRfJ)fB`~o!;M&baM#YFHKD&?OzrRF?=EeYuqF^vsXOJ}zEF!@WNm!!6&09;Heeo48 zymp2D+92o0(w`+-q8V7qj*Z_L77Y_4gvjn)+j;r?b96hM(HOvJv_^k+GSC7g zM8nTrx&6_1?Ovc?l;a2{(N@nE4X3Fp%9x^yV+~}R9IlW6I!Wy}l0al>qyP*&21PU+ zmeA#Bs>NFhg(cKz78NNHvE0cdQz1~s+{LSPsw`SF(y5s9Xcs8W{at#KIB z>R#)pghaSCD#zy_B!3iQnlm882xhqx0uf6o4-h077O~cV04+(`u$tqla@AtlWe9mC1O7PiM$F?GX3v5FE@DMG}*|AF0nXx~081fqguffPVT8>N_6 ziV<3*RYJ(MUNxy$Ut{uti;43hB*#@xh@!l{c?o}OX^mcg$Z8SjWXDb9nj@_;;c`FM zM%{;o7-Pe>0-+gDQ)8%d>&_}3frb7$PK=?Yv%kxEEG?DeD5g3u8hH)Pipd*M)rx(t ze=^X}ph%@^$LZke1znn7T$vGjf0x!|n zI`fV>0oq$1JoF8Wsif+t1|lLVW*RJtfLJs^tP;OqreI=bBB~Lv7%^3%rTsi2Vk#0% oFhSBuO~gbbBC@{n>X{$^7YBB;Uy0h!LjV8(07*qoM6N<$f_AN4=>Px# diff --git a/share/icons/application/32x32/actions/document-properties.png b/share/icons/application/32x32/actions/document-properties.png deleted file mode 100644 index 4700a60d3927ebd2d16784c9673977b5c6a7f389..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1342 zcmV-E1;P4>P)NT=s)3 zUb0J^zcw*0YJ@ByQH+T?(;_+yae~_zgh{e7CwLi}up7)ND$&WGfn#HuZKx3Lh2DEw zzwfN4mcwZ|Fw~R0`Dpphd7k%qpZEPv75JYoh2XQDgzaC(yiQ^$5rK7nzyDMq5C9kk z4E;uAZu zxEKXSgqT8PmpHh9fTZ$BP-CG2pbE$q0!k&3#2bxuC(bR2&J#W`L)x2FO&d)~6L7b6gXQ1ZA46Y$YnmvHh(86Lbh7k_v*VB=$3 zVy0b;gftV1)jgWIYwRknc9o;^M;32xvhl_i?;JK z$j@Dk;(R68j8a&wRu~KhsMTsw^D%dH7)2kzvO!$CSb?*>GccPBc;tRyR9T9GoE#X9 zMrbsedG2311*r027K7(s!o^DyVB~jU%SJT@=N?C)-Hwcm3~065q~Ld?0ByJH-&Nx0 z0T;$y8a%tzjGu-#ps1(-7K9iWzIWZ2+FTKAbG6=eXVnoH!D9`BZQEgoR-0G#7nk$Lb!OnyC^Wt+UxaVW@ZLcQ`4B8b;0ZNLr(5BTQWn=Dn~QfdSL;9 zj)qOIX(GQp+4Qm2+Ft)CE(~U4%~~s$Ic>N2VI!73FT2m@!{p=?Zrr?q$%zS~njYDt zYcwfp>~lIlX>DmKr(EX|Xm}Mnb>@Fn z=jPlPA0NjUIW2c=+s-L6GV&MpRloD)(Z$kR=;ZJKH@RG%^mL=(H2YvM2$#!+sp)BGHQE@s93n%80@KVBfycKXPk?&BCHf+5HVX`Nn+H=R|VHy=K{JsCJ}Lb@Rst&fk+ciJ{f z9jn}a@_WK+vxVMrI8U@RHSJmqJiZMhQ37EF$sjEF@sadMrF&&^$doEDDxLS;>Z-<* z$Byll0xmWQhe+MGVfoh#iEgRjVg#;4<3GFp1}X2qoP(}4DgXcg07*qoM6N<$g40lJ AVE_OC diff --git a/share/icons/application/32x32/actions/document-save.png b/share/icons/application/32x32/actions/document-save.png deleted file mode 100644 index 23079aec057709af6c66107f364d16065d36a08f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1299 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 V23W)$k)r?r002ovPDHLkV1iGqfmHwi diff --git a/share/icons/application/32x32/actions/edit-clear-locationbar-rtl.png b/share/icons/application/32x32/actions/edit-clear-locationbar-rtl.png deleted file mode 100644 index 0207e82cd0918b375370946a768e3ed37d78a985..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1340 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 diff --git a/share/icons/application/32x32/actions/group-empty-trash.png b/share/icons/application/32x32/actions/group-empty-trash.png deleted file mode 100644 index b272ecb565aecbaf5dde4c7ebeeb7e895e1bcd28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2162 zcmV-&2#xoNP)b_@WDhQ zi80YcMBgwxfM|dOjR+~GX=zJ^7J8kT&diyaGc)J5?`yB+!)Z-zX*)jgOLlg$lD&VQ z{C?l{y9jG7pQK4XVRNoN5KkQhD+Xwn-*H?8wO1&!ucVj^Jhi1%^2Z!UR!m`So~B_E zMvF-t&!yq&N1S}|FdO!r;Pjumtem{Y>6KA;u8U>?T}+_F)7!QTe|_)v;XT_o*4?t_ z;5aVU2(-57!crJPt}S6!&NZ{S=dB2%TYz90x)#vM_|p@wR(Emy7e53#H`D-#ryqM@+cW!b z8|C@8Lq4315JiqM0-I;l`e*sh=M_&LxRVRbF0Z^Z%ioTzaIsw?#>Y{TLR(gvi@ZHG z!?{!ExaTVeNWc8OCq7y{a+diwe+H!21=!Fiee0fGBfNApq;=*9cU41j<4)XvaW;-`yz|KV+HoftvL&1~K{PGKzCCUiOxtyYV`b2xC<7-Q?& z2b-!hmvuMh+u42Ji|oUa{=kTH@u!Nsg|D_@2ku$S}Qr%!&6u zpx5tn%Z?pXDiu7>V`^%eEh9d6?A%GcUdQ)+Hjc07%|mDS^T8Ev-}5|cNlP{Gz{2TX7a;_W<6tdRDphX1b&~0+DQdL}juK!JRyr}i zdTE}?dmkehALYpO1+eDkg~(Wgl<=uhSRa&FAAEtMae|x`G-Sx8O5 zPz75zjnN4+=GvOsc${O4W5nAwBc{3RU3xbk^RF>*`OT!J(d5je~t$t2{1kBoc0oDpE z2n#it;t-H%fs!y%g<2U@+GBO4#g?i=9`{%Z4MX(?sWXI>l75(>b3@=dNReVugVN4g zA^<*EgoU#2;3z?E!CEMQq}S%cOcSljRI3&COxBs6U&1J8#t~iZk|r@y3UpyGR{n2) zSxX8K0x2z~04W9^3IT=IEVU!5L+kN8pHv&pU0lGkDUK4zxJN(s5KakSNwfirKwS22 z*D?>-fe@w9h$~bp1oV1+y8RF(1yLN+>-BKZG|CR^%i!9SRyU&;X`+6QUO&WGi}`Q! zSwjl!B@s#~$%?_16lM^ZiwmnwmVgX6Obh#BR0nM zG}Fr|)3F@<s_Gl%{l3SdO7^YM@F4et_>f z2q94l8YK`IEOMG~_2ueO6KtOH;eyL26TYCKn&xDKxmLzjA!bd3uq9Dx@P(D&B z@+_md8nKv^kWx_CwcP?y=p4_Jc+P;`a|Gv`ZT^028D$NlLBVjI)EylPg*GHrY zMD6tIrf@C5kyoGl{cF?g*ib?$MXCpN!S`H3Dx7Ia&dl}s*LlNK+a*dg%JFFok5M0< zAPA}yI>!-$UO!>A2ndTDbLJQ3aGp_+NH{J^dX{s~& z;E#sy{`PmDc=UmZNAI8D#FF6IH@aXz=K^U3u2eXVizfuG>k@bdN2Y{X%Is>N=Gk+^ zC;qy``4`?MoqHL09cW(Z^LhXPF0#6x#=dXXcYW#M?Vs6yt3Nv7xIq;e)KG4K#i2l; zbwLtE^g127%PqPyr}FgdKWAya^>3on2f@Atya#lzUQBKTaM`NBB<{#QDqHvB*C%8! zR99}PBBbLQYlX?v!leC_xYH+HS|pj8#$=~}Q^0v(^~z54{{mb#1=KD*mBHMAJE+xI o2IE2s#6bU&*s_mZJFoEhA8f^vszv)9GBDp*rm#gF^!~R7gWg)FKU344M!%hWH>gCTdg&LD3fz zf*S8WRTB*cH6byPM595u8o(f-Ok2`oWu|3%nQ5nI=FGY6y{^9x=QJaRnSws_#g(kA zovgL~{r$JK{-s2OOW7hWal7OI>fc?UcKmGZl>&%t#<}-n{IvwR5@86Dh#(b^S3yk^ z$4?==MhM^pM8FI9uqx{8y>AmhWCCr!gs5A4hx#YRw~jJ2)JL(H$9rfrG>d1KIdkGP z)zfEB;oRdS$&U$46aE(f1z3LVNM=C+91e-#L4$@!TdJ>G1`77*Hyabw%d2IclRWHU67?+-7at*)zY`Jrp8#*3 z1X0{BTt-U_MD&uQtdFtuR@$ZL2+!7zR9Tq#@>SvuMH4n zjG*@3c;hzuyA^2$+O{>1|S&R`fG z89_Izq_q{gFW-(zq7UK2)oTM-_rczg{_*_4C})=i2_CI|$NV1agr$a|QZ>Z6ZVsMG zICwImWn-GD!Me~{8^}z+n1HcDwlYtl98($@>A|>L)&_8H$L8@dma~{&KAY0i9`D;i zofqa7>V(#Lw!K4h^<{=V*F@a0GsmZP<-w;kb--AUvF%aYfOd+?(gHfGvSDZlLfF1G zfN*6`-$t65@Z6!4Up?Ex*j0#n%dn{&v*+54+EV0=A*!F)bXOFM`i&mB#9_}M1arX%>ka9@dor*eE@{~WKJ zsAIIFTGN=!VqH5^jPW=tXyZA*vO>LC2LjF-h!apz&pH4Sg&+hkARgkt^lZvQ&(t_H zm!gehxVM1z35QOk6!KB~vQgjx=LBPgYBS}`(h`kU0|+32-~%B95K-%8!BUoH#Hzgs z36K*OYnI<1sPpJcHJ(4#pi;Bs6NQK;h@ezk`!kg~uP-jMuw2C$-M&IY0Pj!=ytd#% zV;ulqtyQXY7Zim=z&Y?9q5yanDw^q;I!9(2h*F4vbWXNf(@?9d(8^LIxc1K_KyWAo z73VOuIs`wrHh|*gbFZHv369>P0`DOBHiU#WhzgzJKp~-RRuNE&j=4KpU)bj@Nt_Tx zIm}`O;PBc2as0&6nX~iDXBHS6EVkny_|@^kH`0k&;SDvVg!cS}F?jHfd{-G?YhdQj zW~lhF^;|HEb$_0jKFa8(0)0IRINKKHT}ODQs|X1m9|A!-l~3rb@tqzFagw7@?kAmn z9h`m|rCwYw3m{2;aO#zRG#37OoT)9F$i)KIw$0noh}7YHd;J9qWF|nWS3!dT36uuL zu$2|EnVA65j}S5IWdWcLA@bmnC!a?eHKry8$VIdvf(PK;S8QZza~`c-C(tw>+cm?EJE6cw*_=m?SxQwaE5lUSN)Kl)* zJ<1=yy^V>HBB?5oEA`PZ37`G&C=Y$>2D-{c6pHfjR${e*#tVPPS1V6K^3{u1#qavz z>WmM)q+^!^6xkEK$e@hPoALYTESDj(nvA$rYGA)gVc_aLYAX2VTZ6 zSDu30r-75Jp8n=r0st=sw7p+N(S7}s6NUaOCh!|d)aw~KO>x%Zy+@%?ae|5x5|fb~ zpF_{g1hHm6Bwqt+Zw$4!4$vN5xB+9ntdivB(#S|paj>7L)CF;l-~(P;Y`uXwSHa9L zWa6@?A^r(Oe|qyO|Lp=?hz;+zWBe`n@Lm+-sHg`);e5Sw!W}}ze*yJ3M8cYHoV5V{ d|2VxA?O%5CiR;=Al|BFf002ovPDHLkV1nRkd!7IQ diff --git a/share/icons/application/32x32/actions/key-enter.png b/share/icons/application/32x32/actions/key-enter.png deleted file mode 100644 index 5f20ee92ff47184d29cc9ced10bede3cf06af4b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 760 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 diff --git a/share/icons/application/32x32/actions/password-generate.png b/share/icons/application/32x32/actions/password-generate.png deleted file mode 100644 index cad78bc7b055688210a99ce6998006faad972374..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2457 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 Xd%_JThXh2;00000NkvXXu0mjfdLWc2 diff --git a/share/icons/application/32x32/actions/password-generator.png b/share/icons/application/32x32/actions/password-generator.png deleted file mode 100644 index a06575f8cf52c024ce53493994649d8bb5670e20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1666 zcmX|92{hDe6rZupjBV_)hHTlgr$~&kjBGQ;`krNsEi!0^A=w{Mcoj*=d3bUTqEfx6 zr?QNSBzktTWhZ*JUZ&plob$eO{@-{1_kQiwFb<+r@!w4NiW3eraiGBoY}A5CFg2+}wu_9g>ogl97=) zh!O|{0RaI)K|w4Q3n5iiRXsgD2nh=d3keB<6GGs3b93Y1-~coT0U$&@JUn=LdGUBW zU;@6att}djPE1VX;o(7{P|nWIDJdyXNJK;g5=~4@s8p(}tE+^B1bE!t-2oi3AQz&r zFc{214>TI>@ZrNyh>wpC{Mp&rIyySgKxAYjlol5khobNcwz07>?3|pO;7LkK0vi_e z0Qk^Da&j`APCszMo|>8pyO@}msHkXhaWUAC1vkNfT4`x%1qB7(-rnGZy0D_7qk$uY zfC6M4j0lJWK0v_*upt*HK$Fk|7Z(?BIp7X=fX)FFApAdGpbVH$6nX$m-~!W-l#~QI zPz3-%6j@nWIXO9bd3hLuf`WpgqN0+L(vc%al$DiLR8-W|)YR40H8eCdH8r)gv|uv2 zy1Jm3zP`SJfq|i+p^=f1si~=%nc1mRr_9aGPoF+*VPRouX=!C;1xnf2*f=;iI668K zi9`~K1agqcWYE#m(-XM+`1ttx`uh3#`TP4*D3rj!z~JEEkdTnj(9p23u<-EkbLY;1 zIv_+;R20mI!C=J3#xj}AxVX6Z_;`>2W}cCek(HH|larI1o12%HclGMk!oorrcu7e~ zX=!O$Sy@FzMP+4Wb#--3O-*fWZCzbmV`JmPhYy>Zo1Z;<*4Eb6-rmk)vAVmv-@bj@ z+uPgM*Vo_QKQJ&bI5;>wJUl)=J~1&dIXO8!Jv}osGdnx`>C>mVxw-lI`K6_$)z#It zwYBy2^{uU~OIDfRK%p20$;lPY|5V!73AO*w1hJrn2uxZmxR7k1EYy6t#r|IQ{<&7rnYk(a8_`cH{z+xG-lv zI$C7!l#H_SO>)s$3oK>Sc&e|2bam9xJ%@DC&G1w?1^$my^}0>+C0qjU`tO|VZVrup zuySLfTbTB%IxX@&`Tpmos_T&huYxxFP_1m*T{o)ad9F}5KBd3zaIUlj*eSVCg8C)u zm)(|G*$%CXPl7J^3#Fp%8dmfNGfCbPwtw$#DN6Ex5yr(Pjw&JoPi|-<2EE-#sT8tu zmdgk|ft9&3TgH>38e&{!87E8h*1VgTerH;8DJ7QvjhytJRx(h42MfD+}cdvMM$;sdllY7QC|z=K8#>^?o;f~dKL0l{YblS zJi|-;%#Cqn1(58IR_z4I<%~Nw<`HiybT!9Q@%Be^(|4{U)jVEUkQ(ey>KyH2wb^}< zb4?0PdL;3U{P{-V8~n%<%Fl^o=jTFhcD-z2Z2Lx#yfw#Z(MU;q~UR+jm?XDL#sSFEp|opgpEmyAIPvK@zDGjK6sS~1i{(~Uyt)o F`xj33X*vJ^ diff --git a/share/icons/application/32x32/actions/password-show-off.png b/share/icons/application/32x32/actions/password-show-off.png deleted file mode 100644 index ff5fab697de289859718d9b9de28135757ab22a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1849 zcmV-92gdk`P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L0B*Vf0B*Vg*50uf00007bV*G`2iXY{ z02&;s$-K4z00y~9L_t(o!|hjDOjTDD^*tW5K&=)@qoj$XMhsewQa-E|gHgn$sRE^9 zkOr|zP->iTh~hlq062iks3_vd>=Xu(1n0z{qM!s+5EVY8+9r*6cde8AjHMvb@ux4@ zxjgQ@XRp2X+UuOxqsRZB?tKjZdPWNi3w4c*jC9M-&tG@$+_}>8=g;3kTsw2-Ovb^3 z2PgBfe+ry&^ytxj3knKykW5aVJSoSIAJ-7P%F&}oB`+^e&YnHna_G>ZFoJM)cK-X4 zZ{50e0uuk4lar$orlqCHu3fui=gyt7fB$|3fS|Iovo$^ghRO#F7|`3r#pN%vVN6U+ z3}MxK_Uw^)^X4^8nlvfH%ggJ>Awz})GVu3YUtizkS+i!t3>S|cLc=7Ok`}PT{TgSy692~5>dJjNMxbzx0aNva5vuD?( zq@++AKaC$h-nC!9e%->*6LdCtczF03jYfy=DjQ&}aB=bX_fOriV+TRhKZmA)cEyZ3@ET!JvB-{^zLQBOrr~jg8qqdU|?#ez$GgHi4jj(7|`^ zH&FUuJ<;#5G3lsLqXuKn(@&p1{R<}pN>_pJQdCs*8p6whW`^o?o6Ijmto8u$EkrFo zK0dAnLnj?|cXv0mrx6vfln>L>)03DC%2t7dLJ+*tgif70rGoeHY%<>I1O3>T*TXvY zgh=>S5yCfPf@_UxGw85y{Sit{1Jy?gfxZ8uLB<<+ZK-{_#Bp+Q@?a3KZB%}Zkun6R(k(u8sD z+_`<)Q*VO@FB5rue7uGP8v9HDq?o`IdWW=;0VLGw>+9volP9XrWo2cOUr->? zpf@NeNM_8KA+TG)SwZM_I1eZt5KL`tt+ceXNON}IDlx6A3s)$5I_kk-;ZiSLPKTj*s+3VWz3i{GStIE5)u<-A2uJF5-Y8# zsZlJjbz~wsI+~2C^8=Wx;C|DQHB*)L7^w^{0<;`8?dj8}sucz3bsiYV!4>BK71*ZP zTS>#SG$tI?gp?CBlaoCLnVdg=KJA$85Lk1JS(JYm1Ug?rLc(QI$uUF)+KU%2sA#1F z0;8=)U_!8Qsf0YRP#=7U30}T@DFCjLlSKvvOc1+w?^bIC1O#|><_FvhoH5TO97l|n zVxhRWSdCOtQj!8eA@X<@9v;q-Rn;-5gVxqo#R3KV=+PsotE-bM7*&8(0w)NFy-sgM zhnoX86G!;KTAl;x@vH(biY_RLi;q{;k_CcVzkahiKwisq${eC zq!VFm9O;XDBXr~Gse?EDlRxbY nZ-Fztbyl?K@n8I&?9S&mfAZ-e=1hxm00000NkvXXu0mjfhIw?- diff --git a/share/icons/application/32x32/actions/password-show-on.png b/share/icons/application/32x32/actions/password-show-on.png deleted file mode 100644 index 72cebaa6feefee57fc695dbea62aa7a6e92ca6af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2089 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 YRWH9NefB#WDWD<-Pgg&ebxsLQ0OoZuDF6Tf diff --git a/share/icons/application/32x32/actions/system-help.png b/share/icons/application/32x32/actions/system-help.png deleted file mode 100644 index 19440a190878c69341b4b9d24dc17e7a3ca27bb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2141 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 TP+g;&00000NkvXXu0mjf6}a-G diff --git a/share/icons/application/32x32/actions/system-search.png b/share/icons/application/32x32/actions/system-search.png deleted file mode 100644 index 9c256834747a8448359ca937e09215b817ad25ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2023 zcmYjS2T&787Y;~|bnze{SRf#v^di!v2nZy1m^)E1lmrkW1PK@^0Y#0CqA1dffEQXo zV)zA-L~0N?T8JS5LW#fuN6||F4dtT0Gj}`l_RZVbop0awzTJ2KXNa4u3`hn50LXf{ zyZ8wG!EYudA)GPs@jRgu!#H`sq=X|;3VBZ$OGmk%#|ZZ`znO@WUjB9AqFRirZ_HVA zNDMYGIv9Y(VvmPKqA-ZSsNm!1=+OKXTMz&siS%$e1@mohcoyfyV5!iw(1TUC458eT z($a%7q8)*XET3O~270^6rivl2sQArTN0ReKBFL?Gh8>W7dO3Y1^s8Yp!>U}RD}FQh z(#EL=z@f&Hir^fXj5xwPhI^#Wi0%{i)f2+OZ4|9;BqYrjVO9qYAAW|lz$Ma$hj)yz zE92Cl)Z8V7b8e2))6>+X^}lk>EiFNazVTfh0Pe~jNx3cetM)f*^gVJKGfhQ z-o!riuMcu%v?OdKi2>d2iVhOy@v`bGw#hqwAP~qa&j|i@^z-Lg&p7^KRz0)M|Fzog z`R15~%oYlT6Y#E0gn3f(Q9;3$K(Mje%cW(cS(htNu74{1BZsE?3O`L7rxT#FLuGEI zEagVdi5n{u&GMW#u_)Z7ONED`C0bV-P|S}#Sq9NxJ_lFU)>?KYr{F$+y#FQkL3UP_ z-MQK+ImWviB6~;{FXT)=txY?}txerjtRoUFKB{t0VpqDC$6k9M+@fVS^z@u@3kaB^ z9UzMIw%q|jC*H=Bogmn^qT{IV#qswy)jCFRNLz8m)T91cgCefmT* zc6Dd^D}M>^XBOy_E>)_cqC(Ws(aESk;+9TU(xXVB>Or5(Li@Xm>=N=MWI<15b;p+_ zjAIu^9JH*XO86c3mE|~di;U{i`+Vkiw*?zkC{OUw=>%2oElGJxWq`d@u}$om{$dAo zIG49Tmy4kP);K2XNQaAWjyGYVr`yDxeb1gnaPAp8HpcT8K`QG}@AGKv1{CVL;8o~A zkB~i9GFM(xb94W(3ou3`U=fDf+Pr#I;^x|$*d^ph#V;DKNdc{*xpoQbf!G3$wA3@I#1>vFr&o=7W$ zNBP7(ex?e>Nn6(iqxFb1c}l>Cn)7t9oIe~6cWoBg+}yk!5)$%kAjhQ4>EWQJqT;E= zijdnvwp_`M!SnrvlS|id50}{u)%Q<)SZn>Mulw0!q|K3<0uHe~_8@Z3ac% zB&cJ<^O<4T1SuF;iP+T!gHQ8FCg{qey1*_~Xo2-m&GaoJ)t6E4#C6AaO#rn6kV2bF zxn`m1Eo3sxGICt6v$M1G;-TJW$IM*+Tv{?$`0AaQm-hD`1{xHX=`f(zW#(rw@6$Q-?Lw?aq70ZEV;mSVQ;{5Ds|T(2!ROh zd3rPidIAjU2P1)+KX3}(dsr?0h41%3_&gVA!ni(s#>^;C+P{-1lw0?9Dt z2+`5!2?zo%W!qKiYjyS2 zFTwj79LJE5ct_c)h|P= zy@2j`xdZyn_`QmuI%Xm176h%FI(zTO$|oO`n>)OZYCFJg4}y4`4}ov!N2t3|leV={ z6~8J;2;4p3nfrFzz&cO1W*_>0ea|k3;9)ooO-;>X@w?3ML)$}Y0K}YcyQIHyz_eb` zpSt4pg|k-FGnoZ1i(8$ml^j(>o}$c;@s>X-uJ#s_@Sbtgp{5ROeb@KTGOK8Im0N~pGLx4UqqyCK0{rJ82uluT_hY6|%M(^j zO-*xaCGJXP?cOI(b)BdaY@IgPGI-sqJ-!{?`aP2yKuF_|BhZ_9tNUz3Gv(Bi|J}oJ z-p0wL0l+_u=f9HIcx$e%g%MQyT7Z+F{3seNPU!A|^HlFw!Qa7j4?6inW)d32D^E7#rvxt(HhuF0rVjAVq}ImE8sRz6;CV zW@nh0^L^*^kJ)7}vkTVNH~DgM&X>&doacGpcizt?KP(_Juzr%^14(<-p#TbjRRilM z842K^J(dFTc)YZsp<&nHL1R;wJaVL7wzT&N49u8OtXHp`<4>3n{@CaxB_)rgQYm{Z z0!S%wT{rZ@pS((ONsNMmFww#S<`fn3+UsZ8ckn#V{PrDm_6iSvbrIkB)@=wOauN=% zxlP`03n7?LRD@E>0E|f>5WZ!>?&puPdD99U*QU9(m$#2K^475iPMm7Ru_bHnsw7}r z{P}Nnyzuf#{q2!ex=N8pyFPwx2DvkyKpXLg*X);^|AiP3*WakAzj zM^Cqdlw^IwUk=pr_`}QCuzn%UEj{emSyHnab&^C;g?RvyES1kl=mF%%aB2uA}T1(OYg!wxUJ^gc7D7E@Un zA8Exf6$bad?~k&e{Kw(VTUP9uHoX{aVCs|@>3=kEQ`KbV%`D;YyG_VLbx6P%gABSz z+aeSRko7dm^O-$o5<8zfz}}Z17y&j`F63FTbY9_}g$t%whu=PrG7ydgI8}d{v(0@R z+Vv(vN<=Wgx;ti2+t|a>s!7C(LL~Y#1cDBMkV_!#al^EU?0)_jTQ+?RY3){5`gaUMTB`GP1jlE5RP-tUtd3qT@Yc`+%iPa zAtNPOAt30`mC+d6;^B33aV$wzX_6@)AAu19q(Im?B1nmJEZ#g)&zCkV8nXh1AK5uZ zOjAB+ZTO(6hiFNZSV;tBNo3Gv_2P2k#bN3$r3l!PiBXr|zj2=Bb4$3PD1a6NM1%k- zId!Ik#Z{$a0bUhhgaVPU>!yXsNoiZ8wLzskWZ1$AIh2=1In$A%y)T0?hT^cz%0-ij zM_oMSk<4m3`_h~`)6UchK{CoClgW-tAkPAfZn7;20)!&jzwbMMoK{B zO3t>pb3qw1%c7)xKted=aL-MX@H~xDKJT6HA|A4-C@h4FSJw9{$tdC+r zE(f~XnqF~2g)c@5LRQeV@TEXVK|#bJtwBf-2n^sG@Qgrds3 zqYZ^Yi|Nyf$Gv&J1?=_x%=9CfOyQk=f0{@^5Z95sbH10d(gGaILKp!eH%0h>GT>=L zMQMl+diycTpq0V+aNifIu66sdJE5?>`~BWzvr`n6nPdhX4$|Ipg+IL8#>99bB}E|? zm4%7e65rEgwISmfnl7g~_U}$~Rw1My;HP=&N9(TXy%~~V=+CIL;kmZrvTZxuWbA3D zIEuca&>4lVG+h}@Z`u${YN`rtJPk@|GHFF)YcB0_ z8%_p6hXap04|KQu`n#6c7}V+8*B9PA8;dZOuMvqfDNl2vTM$r+-tH6~ZAr5ISyKP{ zkl#G9oX@XakO%JjKE@b)-yfU-a2zM!f)T8|zgHjKyLPjEf@Q79gFgKgwL*@%c_-kBZWW=JcHoI7+P9dxNzYD(P$K<6hcaj zHbf#JIyyQMKo^j%fa2<;;Z@8I__btvfr~wc$&WtxWZh4+ZX=y1-mMpoN1D>a- ztv%;&-M;-7z(0Y6eceJG1|Y7{QitE(_kEO7SCeiKysoZJZ{5EA2i5zl_X4eecU=PF zI=y`KJFPWJDYDrtwryVpudlB+Pke93&#L!V?->ABo*_^YGAuS*~j{_j7%Y}=-&sECRi zZ=|;N+@-I7^BX%GKWsb%Gy<9HHX^`pZ*T7uLTnnI0v|&N3+Mq(j0Zjz0YmPq_0YgO k@juvP20;5p%RW5*1Drl4>aOOhy8r+H07*qoM6N<$f~Fm~xBvhE diff --git a/share/icons/application/32x32/actions/username-copy.png b/share/icons/application/32x32/actions/username-copy.png deleted file mode 100644 index b781df635b44a7794c30798a97b0a4b13876bb84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1722 zcmV;r21WUaP)&9t;rW~Q^f z=Xc+{Svm_H!JB-2owwX`zH`3&-76qIsQjPU29$EPtr(a_3&R*YCSa0%H_sg>(uO78VwCbaZ@JyXRHS z*Wdr3f~c?~di^7#mYNdV!P*n`<5Q>KE8Qu&#bVj+^?HSL1R(G{=NWVBzdU{lp>PPi z6akM2B%!5m81L`gqC0Tp7~SV$5h2ONq*OkdC^6leVm2cf49euw5-=K#stXOLX%Y!l zB}Fjk4KU~DWA&OfC|fWO$Bul8!)GqTbIXl9)0EqSb8+Me@5GW#M<5Uih~1qXc>TG@ zv0=*=gf%&k)JE`X9l`-0w(Z)3qrW{5W=%{11tbML2{1}P6lGtzv;_SAplG%hqN0># zr=sA&^pe6snpBjDEG(G;on8Z;O_)R?MU4a%qQAr8aAphGD48=II&Y8cIVXm}5x68$ zJ%~Yh)|%=n=nXmGcs`zeQs&9>r>(b44;GeJVW6=t0#2Yjqe2t$M1+xec;yNLS7o#y zZ5^a8`OE}#c?AGT4F>f>*=J7T z<ore2^_Z#aybz5r}d`GNFG zq*-!YYAtZLCBQNq9QDB7(t>&ON+6OAb*JmG;)%5r8qn=XWEk05z$UTgTjA*JLR(KS z1hpE=s+SXhDq>3-&_v)&B(k=kyR#GK(rOqD7opPYV9GPWU@)T7D?ryjh-uSoDXlmY za3(V8_X0uC?AyN=j<$9j{^Sq_Y5ilQO|D@l@}W0duxIyssHv&O`j=k-&+{oDSz0i$ z23Az8u<562j_M4CnuarfLZ?$hrBb1&WD$P+yb(E85j9WEhr`~A-Wz@R=GaM`J^L%F zsVQyRya5`GCMzN-7OYzJSh*%o`a*!J%+>GXJfl9@(?BqY_V#x8d_J_+9)iEP7_-YC z!sRPh!X{Ad}S644!S7*AaYkxSNGjy3dAi?tGPGE zM*OtpD-%j#c>{v-d&Z2J$fvD1(A*5CpvJ1ro6y{BhZH-dNXj=6prIir8XNz@(xpo> z7Gbgl53(Cl}<8r}kx6@AP#|`p7ItlG+a-E3mxAOh@^AQ5c zpozdS0(3CR6NJzNW|5bbrP$pAX?PeW0&=;W3V|Ck$;c4t-8c}lWSHKMf-{0_K6Z`T zrdF%t7Tg>i`bDD_?#s>1Q^zJz3XF{&A`qbSCp#va$H(F9?WMx(fTEfFSAJgs4+a8Q zu%HxUV`K1mJP3tCR7-&f0V2X07@av|p9*b`>)Xs`^V>de{t2~4Tc+Z9i$<&FH5#3q zg}v1Q(d)r1dYh*S>=!SQD&6w#$R-U2{a}fi%qEmCuEfFGkK~M-sb<&;%E`%L=Rdiv zOQJ-_Jv{~n2H1&sm||RBh9i-&M%gVbHk)nV+O_Mp4&NMs*W<-Mp&*=(tdt3PE?j`k zR)C_SskGcIc;?x4sH~`%;5So(WnNHFfXd3sn5`k2$8K*4@7THX1G;sIVk}cDzFJ%R zu`D5D9_{)=Vzg574lWs~*NUtj<8SFEmClg(;3ywf2s zH{xb8<>K14Yr%~hH|=g}YC4q+J}CjC!SJ8Kne4QxFniW4mbvrA^)J2K)zQ^JkGd%Q z*=)z*{{DU!$@@xT3EY7Yte=liT+9TXjsPoLwniJ2XWU)bBq^ZoWZ4tPzu)l1u>~D^ Q%K!iX07*qoM6N<$f__jn1poj5 diff --git a/share/icons/application/32x32/actions/view-history.png b/share/icons/application/32x32/actions/view-history.png deleted file mode 100644 index fe9a9d113a0285f1ab2547b83e9618e8c0d186bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1566 zcmV+(2I2XMP)wd5`;cj`XVhB1YeqmLSGcpMuHHK6x&J@u~JLw4;U;Mj5SG{O*UEczdO4- zvpc`HXC|?Y(ZVLI(wm&UKXd1t@1Aq#-1||mqC~^d8H%P^uU7=G#|us!2m~P%3PC6= z(E`tbVd!9*254Fll!5|kUIj(XL$Rnq0sHxU-Yyo4U=!OF%fi~WP;8SBvVOTeb|nA; zAW;C!@-fpiK~Ypxu-GpYR5Vs_Ygd-qXkbaKfAx_^)LSd<(T@$VRK&1!ss4q{iK(en z!ln#~6+jxiHvl6ef56<_G_WiOet!UK7A(_pjT;7fL@z=?E4W@^I7Z@lPTKMMt_06< z3Cpr1Tzz<@y~jK-H#e7aA21AW5eo(e2GVY5BJ0LcNwsYgpxl|1E5!L*woIpuGA_%q z`yLwSDaw5#4NoX6lvw7|bDQ2j@ZLwQtw%pD7SupzXQzDadJ3M4S4_RKWxZ+9;rpRb zWCCL^cXxN6E>&;I0GgVb#AC;fZL6)VoxyH@WmRQcGMV)H{eDvCxe@nAx6h`us^Z|_ zU?3b0$J^W6$I1!-5;23ppn)5)2A`J% zfQHhGFg-ne1C`4NLVfhpVQruc8aJXFYy>Elsl5lLi6+U9407gF)`8P&d{vT)z!6K5QO&C=v);|1!bvQKn%sg=L;6VmI!=soP@A!Ov+2;!$ zz%l-r0l<{|+41AY^_rTRGy*6bKJeD>n0!7xapDC1)B&`#v>ZNp^5iZ|yZzBec#v`}S2j zM==8UOVbpeWn`N|uwO{VVd%I8zx z5n_CZH?sPfLiEdlKV2`)}O3 z_?l_4UpdLL>;Q1}Bfnk$RSX6F((|zx95L_1za5-)wkyVP1|JMS!ea#Ed9FUXf9I8OtBOmI8Ka7z#pW&qBrEj zcq3r}(>6fYbfjl0s17$&7ro%JBTaL7qm6e4*|bccXo}Kx!=Y=W6mCf2#E4}PMn$^C z8Ju(Mx^6~-1Gu@|NsbYRpyI0Cw(hjCc8I9UjBK06cEo9;NgT5uvL|b4(d54X5afmu z6497NOis5MATP)`G0S6Z_+Hc=B8N^`!89?YG`Zm zolen#taF3GhAZ=UnwdCvE9zQ6Mveg|%|#DAXle~oXI5+D+QnwXdv zvn;FsR=R7gFANS2?oxmhLiAUw)tfT$`ROu~O^f}V3at(AG!=cKPTIG5YSQM;vuF9z zBg6Q<-w#LyV2mM-Ku1Getc<{5seCm_C)_v30w{aZDF-uEJ>jB!^+fhnwYW{eUpaPGX*#hNqosmNWeCtOqF+RSy<-)EB@Z9sS)0xXs zsz#);A=`89xUS;0e_dp9sm8m@b!O+woP2A6sl^IYi&f6et#WRzOxN}dPk#IN*9Wj! z30^!pisdL!l26~en@TID7Q?=~dpUTZpM!@6nYdKJPB}D@XiHLBtub@4%sb~6sjGma z$4=i^3Ep^PnnYT}LekL@5*a}PG_@g<2}rm396vovU0T$Tti%bzbI{5nu`Kp|bQg9e zp4fy)6vCxTSyAe7sy0v8j-BvM|U zTnd8FB_d&Vwz%+B$`?y;^NdLBzl8+6h#~N!27kMFGDJ@ zX-Yw@rEy)wFaJEw$WRV9?c-Y(i}i>`Q)86I08$&g(52aE^4hE8xQ!~?oU6AP>k_yc zz!x7q%xbZQBL$sp9;KS*&h9j)=c_z4(!=Ku_wv}`K1M#=g%^5A$Hwy%-C2+2l`1mU z_{wGX&aN#1HcgTIDLQ zoSGz#Kt~C&(fshc_uW_m0Ka4Sh;OH-+|n3*on zF*DD@_xAC`V-H@lbNWC406HD=jqe}f)Y&Co`r9Z!{rd@G6(R9Rdu@Dd|4zQ}^^ep4 zq4agNZXXz6Z!Tm{cffrk+xg~0Lo}ioZ4zut;wcN?xqK3C75kO|Q50Pl2!JiXb_DO; zWn!Y}`kPv<^*R7kC=^beIC0_tw@{^0=_Fuo=he&rn*Zx|>)#~{q;K%^7AF7z002ov JPDHLkV1nt9@KFE& diff --git a/share/icons/application/32x32/apps/keepassxc-dark.png b/share/icons/application/32x32/apps/keepassxc-dark.png deleted file mode 100644 index eb3f274f305de397d83d96127235e45ce9a19b40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1063 zcmV+?1laqDP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY1i1hJ1i1kb;ms!i000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs000A~NklQUvD>Bsf0AYI z=NZq;&U`CLwXgZoVyTg1F@O5ctD%1L%7I{}SzgD{tOPTm{`yT)!@n8(eP<%d)zLx; zR06<&@w-|m4dhoVzD~(ExA6kczObAva+C5*5Xcn6Sx>99pqBYUUtjzKMiql^+8GqX zF?<$%*D7lW#XtHlOABW;Z@hJTf>8dE_3N8k`?(cXiI;$*I2 zudDK5S>!%WTIhBbWmEcWMV!*3Y&+YnO=zMPVOjFp_P(=n5z2q`7(|&s2F>OmgG{y^ z{_r29i~fY&HW5W^+&y&%GjSY#v+3r)`D%r~Cw>4;vejDkTds{lS=HOfPFBGs-K%g*g!;U+vxhq8%r0_6_D0D~0-vkqM?jh4L+R4|C%Q zt~hv%rgKBw0;|EtV$7B_%c25(sFk_#3=g26MhiVqr*lapT05dhizF$qUg0BCK8N=+ zH=g4z{KH5ihYwOWcV0xt)+$oKc5o3NU~W9e-IfC=zr}6T$z2og*~ zfqQHRlQd48+;t;`HL^&-D&ifrjk)m>_gW4nX(w@WcO6BPLj7VJKfw{^#!K9Xe`@An zpCWGVGmm+!v?65=;>Ig^F*c{kQRc)eoU|N>_?%1!5hIq&*KM#`d9~5XvU-d;F@jT; zgAwdzWV4kO$zu(JH<2!iX6}XLO2($eqxkLf~c% zUH0{!%pqiI;TSwr2hk0D0}rBy>KK^Xh*A5;#u~D294rlc!!qBISq~O~N5xfHo_pQg zr<2#8Xi`%7@e&qC8wE-Lt7D~9zDe8>_4yHpX-gTb;J)`II hSqpG1lIGR^=r4#`jBa1Z_M89!002ovPDHLkV1lCw=4=1} diff --git a/share/icons/application/32x32/apps/keepassxc-locked.png b/share/icons/application/32x32/apps/keepassxc-locked.png deleted file mode 100644 index cc08472c28adc9043091bfc4c285996bd8981f3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1574 zcmV+>2HE+EP)Tx)G@T?!8m z9|4&%koA2P!xua}JZ^-Bh8l*4hc^`?@@as3hlYkWL3S8)=uy{;8ovl9Xecf&e#UVe zYj0p{Ys*w#Ue4y{=W~dPiVAjnd)s8ckA)xT_{-1FuMu^g+PJ^Jf7g>IPuAfiqN2z~ z@!sB^7#9~uVq;^?z%kC*8K<`)K6>guXcSy}0D z3%$L)Y(heUh`_P^dR0{w3pqh=MM5Whe0&;Rh67`c*G^A9H#f&8CMKFPGBSkYVvVP? zza5Iq%uFFEDakZHKhM*fC@Lyi@bvUdahmOza5Uz4a!??~$Hx;mB{@7ilunz3xEEzm zHaR)jw7$M>qB(&!z?P4Lbx67jgTb(g8;_2TBuFkEl~6!dv!`_c_gc#q6cq5N6W0a@ z2R9)pOC3lC-#LA1Jv}{)Mx!Ct$ji&)4-O8AdlY^f={XsV40^g1Tu4u3o)rpdqkHLZ|}_ zoPxN*Hnw;ZdM|{ zC7K}!&B)}Mf3r!5(@PnMPpn@vbK9vmaqfM>|>@9&EU9OK-dIWOeEh|{JN8%QV| z)=jAd{LcdY3b$|HCKTuxg8tw0<7a2$0By_bv4^8ML7ynRVZOh=pXu!EWbfa<&mnM( zbGx0l#Ith1{9%F(B=&I1>p1blMC_}U&(ZsvGES1;wt0s_EzVroKfE60)_(J|=l?R^<^uGqM*uWzGFCVN13VgN-9 z%UCI%oS8I)<_6=SoB$JGJUKmSu#}-3(9zu7{J_V@=h{k~aNV?N)5VsSmN7y$f`a?n zLH?tfOuJL4SJ$%P`P&VEdUc)MI&khk8lPrvNoqf5qn`TA9*fZe^+ydqbHG{pFiN?;c?1ov_rtrJBOX`DGE1i`z#pm zC-k_fnSKMB{xa(HU~OkO`%X|Nl+HqB^h|<^r_YMR5KApq1Y1dN)B?fPiOIR z#%JeeaDpVBv*p+I)X)uZK|w((t7Jv8SA11f)i4MwunWe6#c%PE=fkiqvKunkd1lTi zcJx_w>8vAeJkd#BhKXBNR;Cd$=F$(+tB~OqZ)ub$`l~SP3`ynj{IE`A_vSUaA$_9c z058Vv8CD^X9UUE=hO})iEF_OBAeF*mY3Bvz&2L`AV)(VM4cR)-(tC|vj#u7w(PV=J zIzw!B(E_#s3JbmeoTgLk-tS(=V)*T^c2v-k%}&nzZB9N7cWapCc)8N0d;+#0=7{ZA z!2zF`m>9KCSQayDkE4CX+prjJ^tYqajVF+9=8^d-dPjRu+~shC7%#K#I6XNW6%_>z z_}Dpcx-l8WUfPbuaATkyWi@?Z<$x)1jVRF}$!>!)URKiy>VU!0feqw1%vVs@md(bO z?!aQWc~6ecNK@<_c>F|XNUn+Jo$=1KXPd7eBqT&dufQq@uc$9Fhb23)7;X;AQBHFz z(#>kk4onf_X|+dqF<$kp5{E(P0#AZI!#~G==Z;kF!eZ#UFGsmDYX`>v8PTWw7{`n8 z`X+i!r#vz;GQD~8=CWrc(A71zyu4gv%GZcpOs$LK-}xaNJ44q{2l}-2q?H4J>f6PP zGfrx491prpPrbBMLu~H1O2yDd(KwLzH!_J=BCs?n8a>KoV*l*1h;U!IO8EgWd?uVJ~N|$fRXj2LeBoOFHu!u2sYOb>% zw;twCHbrBKvmHk{faM&>bPio-fkSsrbd3?64Xf&mNrzMdJsgb*`a}nHe{j-12_w06 zmi@Zx6av5)=3tFJHG1dd*au0My{fj{{9ytOB=m5?>saxJ2?#>8L%<1l?)p4mNS@Hc z!P|vS2AT;-r9#etD>A?s<{-m?49JrEE_530YyCKdj7idVnV+AZ0ty)YIE69P6Ic^6 pocoLZQeG!iG9LjjerYX={sSwW{``?*Lqh-n002ovPDHLkV1l_(?9>1N diff --git a/share/icons/application/32x32/apps/keepassxc.png b/share/icons/application/32x32/apps/keepassxc.png deleted file mode 100644 index 5aff3b570ef30844bb798e5df401863fde38a8d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1591 zcmV-72FUq|P)svGES1;wt0s_EzVroKfE60)_(J|=l?R^<^uGqM*uWzGFCVN13VgN-9 z%UCI%oS8I)<_6=SoB$JGJUKmSu#}-3(9zu7{J_V@=h{k~aNV?N)5VsSmN7y$f`a?n zLH?tfOuJL4SJ$%P`P&VEdUc)MI&khk8lPrvNoqf5qn`TA9*fZe^+ydqbHG{pFiN?;c?1ov_rtrJBOX`DGE1i`z#pm zC-k_fnSKMB{xa(HU~OkO`%X|Nl+HqB^h|<^r_YMR5KApq1Y1dN)B?fPiOIR z#%JeeaDpVBv*p+I)X)uZK|w((t7Jv8SA11f)i4MwunWe6#c%PE=fkiqvKunkd1lTi zcJx_w>8vAeJkd#BhKXBNR;Cd$=F$(+tB~OqZ)ub$`l~SP3`ynj{IE`A_vSUaA$_9c z058Vv8CD^X9UUE=hO})iEF_OBAeF*mY3Bvz&2L`AV)(VM4cR)-(tC|vj#u7w(PV=J zIzw!B(E_#s3JbmeoTgLk-tS(=V)*T^c2v-k%}&nzZB9N7cWapCc)8N0d;+#0=7{ZA z!2zF`m>9KCSQayDkE4CX+prjJ^tYqajVF+9=8^d-dPjRu+~shC7%#K#I6XNW6%_>z z_}Dpcx-l8WUfPbuaATkyWi@?Z<$x)1jVRF}$!>!)URKiy>VU!0feqw1%vVs@md(bO z?!aQWc~6ecNK@<_c>F|XNUn+Jo$=1KXPd7eBqT&dufQq@uc$9Fhb23)7;X;AQBHFz z(#>kk4onf_X|+dqF<$kp5{E(P0#AZI!#~G==Z;kF!eZ#UFGsmDYX`>v8PTWw7{`n8 z`X+i!r#vz;GQD~8=CWrc(A71zyu4gv%GZcpOs$LK-}xaNJ44q{2l}-2q?H4J>f6PP zGfrx491prpPrbBMLu~H1O2yDd(KwLzH!_J=BCs?n8a>KoV*l*1h;U!IO8EgWd?uVJ~N|$fRXj2LeBoOFHu!u2sYOb>% zw;twCHbrBKvmHk{faM&>bPio-fkSsrbd3?64Xf&mNrzMdJsgb*`a}nHe{j-12_w06 zmi@Zx6av5)=3tFJHG1dd*au0My{fj{{9ytOB=m5?>saxJ2?#>8L%<1l?)p4mNS@Hc z!P|vS2AT;-r9#etD>A?s<{-m?49JrEE_530YyCKdj7idVnV+AZ0ty)YIE69P6Ic^6 pocoLZQeG!iG9LjjerYX={sSwW{``?*Lqh-n002ovPDHLkV1l_(?9>1N diff --git a/share/icons/application/32x32/apps/preferences-desktop-icons.png b/share/icons/application/32x32/apps/preferences-desktop-icons.png deleted file mode 100644 index 3965468a5cd86244bf7ebc8b7b23f6aecf0cdc69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2264 zcmV;}2q*W6P)qE6#oz&FS6jM`EbM)rTn>*9e z(pJaB#5f)}aNrMf=g$2lJ@*pX56NC2dyZ@(*@P#UwV6Hhvc7on;@f?FebDyy3hSls zY<)T%bY%Vg{TLV=WV_!#Fkqs4{XIQBny#)cWoKt+d3$?%W^;4%`G$suFoiMuhQi@*bTXozUFTh~G?jNUz7x&=9}VN*FfZM&QNT+S&p&d2=qc z41rf#@Kv=2rz(krq6vi+Dv^fa;o&D5lSr4RLzSk2woHqrj3%_zwxO3&X)qW-nrbJE z$xj1k!Prz)Rn?W0lpyLt0kZ2UtUbNLOE#0qB!Ji}l~RTB%yQIJ)(B4}5(SqF(2=Ks z;idtDr9)6(QKPk`RlHBmsmuvjkAgnGeEIU1Vq;_9BDEhUA&qdjMA*ofxj8ufkKD){ zBrf?Hw|CYf%Pj-)KYtCX7V&>`V+)$Dx4>9!f>B{YUv@uQDqBS4Xnh6Y{alDGJUsk2 zg^r<%@6ZJlGl=TcXuN( zSdRL4n=r830R5Ln4DL5T?cRZC*H{!57785{T`opnqmCjl40T>B$WgIYYHI3v(Lbm* zzmUMWj^mnURr!y1F`1 ze$u8z>@N6)^t4+A0_fzT5Xa5$;N z${Ib~R3^BRcw9>R@$SwPBqt|7vEErDk&%%I2nY}nVNWfeDK0J+x-uu_;^Oj-*fSO{ zUTl+;l$5~AKYH{iR_wcuRkcK*^*;Wj8pb=B-PjWFO-{#SRP!Pgl8~9e(Kpsg!_Am zfVI87{p8BZ$_DdBGhSnG1{@r1X41AzJF6&Neung5B;4Og1gt0mKaP%$4zp}FV>Jfm z$|zUdAmx{cytDY&YHF%PblFLbM$=EQZ<-G#&$+w1FJ*Cz7o5XeRGNlxnG*^XUdSq5 zkAFtGp!8;mXg*qP53+NvO51cNVZLEbV1lEg<8NuG7#wfd3knKwD%KIowvFiS--#}r z4{oVeAu@iw=mQ*uq?BkhHa7BR)l8VLnG>*{IdkT#l&2EQ#W|O{(GkqDnMlThi;k8d zSK&x$*i2zQOcsnT?K;G!ID^Vu?3~xLuA#cRngv-!n6FyShA*BtaUwuU1AA3_y9UQD zeuhJ-ZV3AKYDA>%2Q{rHijG=3ZdASpqhT*bjFA{NoTU4oAueSF&YnFhS~)iI4tXZ>G+1c66 znmTpruf+)V-;>i*Pe#_ZwzjX+Nk%OV6TA_vTenW^3xR=wB9G=3K;GWZ&(H6rt+9!mMnRhau@ZqS)T-VpK{qB8X8*R=H_;4 z&YU?LiL?VZG6Mg_vSrKoj5uj5XF$vAc~@6gXUe6K0l6%xB=wX>rI(l2Ir47v^y$-` zc?X?6d-iKQ#+!w8T!4?IffoOXv$OLWZ*T9gMT-{gq8z?Yr(j#2o_K)a8N%$e`+vD6 mIypK0h(pSyIp#c$;rb6nBfMO?z%3xJy*`k_gU;S6%azF7AeA+ zhih^}uh)%$bfDdCL8sl8z!ES;$w3uxV*gu_ zAHQyI!{Xu-sO|5=$4?(%XQzy8d*FI*9Au$Ol5sMi7HJkx2W@U{!rIy@6bc1+w)$Lx z%OElX5Mmd>5V(C~z;k`LGdl}&59i?E-~e8}deJuk#Sz%6j7TWZ?RKQxzk3hfynPK@ zTU*lPmSv+g&T2KF*=&NqXg9ND*r6%lhisNto`xn5)+1*|;QhOG?Zx>=^U^iL24JQO zMCjBs-%{i+EIh{Q7}t8r0y6-Qr2u_UvV2Q`jb0-vyQ)kx>mPa8S{e!?st>7v(QnA~ zi&8$RfO&AT0*VYJPs9TKii=P{hbVb03vg1LSge!CRPw}PfY<*db4Y%mj)0wN0mpIl zZ@&7r^Nh#{z+nbGPi+U|bsIITxVM*%xn)p4_B1EuK zsr(>JNgRUy{0ho#lyh+U{}0LHb(AkCzZ4ib*IQ_zT7h VI*&v+s0#o9002ovPDHLkV1n7@#8UtO diff --git a/share/icons/application/32x32/categories/preferences-other.png b/share/icons/application/32x32/categories/preferences-other.png deleted file mode 100644 index 24c03a12949d00413ed4f0b3b1e74063653144b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1997 zcmV;;2Qv7HP)dTY*Z_;LP!=e#py9Q^u1QdX$a8qe`z23#uP%xbVfUV% zv$HtV$E2pqo%v?({QvpyclO+KzH{yyZ>-P7LcEFCgxHGMf_O{4zV6osb8~auw6wJ3 zn>TOHcXxL&eSN*lot>S-rKP1VYRA^?V(s3&`{y}1IfHla-i6uO8IZ|jFfW@|{SFTg zDfwNU|?WWR$ivOH+c^pK70s^j~Ah@ zzwdk8+e^_|&(PELmZPKNpR2Eb3vF#}Fo#i~NQ_U6!_lKh53C_)sb7JXrX#W0)6+Aj zvZ?}_o10;5Y!pUDhGB4M5DE(NpSZibAEk1PjEdxBWC*&wy*+)^rKz!N`xgmcOjlp8 zrfi3X#(L=L>Vi8R?LbjUPWf`VqEb9}>((tuOiWN%SXj8L^FcGU&t|hN^9%AAL52W| zt`tI0NDyGjL4AE4d{=W5va&Lvrltl&A`$rc``*Vh#+uBD&EetUC3o($1J~IZB-bjS zLQ)1+2d&`R*HRE>X8|vXN9737G0{pB6O$M`qoc9p^YZdKg`rg}EiFNEax%EMxf(jb+yU?_fw}J zHZBh05TAYK&73}c`l*u>cYg2Qy?i`lLgzHmY&Lp$c!ajLwkjhc{th-r*bp2Nj0u(q zK|w)s4u?~Rgz;!c`hO5%gwRuyv;JRl?Xzdk#$vrQzP>(SW5Wj20Kgu=*xA`#!~IVX z@2Ls5s!27Ha74Z-Fffqc-rf$Lo_~YGAF|=2kA8=>UI1J!m&iX*x8DtFdyR<=Yy0-? zKZ}lzE^2CO0#~;`0^7zKu%00|Hy2Q7YKS~4>Yuw-iEC?{rwOZ0R91ec_oM3^bzUbS zA>rEX+qW^hxxhYb4W=KMLPkag`1$#DqK!pLN@k2qw!jF5q6mrPy3yF!l#)RAv_^XZ zL2l%0lbf2FzLS}m*+^M#XU_p^_HW_W?;AsWd^}vZaA87AOX~=Q-PzgI0!iS(18hW# zOR&7GP=W4gjrLqz{&e8A$gQode_2>q*ilhY0d@{{aLD=~>^6E2LPA1-$K%QM_4OSE zg8VPP`l>2WBr4+(2@$rnbzyXJRcLSTR;|$<*_4z>19EeV|M7$5$BrHQO;uIZP*G74 z*m91;!9!MHXt)bdcu-oio){Y&|5Z;~a2l%yL><4v-Dm(wSG0zlm-2 z_t;Y9>FMe4sokewX>|bJHFyV(A3qKyB_#@sOcL4|u9h19lT95RgV5VMfQcL3K4Xs@*N7Le`Qk&JcX#ECtqZi~Ue!3}0+W@)H)(+Ds!GJ@jb zVn|6z8P(R-KBAV`7v39>CkQJjkSL_m1{fN;2NM%BOixe0_{NPJW|+tadVBjTXl-C% z0;JM<$j`4*(3xi~H8nLg6#pejra2`S61$d{mor3;RWPrst81@rDy)~hTP+?>__(l8 z%813+na0L0bYK?l-+#nRPR;>=?#IVxprN4)%FC-6kx0rUBxF5aEyGP0FJ6q1N~H_i zwr%?bIfN1USR#=qQGgZ<4GrDY)5|NnnXT;!J1eWhCS0zoeN)pNW@u;}zXzu9%Sfie z*w`e{8tsvdm6eqVop~04ef#!(h~4ZdMxh%Ai`1)EuTBXBf(0C%&Zt}J|I|wP{0#fS z!BMEKt$SKmSGOXU%YlIXV};fR21ZaP(j8vbVt3fv+w*anS;Y6fg2UwKo;`brT>o|Y zxR=-GPKAY67I09Zc|fb7q2URE?rBXVDq0|$mx>SKT?EbU^uDQK=>Etd;@JlXpU?l6 fuqJn2c;0^i28d?_tLSbi00000NkvXXu0mjf+~3Zp diff --git a/share/icons/application/32x32/mimetypes/application-x-keepassxc.png b/share/icons/application/32x32/mimetypes/application-x-keepassxc.png deleted file mode 100644 index d7cf40a2875c38084651cea952d81c070905bda9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1370 zcmV-g1*Q6lP)^EDB1o5R8Tp!GYY?43ve-s2dBHt^@r5*R~W`xfP_8Tfd&)8FZA zUJ$P|H8m~cgqNS6pM+_>;0fT>hK2_FNdx%$`ot1i=Mf(tkLKoP zF5qyiy2KK%e~fb#XAo8OE*xtPx9{8A+aVTt%ClV7AVYl0l20*dQ$;sh|{W*ApKke1-JeQa!yV+mC z_ubH3;qOaJOHox-B^<~m(-{A*eP<#OUs@h6Bg@wUpvk3#}>gqyfW+p$F+)Rh=SlQp%Zn>}{Ueil-(KE1f^>+H?N5H}TkJfljC6p%er_4TfrbXhys1V6yb4 zaNK1{xdR|m#(3P#T!Dg1~3jNRE2;PQhXTmVX4HO^P~@7mX*S72#r zi8nAo08*(GCX)#obJw8&C?~#a67AY|e^|*AIUylI7(jl0J~SE)thOm0%Yy*^pMX0y z`dE*Vkr5tJr_*_O0P6Jx1qJ-@3}w`T0D4Ow&XxP)^YSw;OZekMtgf!2y1E()g#zpA z>)6`bIvl{_;v(2!f|ZpOo=AHEP`@CN%;q?JCO?g{m!F!dzmDG{At50c9315L1YK%A zK0e{wlh^*8P%f9FrshdoE$PT~1E6?QgCcQiHnN+s_@vMeA6;VhiyyvM#KCAXqPDh{ zyMJtK3`V07Qx?l6(|MiQGY0}t_oD1I7!0iYt)Q?y{b`M}jK4il$31lCVFNxbIE}Ev z_wc1W2zl2t(Em^awOWnr>}=i#sG_FX#l({F;$H6ecpDB4u{JCcVRUrVbs>GlV#^^| z%!}J)I9uY6_jBHWQmw%3f|ZAl+)YnCGd&H{_&BtEea;xM_*>JrpFkfY(Q0%o&`Ly}i)3}V`1fu22mPj@%sB$D)l#P^+usFtZT z!^6W-UtiC}%sfP7)G=uyqEU^m-W~D5ZSgN+W2J`?Kh#V}iA0G-6B6Y|P*4!BmYA3r z4zR?4xH$6~iN^{M7d#bHW1-rnsfofwUZO-ZY>P`Ik_;i@#{!^Cr^GX`0kuc1c1A`< zN`;Ca34oH0N{rg}JOkJkPd|#dU;t{XlysAmlf2|c9zAl3k0H(h8XFr|4*T>pv{Dri zz@*viWc}jnViiUt^fc407*qoM6N<$f+rAv{{R30 diff --git a/share/icons/application/32x32/status/security-high.png b/share/icons/application/32x32/status/security-high.png deleted file mode 100644 index 01f7fcc467159b4a72a10fa2f1945638784b6b75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1625 zcmV-f2B!ImP)R96(nA6ublLX;&Gg2oa=B~99b#uy9QbV=J( z+uE8&QIEp->m*>$&q@ z329OsE%h&N@=M;lcklVmJ?GqIIPOJz_NGS|2whXkpL8Bg2}8hK4Jw4`DJ{ z(O^c5*{sD9aeVtl)qS;k5CcO45bYOXPU0NQmCn^Z-@AVg0|Nt4HY@LkhJ_}uF~m#H z>WG0b3o9QVpY;_-%6o6z?A6@4dmC-NZJ3un4_rJ4Zi_%12d7LYwDdHiukR)ZFy>T5qH?CrnJQSvprr=UYw2&wx=j(AJ96ibhR-{*GV${r$4Y=Bv7-#)x>Uyj-sC=xj$<_fKf)XhvXB z04&1H!G)7hnjpx;j}tZHoJb`$b1>g%4pELbj@DM9wyp+M)s>LRwhlAfg2ul#mb3SF z7UqqdI$aO9&2BLEG6omG@#0KiCs9IF6Q_uiL=|z6$QSyszBI8 z9x&U2#)J`Y-6G5Q{aEc$Oj$JroG%&sjz}W55qpRuL_KkaP!K1G3J!YT>%ltL8e9VX z4&$_AO}{inRE!ww{6uP9B49|o5tkU<&m6GvwE-7Mh9ttw*bGaBurpz$fPznnv_*;_;2~;H3N*?kf)OguIP#aTt~?T~f_#>#`K5*xSz%%gakN z$;F9UBdoHcjz?())DP8IRJsV#Iw(5^DAMmkS-cC zJ!m>4F%j_eT6>plx;?1?$Jc?4x3{O_L_-ahXD$O5%4x%Do@ox%EtOVq=+V%acurFe z^vd=4;w%(a42^M#mK5#3#nW-D6ch*;=S;_{XPV+QNkk zcGlEZK~}X5hGIkh0Fn{=DH2;PVX%iH(@4LM5eI~C)Q9!`HaiHBQ4w%)dGiNm>sd}k zR*df@O5=Vx)6|HS1uOZ@MJfmt@s4QOCG3(({HFC`)(1#VO@gIe%m7XP#F1({}4k->u$(2GJ8Ln}1nw!IH{Flp#c5C)u zzfRS3UV+0+WtfpNgLfj154V7U{+5$jD5?iZ4Fxq_NIV+In?b5hf-ko^c7W z_f+kzC{xPeB6H!3QX)Xl^JVj~qcRV>cNfCXZ{47gk_XA~O6>bMtB8G{#~wZ^`=X>TNf}E-^np?~lvf zUFf-X71yp`! XHsgk=(S36^00000NkvXXu0mjfdTjR3 diff --git a/share/icons/application/48x48/apps/keepassxc-dark.png b/share/icons/application/48x48/apps/keepassxc-dark.png deleted file mode 100644 index 81cdcfa19fa1310f908a108e8618993c165aa17c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1661 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 M07*qoM6N<$g8BM}KL7v# diff --git a/share/icons/application/64x64/apps/keepassxc-locked.png b/share/icons/application/64x64/apps/keepassxc-locked.png deleted file mode 100644 index c6e7e239cd15f1a4a73d23e6a5889805f539688b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3321 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 D+aNFX diff --git a/share/icons/application/64x64/apps/keepassxc-unlocked.png b/share/icons/application/64x64/apps/keepassxc-unlocked.png deleted file mode 100644 index 3e1d4e5cecb4f3662fb80b25f2ce0691d9516ff3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3458 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 diff --git a/share/icons/application/64x64/apps/keepassxc.png b/share/icons/application/64x64/apps/keepassxc.png deleted file mode 100644 index 3e1d4e5cecb4f3662fb80b25f2ce0691d9516ff3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3458 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 diff --git a/share/icons/application/64x64/mimetypes/application-x-keepassxc.png b/share/icons/application/64x64/mimetypes/application-x-keepassxc.png deleted file mode 100644 index f26e140f9f7ba7a59186272145e7e10f3745139d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3122 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 M07*qoM6N<$f=_hXivR!s diff --git a/share/icons/application/scalable/actions/application-exit.svg b/share/icons/application/scalable/actions/application-exit.svg new file mode 100644 index 0000000000..aea8b16ad2 --- /dev/null +++ b/share/icons/application/scalable/actions/application-exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/auto-type.svg b/share/icons/application/scalable/actions/auto-type.svg new file mode 100644 index 0000000000..05126f2a36 --- /dev/null +++ b/share/icons/application/scalable/actions/auto-type.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/bugreport.svg b/share/icons/application/scalable/actions/bugreport.svg new file mode 100644 index 0000000000..0f21ca6020 --- /dev/null +++ b/share/icons/application/scalable/actions/bugreport.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/chronometer.svg b/share/icons/application/scalable/actions/chronometer.svg new file mode 100644 index 0000000000..3a6eca3d92 --- /dev/null +++ b/share/icons/application/scalable/actions/chronometer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/configure.svg b/share/icons/application/scalable/actions/configure.svg new file mode 100644 index 0000000000..6ab8c5e7c7 --- /dev/null +++ b/share/icons/application/scalable/actions/configure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/database-change-key.svg b/share/icons/application/scalable/actions/database-change-key.svg new file mode 100644 index 0000000000..7feeb28575 --- /dev/null +++ b/share/icons/application/scalable/actions/database-change-key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/database-lock.svg b/share/icons/application/scalable/actions/database-lock.svg new file mode 100644 index 0000000000..1c1c86e8db --- /dev/null +++ b/share/icons/application/scalable/actions/database-lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/database-merge.svg b/share/icons/application/scalable/actions/database-merge.svg new file mode 100644 index 0000000000..f7ade0459c --- /dev/null +++ b/share/icons/application/scalable/actions/database-merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/dialog-close.svg b/share/icons/application/scalable/actions/dialog-close.svg new file mode 100644 index 0000000000..7f72898b49 --- /dev/null +++ b/share/icons/application/scalable/actions/dialog-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/dialog-ok.svg b/share/icons/application/scalable/actions/dialog-ok.svg new file mode 100644 index 0000000000..0f68c682c7 --- /dev/null +++ b/share/icons/application/scalable/actions/dialog-ok.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-close.svg b/share/icons/application/scalable/actions/document-close.svg new file mode 100644 index 0000000000..7f72898b49 --- /dev/null +++ b/share/icons/application/scalable/actions/document-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-edit.svg b/share/icons/application/scalable/actions/document-edit.svg new file mode 100644 index 0000000000..72f075632c --- /dev/null +++ b/share/icons/application/scalable/actions/document-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-new.svg b/share/icons/application/scalable/actions/document-new.svg new file mode 100644 index 0000000000..a721fff66d --- /dev/null +++ b/share/icons/application/scalable/actions/document-new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-open.svg b/share/icons/application/scalable/actions/document-open.svg new file mode 100644 index 0000000000..7f1d1d7156 --- /dev/null +++ b/share/icons/application/scalable/actions/document-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-properties.svg b/share/icons/application/scalable/actions/document-properties.svg new file mode 100644 index 0000000000..e1ffc95411 --- /dev/null +++ b/share/icons/application/scalable/actions/document-properties.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-save-as.svg b/share/icons/application/scalable/actions/document-save-as.svg new file mode 100644 index 0000000000..527eb5c9eb --- /dev/null +++ b/share/icons/application/scalable/actions/document-save-as.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/document-save.svg b/share/icons/application/scalable/actions/document-save.svg new file mode 100644 index 0000000000..fb996b4374 --- /dev/null +++ b/share/icons/application/scalable/actions/document-save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/donate.svg b/share/icons/application/scalable/actions/donate.svg new file mode 100644 index 0000000000..9231a09e53 --- /dev/null +++ b/share/icons/application/scalable/actions/donate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg b/share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg new file mode 100644 index 0000000000..b240239dc0 --- /dev/null +++ b/share/icons/application/scalable/actions/edit-clear-locationbar-ltr.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg b/share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg new file mode 100644 index 0000000000..9822377906 --- /dev/null +++ b/share/icons/application/scalable/actions/edit-clear-locationbar-rtl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-clone.svg b/share/icons/application/scalable/actions/entry-clone.svg new file mode 100644 index 0000000000..1886d76211 --- /dev/null +++ b/share/icons/application/scalable/actions/entry-clone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-delete.svg b/share/icons/application/scalable/actions/entry-delete.svg new file mode 100644 index 0000000000..1e42b6dafc --- /dev/null +++ b/share/icons/application/scalable/actions/entry-delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-edit.svg b/share/icons/application/scalable/actions/entry-edit.svg new file mode 100644 index 0000000000..82b72366f4 --- /dev/null +++ b/share/icons/application/scalable/actions/entry-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-new.svg b/share/icons/application/scalable/actions/entry-new.svg new file mode 100644 index 0000000000..d04166069d --- /dev/null +++ b/share/icons/application/scalable/actions/entry-new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/favicon-download.svg b/share/icons/application/scalable/actions/favicon-download.svg new file mode 100644 index 0000000000..a5f21d0c58 --- /dev/null +++ b/share/icons/application/scalable/actions/favicon-download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/getting-started.svg b/share/icons/application/scalable/actions/getting-started.svg new file mode 100644 index 0000000000..3d62971d2c --- /dev/null +++ b/share/icons/application/scalable/actions/getting-started.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/group-delete.svg b/share/icons/application/scalable/actions/group-delete.svg new file mode 100644 index 0000000000..47cd85aa31 --- /dev/null +++ b/share/icons/application/scalable/actions/group-delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/group-edit.svg b/share/icons/application/scalable/actions/group-edit.svg new file mode 100644 index 0000000000..82005ed29f --- /dev/null +++ b/share/icons/application/scalable/actions/group-edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/group-empty-trash.svg b/share/icons/application/scalable/actions/group-empty-trash.svg new file mode 100644 index 0000000000..10f79c1039 --- /dev/null +++ b/share/icons/application/scalable/actions/group-empty-trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/group-new.svg b/share/icons/application/scalable/actions/group-new.svg new file mode 100644 index 0000000000..9b35f56161 --- /dev/null +++ b/share/icons/application/scalable/actions/group-new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/help-about.svg b/share/icons/application/scalable/actions/help-about.svg new file mode 100644 index 0000000000..74ebf8c88e --- /dev/null +++ b/share/icons/application/scalable/actions/help-about.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/key-enter.svg b/share/icons/application/scalable/actions/key-enter.svg new file mode 100644 index 0000000000..05126f2a36 --- /dev/null +++ b/share/icons/application/scalable/actions/key-enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/keyboard-shortcuts.svg b/share/icons/application/scalable/actions/keyboard-shortcuts.svg new file mode 100644 index 0000000000..bee8fddccb --- /dev/null +++ b/share/icons/application/scalable/actions/keyboard-shortcuts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/message-close.svg b/share/icons/application/scalable/actions/message-close.svg new file mode 100644 index 0000000000..7f72898b49 --- /dev/null +++ b/share/icons/application/scalable/actions/message-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/object-locked.svg b/share/icons/application/scalable/actions/object-locked.svg index 090e038c0b..1c1c86e8db 100644 --- a/share/icons/application/scalable/actions/object-locked.svg +++ b/share/icons/application/scalable/actions/object-locked.svg @@ -1,14 +1 @@ - - - - - - + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/object-unlocked.svg b/share/icons/application/scalable/actions/object-unlocked.svg index f6c53e5816..2925abfebd 100644 --- a/share/icons/application/scalable/actions/object-unlocked.svg +++ b/share/icons/application/scalable/actions/object-unlocked.svg @@ -1,15 +1 @@ - - - - - - + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/paperclip.svg b/share/icons/application/scalable/actions/paperclip.svg new file mode 100644 index 0000000000..b201d48bc2 --- /dev/null +++ b/share/icons/application/scalable/actions/paperclip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/password-copy.svg b/share/icons/application/scalable/actions/password-copy.svg new file mode 100644 index 0000000000..778d19fdfd --- /dev/null +++ b/share/icons/application/scalable/actions/password-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/password-generate.svg b/share/icons/application/scalable/actions/password-generate.svg new file mode 100644 index 0000000000..7192714859 --- /dev/null +++ b/share/icons/application/scalable/actions/password-generate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/password-generator.svg b/share/icons/application/scalable/actions/password-generator.svg new file mode 100644 index 0000000000..7192714859 --- /dev/null +++ b/share/icons/application/scalable/actions/password-generator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/password-show-off.svg b/share/icons/application/scalable/actions/password-show-off.svg new file mode 100644 index 0000000000..ac890b0931 --- /dev/null +++ b/share/icons/application/scalable/actions/password-show-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/password-show-on.svg b/share/icons/application/scalable/actions/password-show-on.svg new file mode 100644 index 0000000000..923a35af64 --- /dev/null +++ b/share/icons/application/scalable/actions/password-show-on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/sort-alphabetical-ascending.svg b/share/icons/application/scalable/actions/sort-alphabetical-ascending.svg new file mode 100644 index 0000000000..50d98a418a --- /dev/null +++ b/share/icons/application/scalable/actions/sort-alphabetical-ascending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/sort-alphabetical-descending.svg b/share/icons/application/scalable/actions/sort-alphabetical-descending.svg new file mode 100644 index 0000000000..602ea069d7 --- /dev/null +++ b/share/icons/application/scalable/actions/sort-alphabetical-descending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/statistics.svg b/share/icons/application/scalable/actions/statistics.svg new file mode 100644 index 0000000000..caf1402098 --- /dev/null +++ b/share/icons/application/scalable/actions/statistics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/system-help.svg b/share/icons/application/scalable/actions/system-help.svg new file mode 100644 index 0000000000..6fe00db35c --- /dev/null +++ b/share/icons/application/scalable/actions/system-help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/system-search.svg b/share/icons/application/scalable/actions/system-search.svg new file mode 100644 index 0000000000..8532cdce09 --- /dev/null +++ b/share/icons/application/scalable/actions/system-search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/system-software-update.svg b/share/icons/application/scalable/actions/system-software-update.svg new file mode 100644 index 0000000000..8811378699 --- /dev/null +++ b/share/icons/application/scalable/actions/system-software-update.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/url-copy.svg b/share/icons/application/scalable/actions/url-copy.svg new file mode 100644 index 0000000000..1d96b3d3ac --- /dev/null +++ b/share/icons/application/scalable/actions/url-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/user-guide.svg b/share/icons/application/scalable/actions/user-guide.svg new file mode 100644 index 0000000000..8bad778981 --- /dev/null +++ b/share/icons/application/scalable/actions/user-guide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/username-copy.svg b/share/icons/application/scalable/actions/username-copy.svg new file mode 100644 index 0000000000..b19fe07a60 --- /dev/null +++ b/share/icons/application/scalable/actions/username-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/view-history.svg b/share/icons/application/scalable/actions/view-history.svg new file mode 100644 index 0000000000..47e3ae9ade --- /dev/null +++ b/share/icons/application/scalable/actions/view-history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/web.svg b/share/icons/application/scalable/actions/web.svg new file mode 100644 index 0000000000..93043316c6 --- /dev/null +++ b/share/icons/application/scalable/actions/web.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/apps/freedesktop.svg b/share/icons/application/scalable/apps/freedesktop.svg index 455a0b3a52..19cda86784 100644 --- a/share/icons/application/scalable/apps/freedesktop.svg +++ b/share/icons/application/scalable/apps/freedesktop.svg @@ -57,7 +57,7 @@ + style="fill:#ffffff;fill-rule:nonzero;stroke:#000000;stroke-width:2.45880008;stroke-miterlimit:4"> \ No newline at end of file diff --git a/share/icons/application/scalable/apps/preferences-desktop-icons.svg b/share/icons/application/scalable/apps/preferences-desktop-icons.svg new file mode 100644 index 0000000000..98ccc57d41 --- /dev/null +++ b/share/icons/application/scalable/apps/preferences-desktop-icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/apps/preferences-system-network-sharing.svg b/share/icons/application/scalable/apps/preferences-system-network-sharing.svg new file mode 100644 index 0000000000..d9dbbc2a66 --- /dev/null +++ b/share/icons/application/scalable/apps/preferences-system-network-sharing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/apps/utilities-terminal.svg b/share/icons/application/scalable/apps/utilities-terminal.svg new file mode 100644 index 0000000000..c95f81788f --- /dev/null +++ b/share/icons/application/scalable/apps/utilities-terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/categories/preferences-other.svg b/share/icons/application/scalable/categories/preferences-other.svg new file mode 100644 index 0000000000..16cc434e91 --- /dev/null +++ b/share/icons/application/scalable/categories/preferences-other.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/status/dialog-error.svg b/share/icons/application/scalable/status/dialog-error.svg new file mode 100644 index 0000000000..4cec1e30d4 --- /dev/null +++ b/share/icons/application/scalable/status/dialog-error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/status/dialog-information.svg b/share/icons/application/scalable/status/dialog-information.svg new file mode 100644 index 0000000000..74ebf8c88e --- /dev/null +++ b/share/icons/application/scalable/status/dialog-information.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/status/dialog-warning.svg b/share/icons/application/scalable/status/dialog-warning.svg new file mode 100644 index 0000000000..cf0f8c0746 --- /dev/null +++ b/share/icons/application/scalable/status/dialog-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/status/security-high.svg b/share/icons/application/scalable/status/security-high.svg new file mode 100644 index 0000000000..ec348fd624 --- /dev/null +++ b/share/icons/application/scalable/status/security-high.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/svg/application-exit.svg b/share/icons/svg/application-exit.svg deleted file mode 100644 index 81868b89f2..0000000000 --- a/share/icons/svg/application-exit.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/application-x-keepassxc.svg b/share/icons/svg/application-x-keepassxc.svg deleted file mode 100644 index 4b33c5a69e..0000000000 --- a/share/icons/svg/application-x-keepassxc.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/auto-type.png b/share/icons/svg/auto-type.png deleted file mode 100644 index 5e3c4253512ade5b341f9516a4cdf5dac2838912..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7352 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 - diff --git a/share/icons/svg/dialog-close.svg b/share/icons/svg/dialog-close.svg deleted file mode 100644 index 9b6b717cd6..0000000000 --- a/share/icons/svg/dialog-close.svg +++ /dev/null @@ -1,238 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/dialog-error.svg b/share/icons/svg/dialog-error.svg deleted file mode 100644 index b09885d35c..0000000000 --- a/share/icons/svg/dialog-error.svg +++ /dev/null @@ -1,474 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/dialog-information.svg b/share/icons/svg/dialog-information.svg deleted file mode 100644 index 35992e0fca..0000000000 --- a/share/icons/svg/dialog-information.svg +++ /dev/null @@ -1,370 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/dialog-ok.svg b/share/icons/svg/dialog-ok.svg deleted file mode 100644 index 5ab1fad37a..0000000000 --- a/share/icons/svg/dialog-ok.svg +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/dialog-warning.svg b/share/icons/svg/dialog-warning.svg deleted file mode 100644 index 80e215b6e2..0000000000 --- a/share/icons/svg/dialog-warning.svg +++ /dev/null @@ -1,383 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/document-close.svg b/share/icons/svg/document-close.svg deleted file mode 100644 index 44b4a6bedf..0000000000 --- a/share/icons/svg/document-close.svg +++ /dev/null @@ -1,426 +0,0 @@ - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/share/icons/svg/document-edit.svg b/share/icons/svg/document-edit.svg deleted file mode 100644 index 4f462832cb..0000000000 --- a/share/icons/svg/document-edit.svg +++ /dev/null @@ -1,634 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/share/icons/svg/document-new.svg b/share/icons/svg/document-new.svg deleted file mode 100644 index 399b5236c1..0000000000 --- a/share/icons/svg/document-new.svg +++ /dev/null @@ -1,477 +0,0 @@ - - - - - - - - - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/share/icons/svg/document-open.svg b/share/icons/svg/document-open.svg deleted file mode 100644 index c48f769406..0000000000 --- a/share/icons/svg/document-open.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/document-properties.svg b/share/icons/svg/document-properties.svg deleted file mode 100644 index 59337c4aa1..0000000000 --- a/share/icons/svg/document-properties.svg +++ /dev/null @@ -1,601 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/share/icons/svg/document-save-as.svg b/share/icons/svg/document-save-as.svg deleted file mode 100644 index 833ebc6d35..0000000000 --- a/share/icons/svg/document-save-as.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/document-save.svg b/share/icons/svg/document-save.svg deleted file mode 100644 index 8e681fdb71..0000000000 --- a/share/icons/svg/document-save.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/edit-clear-locationbar-ltr.svg b/share/icons/svg/edit-clear-locationbar-ltr.svg deleted file mode 100644 index 010d954ac6..0000000000 --- a/share/icons/svg/edit-clear-locationbar-ltr.svg +++ /dev/null @@ -1,391 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/edit-clear-locationbar-rtl.svg b/share/icons/svg/edit-clear-locationbar-rtl.svg deleted file mode 100644 index d656a0b0df..0000000000 --- a/share/icons/svg/edit-clear-locationbar-rtl.svg +++ /dev/null @@ -1,380 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/internet-web-browser.svg b/share/icons/svg/internet-web-browser.svg deleted file mode 100644 index 0d00ac6dc3..0000000000 --- a/share/icons/svg/internet-web-browser.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/share/icons/svg/key-enter.svg b/share/icons/svg/key-enter.svg deleted file mode 100644 index 7c983be54a..0000000000 --- a/share/icons/svg/key-enter.svg +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/message-close.svg b/share/icons/svg/message-close.svg deleted file mode 100644 index a36700faf7..0000000000 --- a/share/icons/svg/message-close.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - diff --git a/share/icons/svg/paperclip.svg b/share/icons/svg/paperclip.svg deleted file mode 100644 index ad1b8d6167..0000000000 --- a/share/icons/svg/paperclip.svg +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/share/icons/svg/password-copy.svg b/share/icons/svg/password-copy.svg deleted file mode 100644 index 8d7e33c02d..0000000000 --- a/share/icons/svg/password-copy.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/password-generator.svg b/share/icons/svg/password-generator.svg deleted file mode 100644 index 440d690a01..0000000000 --- a/share/icons/svg/password-generator.svg +++ /dev/null @@ -1,568 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - Oxygen team - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/preferences-desktop-icons.svg b/share/icons/svg/preferences-desktop-icons.svg deleted file mode 100644 index 3d2fd2006f..0000000000 --- a/share/icons/svg/preferences-desktop-icons.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/share/icons/svg/preferences-other.svg b/share/icons/svg/preferences-other.svg deleted file mode 100644 index 41b0e60546..0000000000 --- a/share/icons/svg/preferences-other.svg +++ /dev/null @@ -1,1012 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/security-high.svg b/share/icons/svg/security-high.svg deleted file mode 100644 index d5c23d1e83..0000000000 --- a/share/icons/svg/security-high.svg +++ /dev/null @@ -1,380 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/system-search.svg b/share/icons/svg/system-search.svg deleted file mode 100644 index 7a4bcbb49e..0000000000 --- a/share/icons/svg/system-search.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/url-copy.svg b/share/icons/svg/url-copy.svg deleted file mode 100644 index 1d104240f9..0000000000 --- a/share/icons/svg/url-copy.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/username-copy.svg b/share/icons/svg/username-copy.svg deleted file mode 100644 index 3a6f056f26..0000000000 --- a/share/icons/svg/username-copy.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/share/icons/svg/utilities-terminal.svg b/share/icons/svg/utilities-terminal.svg deleted file mode 100644 index df601b7b40..0000000000 --- a/share/icons/svg/utilities-terminal.svg +++ /dev/null @@ -1,1517 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/share/icons/svg/view-history.svg b/share/icons/svg/view-history.svg deleted file mode 100644 index 519a4d3eef..0000000000 --- a/share/icons/svg/view-history.svg +++ /dev/null @@ -1,753 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 5b03227076..2725f4671d 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -143,7 +143,7 @@ QIcon FilePath::icon(const QString& category, const QString& name, bool fromThem return icon; } - if (fromTheme) { + if (fromTheme && !getenv("KEEPASSXC_IGNORE_ICON_THEME")) { icon = QIcon::fromTheme(name); } diff --git a/src/gui/LineEdit.cpp b/src/gui/LineEdit.cpp index 4ad18fef5c..654f9a3a7b 100644 --- a/src/gui/LineEdit.cpp +++ b/src/gui/LineEdit.cpp @@ -30,16 +30,19 @@ 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"); + + QIcon icon; + if (!getenv("KEEPASSXC_IGNORE_ICON_THEME")) { + icon = QIcon::fromTheme(iconNameDirected); if (icon.isNull()) { - icon = filePath()->icon("actions", iconNameDirected); + icon = QIcon::fromTheme("edit-clear"); } } + if (icon.isNull()) { + icon = filePath()->icon("actions", iconNameDirected); + } m_clearButton->setIcon(icon); m_clearButton->setCursor(Qt::ArrowCursor); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 6452c051bb..461f25c1ec 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -315,6 +315,7 @@ MainWindow::MainWindow() 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->actionDatabaseMerge->setIcon(filePath()->icon("actions", "database-merge")); m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); @@ -325,17 +326,26 @@ MainWindow::MainWindow() 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->actionGroupSortAsc->setIcon(filePath()->icon("actions", "sort-alphabetical-ascending")); + m_ui->actionGroupSortDesc->setIcon(filePath()->icon("actions", "sort-alphabetical-descending")); 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->actionEntryOpenUrl->setIcon(filePath()->icon("actions", "web")); 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->actionDonate->setIcon(filePath()->icon("actions", "donate")); + m_ui->actionBugReport->setIcon(filePath()->icon("actions", "bugreport")); + m_ui->actionGettingStarted->setIcon(filePath()->icon("actions", "getting-started")); + m_ui->actionUserGuide->setIcon(filePath()->icon("actions", "user-guide")); + m_ui->actionOnlineHelp->setIcon(filePath()->icon("actions", "system-help")); + m_ui->actionKeyboardShortcuts->setIcon(filePath()->icon("actions", "keyboard-shortcuts")); m_ui->actionCheckForUpdates->setIcon(filePath()->icon("actions", "system-software-update")); m_actionMultiplexer.connect( diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 068788ecb3..bd7a70f55a 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -346,8 +346,8 @@ - 22 - 22 + 32 + 32 diff --git a/utils/makeicons.sh b/utils/makeicons.sh new file mode 100644 index 0000000000..8ce4354769 --- /dev/null +++ b/utils/makeicons.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# +# Copy icon files from the Material Design icon set. +# +# Copyright (C) 2020 Wolfram Rösler +# Copyright (C) 2020 KeePassXC team +# +# 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 . +# +# How to use: (assuming you check out stuff in ~/src) +# +# 0. Make sure to have a clean working tree +# +# 1. Download the Material Design icon set: +# $ cd ~/src +# $ git clone https://github.com/Templarian/MaterialDesign.git +# +# 2. Go to the icon source directory: +# $ cd ~/src/keepassxc/share/icons +# +# 3. Create the icons: +# $ bash ../../utils/makeicons.sh ~/src/MaterialDesign +# +# 4. Re-build KeePassXC: +# $ cd ~/keepassxc/build +# $ make keepassxc +# +# 5. Check icons by disabling the OS icon theme: +# $ KEEPASSXC_IGNORE_ICON_THEME=1 src/keepassxc +# +# Material icons: https://materialdesignicons.com/ + +if [ $# != 1 ];then + echo "Usage: $0 MATERIAL" + echo "MATERIAL is the check-out directory of the material icons repository" + echo "(git clone https://github.com/Templarian/MaterialDesign.git)". + exit +fi + +MATERIAL="$1" +if [ ! -d "$MATERIAL" ];then + echo "Material check-out directory doesn't exist: $MATERIAL" + exit 1 +fi + +if [ ! -d application ];then + echo "Please run this script from within the share/icons directory" + echo "of the KeePassXC source distribution." + exit 1 +fi + +# Map KeePassXC icon names to Material icon names. +# $1 is the name of the icon file in the KeePassXC source (without +# path and without extension, e. g. "document-new"). +# Writes the name of the Material icon (without path and without +# extension, e. g. "folder-plus") to stdout. +# If the icon name is unknown, outputs nothing. +map() { + case "$1" in + application-exit) echo exit-to-app ;; + auto-type) echo keyboard-variant ;; + bugreport) echo bug-outline ;; + chronometer) echo clock-outline ;; + configure) echo settings-outline ;; + database-change-key) echo key ;; + database-close) echo close ;; + database-lock) echo lock-outline ;; + database-merge) echo merge ;; + dialog-close) echo close ;; + dialog-error) echo alert-octagon ;; + dialog-information) echo information-outline ;; + dialog-ok) echo checkbox-marked-circle ;; + dialog-warning) echo alert-outline ;; + document-close) echo close ;; + document-edit) echo pencil ;; + document-new) echo plus ;; + document-open) echo folder-open-outline ;; + document-properties) echo file-edit-outline ;; + document-save) echo content-save-outline ;; + document-save-as) echo content-save-all-outline ;; + donate) echo gift-outline ;; + edit-clear-locationbar-ltr) echo backspace-reverse-outline ;; + edit-clear-locationbar-rtl) echo backspace-outline ;; + entry-clone) echo comment-multiple-outline ;; + entry-delete) echo comment-remove-outline ;; + entry-edit) echo comment-edit-outline ;; + entry-new) echo comment-plus-outline ;; + favicon-download) echo download ;; + getting-started) echo lightbulb-on-outline ;; + group-delete) echo folder-remove-outline ;; + group-edit) echo folder-edit-outline ;; + group-empty-trash) echo trash-can-outline ;; + group-new) echo folder-plus-outline ;; + help-about) echo information-outline ;; + internet-web-browser) echo web ;; + key-enter) echo keyboard-variant ;; + keyboard-shortcuts) echo apple-keyboard-command ;; + message-close) echo close ;; + object-locked) echo lock-outline ;; + object-unlocked) echo lock-open-variant-outline ;; + paperclip) echo paperclip ;; + password-copy) echo key-arrow-right ;; + password-generate) echo dice-3-outline ;; + password-generator) echo dice-3-outline ;; + password-show-off) echo eye-off-outline ;; + password-show-on) echo eye-outline ;; + preferences-other) echo file-document-edit-outline ;; + preferences-desktop-icons) echo emoticon-happy-outline ;; + preferences-system-network-sharing) echo lan ;; + security-high) echo shield-outline ;; + sort-alphabetical-ascending) echo sort-alphabetical-ascending ;; + sort-alphabetical-descending) echo sort-alphabetical-descending ;; + statistics) echo chart-line ;; + system-help) echo help ;; + system-search) echo magnify ;; + system-software-update) echo cloud-download-outline ;; + url-copy) echo earth-arrow-right ;; + user-guide) echo book-open-outline ;; + username-copy) echo account-arrow-right-outline ;; + utilities-terminal) echo console-line ;; + view-history) echo timer-sand-empty ;; + web) echo web ;; + esac +} + +# Now do the actual work +find application -type f -name "*.svg" | while read -r DST;do + + # Find the icon name (base name without extender) + NAME=$(basename $DST .svg) + + # Find the base name of the svg file for this icon + MAT=$(map $NAME) + if [[ -z $MAT ]];then + echo "Warning: Don't know about $NAME" + continue + fi + + # So the source file is: + SRC="$MATERIAL/svg/$MAT.svg" + if [ ! -f "$SRC" ];then + echo "Error: Source for $NAME doesn't exist: $SRC" + exit 1 + fi + + # Replace the icon file with the source file + cp "$SRC" "$DST" || exit + +done From 2ca8dbebeaa3e34f9aff278177a3f4e81572bbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= Date: Fri, 27 Dec 2019 14:51:07 +0100 Subject: [PATCH 017/215] Show dark KeePassXC icon in the system tray menu for the "Toggle Window" menu item. It matches the other (Material Design) icons much better than the colored icon. Fixes #475 --- src/gui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 461f25c1ec..e4a6278912 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1087,7 +1087,7 @@ void MainWindow::updateTrayIcon() QAction* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); - actionToggle->setIcon(filePath()->icon("apps", "keepassxc")); + actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark")); menu->addAction(m_ui->actionLockDatabases); From 05ef937e92a3de5aac85d5c9436e84656a020c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= Date: Sat, 28 Dec 2019 23:51:03 +0100 Subject: [PATCH 018/215] Use Qt::AA_UseHighDpiPixmaps on all platforms ... not only on Linux, in order to prevent icons from being fuzzy. Fixes #475 --- src/main.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index dd503d9570..846baa67db 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,10 +50,8 @@ int main(int argc, char** argv) #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); -#ifdef Q_OS_LINUX QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif -#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) QGuiApplication::setDesktopFileName("org.keepassxc.KeePassXC.desktop"); From 84e3925e7b9a3b660227df7d62a04b2c44efce5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= Date: Sat, 4 Jan 2020 13:26:55 +0100 Subject: [PATCH 019/215] Remove "Create new database" from tool bar It's used extremely rarely, having it in such a prominent position in the tool bar isn't justified. Also, with the Material Design icons, its tool bar icon can easily be confused with "create new entry". Fixes #475 --- src/gui/MainWindow.ui | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index bd7a70f55a..e09c91dd79 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -356,7 +356,6 @@ false - From bf8e2e5959e746a63980a1daf4533e620d1654af Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 20 Jan 2020 21:45:17 +0100 Subject: [PATCH 020/215] Fix AppImage builds due to missing PNG app icons. All PNGs were removed in 36f92b7, including the 256x256 application icons needed for building AppImages. --- .../application/256x256/apps/keepassxc.png | Bin 0 -> 16827 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 share/icons/application/256x256/apps/keepassxc.png diff --git a/share/icons/application/256x256/apps/keepassxc.png b/share/icons/application/256x256/apps/keepassxc.png new file mode 100644 index 0000000000000000000000000000000000000000..03485c64eb21ed5a7406257ff8ba331706e56bd6 GIT binary patch literal 16827 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 literal 0 HcmV?d00001 From b34a8f9d94fff78950f70d7d941acaac0a0480ca Mon Sep 17 00:00:00 2001 From: louib Date: Sat, 4 Jan 2020 16:23:23 -0500 Subject: [PATCH 021/215] Use stderr for help text on error. Also not sure why qCritical was used instead of and stderr output stream. Added translation on the invalid command string. --- src/cli/keepassxc-cli.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index 179b79a435..85351e61db 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -189,6 +189,7 @@ int main(int argc, char** argv) Commands::setupCommands(false); TextStream out(stdout); + TextStream err(stderr); QStringList arguments; for (int i = 0; i < argc; ++i) { arguments << QString(argv[i]); @@ -223,6 +224,7 @@ int main(int argc, char** argv) out << debugInfo << endl; return EXIT_SUCCESS; } + // showHelp exits the application immediately. parser.showHelp(); } @@ -234,10 +236,9 @@ int main(int argc, char** argv) auto command = Commands::getCommand(commandName); if (!command) { - qCritical("Invalid command %s.", qPrintable(commandName)); - // showHelp exits the application immediately, so we need to set the - // exit code here. - parser.showHelp(EXIT_FAILURE); + err << QObject::tr("Invalid command %1.").arg(commandName) << endl; + err << parser.helpText(); + return EXIT_FAILURE; } // Removing the first argument (keepassxc). From b6ff61318984d56a591343271f1a3b8a1f521678 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Mon, 20 Jan 2020 22:46:50 +0100 Subject: [PATCH 022/215] Exit with error code if AppImage creation fails midair --- release-tool | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/release-tool b/release-tool index f6b8c8e275..48ca14b769 100755 --- a/release-tool +++ b/release-tool @@ -616,11 +616,9 @@ appimage() { fi if [ ! -d "${appdir}" ]; then - logError "AppDir does not exist, please create one with 'make install'!\n" - exit 1 + exitError "AppDir does not exist, please create one with 'make install'!" elif [ -e "${appdir}/AppRun" ]; then - logError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'.\n" - exit 1 + exitError "AppDir has already been run through linuxdeploy, please create a fresh AppDir with 'make install'." fi appdir="$(realpath "$appdir")" @@ -652,14 +650,18 @@ appimage() { logInfo "Downloading linuxdeploy..." linuxdeploy="./linuxdeploy" linuxdeploy_cleanup="rm -f ${linuxdeploy}" - curl -L "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy" + if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" > "$linuxdeploy"; then + exitError "linuxdeploy download failed." + fi chmod +x "$linuxdeploy" fi if ! ${docker_test_cmd} which ${linuxdeploy_plugin_qt} &> /dev/null; then logInfo "Downloading linuxdeploy-plugin-qt..." linuxdeploy_plugin_qt="./linuxdeploy-plugin-qt" linuxdeploy_plugin_qt_cleanup="rm -f ${linuxdeploy_plugin_qt}" - curl -L "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt" + if ! curl -Lf "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" > "$linuxdeploy_plugin_qt"; then + exitError "linuxdeploy-plugin-qt download failed." + fi chmod +x "$linuxdeploy_plugin_qt" fi @@ -668,7 +670,9 @@ appimage() { logInfo "Downloading appimagetool..." appimagetool="./appimagetool" appimagetool_cleanup="rm -f ${appimagetool}" - curl -L "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool" + if ! curl -Lf "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" > "$appimagetool"; then + exitError "appimagetool download failed." + fi chmod +x "$appimagetool" fi @@ -718,6 +722,10 @@ EOF --library=\$(ldconfig -p | grep x86-64 | grep -oP '/[^\s]+/libgpg-error\.so\.\d+$' | head -n1)" fi + if [ $? -ne 0 ]; then + exitError "AppDir deployment failed." + fi + logInfo "Creating AppImage..." local appsign_flag="" local appsign_key_flag="" @@ -737,8 +745,10 @@ EOF # Run appimagetool to package (and possibly sign) AppImage # --no-appstream is required, since it may crash on newer systems # see: https://github.com/AppImage/AppImageKit/issues/856 - "$appimagetool" --updateinformation "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-x86_64.AppImage.zsync" \ - ${appsign_flag} ${appsign_key_flag} --no-appstream "$appdir" "${out_real}/${appimage_name}" + if ! "$appimagetool" --updateinformation "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-x86_64.AppImage.zsync" \ + ${appsign_flag} ${appsign_key_flag} --no-appstream "$appdir" "${out_real}/${appimage_name}"; then + exitError "AppImage creation failed." + fi logInfo "Cleaning up temporary files..." ${linuxdeploy_cleanup} @@ -906,7 +916,7 @@ build() { if ! ${build_snapshot} && [ -e "${OUTPUT_DIR}/build-release" ]; then logInfo "Cleaning existing build directory..." - rm -r "${OUTPUT_DIR}/build-release" 2> /dev/null + rm -rf "${OUTPUT_DIR}/build-release" 2> /dev/null if [ $? -ne 0 ]; then exitError "Failed to clean existing build directory, please do it manually." fi From 796b5ceacb9f87f812c3ec9e3beed370e24edff0 Mon Sep 17 00:00:00 2001 From: Andrey Izman Date: Wed, 22 Jan 2020 20:24:06 +0200 Subject: [PATCH 023/215] Fix closing tag typo --- src/format/HtmlExporter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/format/HtmlExporter.cpp b/src/format/HtmlExporter.cpp index 152f2933af..457623ec98 100644 --- a/src/format/HtmlExporter.cpp +++ b/src/format/HtmlExporter.cpp @@ -176,7 +176,7 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat if (!u.isEmpty()) { item.append(""); item.append(QObject::tr("User name")); - item.append(""); + item.append(""); item.append(entry->username().toHtmlEscaped()); item.append(""); } From 6ff3e8801d8efbc5fca7b9f4bf2e3785ea4ee6ef Mon Sep 17 00:00:00 2001 From: Kjell Braden Date: Tue, 21 Jan 2020 11:32:47 +0100 Subject: [PATCH 024/215] retrieve login1 session object from manager (#3339) --- src/core/ScreenLockListenerDBus.cpp | 38 ++++++++++++++++++++++++----- src/core/ScreenLockListenerDBus.h | 2 ++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/core/ScreenLockListenerDBus.cpp b/src/core/ScreenLockListenerDBus.cpp index 5c57861bda..66970aee39 100644 --- a/src/core/ScreenLockListenerDBus.cpp +++ b/src/core/ScreenLockListenerDBus.cpp @@ -19,7 +19,9 @@ #include #include +#include #include +#include #include ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget* parent) @@ -57,12 +59,14 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget* parent) SLOT(logindPrepareForSleep(bool))); QString sessionId = QProcessEnvironment::systemEnvironment().value("XDG_SESSION_ID"); - systemBus.connect("", // service - QString("/org/freedesktop/login1/session/") + sessionId, // path - "org.freedesktop.login1.Session", // interface - "Lock", // signal name - this, // receiver - SLOT(unityLocked())); + QDBusInterface loginManager("org.freedesktop.login1", // service + "/org/freedesktop/login1", // path + "org.freedesktop.login1.Manager", // interface + systemBus); + if (loginManager.isValid()) { + QList args = {sessionId}; + loginManager.callWithCallback("GetSession", args, this, SLOT(login1SessionObjectReceived(QDBusMessage))); + } sessionBus.connect("com.canonical.Unity", // service "/com/canonical/Unity/Session", // path @@ -72,6 +76,28 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget* parent) SLOT(unityLocked())); } +void ScreenLockListenerDBus::login1SessionObjectReceived(QDBusMessage response) +{ + if (response.arguments().isEmpty()) { + qDebug() << "org.freedesktop.login1.Manager.GetSession did not return results"; + return; + } + QVariant arg0 = response.arguments().at(0); + if (!arg0.canConvert()) { + qDebug() << "org.freedesktop.login1.Manager.GetSession did not return a QDBusObjectPath"; + return; + } + QDBusObjectPath path = arg0.value(); + QDBusConnection systemBus = QDBusConnection::systemBus(); + + systemBus.connect("", // service + path.path(), // path + "org.freedesktop.login1.Session", // interface + "Lock", // signal name + this, // receiver + SLOT(unityLocked())); +} + void ScreenLockListenerDBus::gnomeSessionStatusChanged(uint status) { if (status != 0) { diff --git a/src/core/ScreenLockListenerDBus.h b/src/core/ScreenLockListenerDBus.h index ab73a8cf3d..59120eed33 100644 --- a/src/core/ScreenLockListenerDBus.h +++ b/src/core/ScreenLockListenerDBus.h @@ -18,6 +18,7 @@ #ifndef SCREENLOCKLISTENERDBUS_H #define SCREENLOCKLISTENERDBUS_H #include "ScreenLockListenerPrivate.h" +#include #include #include @@ -32,6 +33,7 @@ private slots: void logindPrepareForSleep(bool beforeSleep); void unityLocked(); void freedesktopScreenSaver(bool status); + void login1SessionObjectReceived(QDBusMessage); }; #endif // SCREENLOCKLISTENERDBUS_H From 332c133893c2e9719f610b2f8dc11bf13539dbd8 Mon Sep 17 00:00:00 2001 From: Shun Sakai Date: Thu, 16 Jan 2020 19:18:37 +0900 Subject: [PATCH 025/215] Fix CLI man page - Fix lacking commas in the generate options section - Fix a typo in groff command --- share/docs/man/keepassxc-cli.1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index 2be6b198ac..d925d46bca 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -7,7 +7,7 @@ keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager .B keepassxc-cli .I command .B [ --I options +.I options .B ] .SH DESCRIPTION @@ -222,22 +222,22 @@ Flattens the output to single lines. When this option is enabled, subgroups and .IP "-L, --length " Sets the desired length for the generated password. [Default: 16] -.IP "-l --lower" +.IP "-l, --lower" Uses lowercase characters for the generated password. [Default: Enabled] -.IP "-U --upper" +.IP "-U, --upper" Uses uppercase characters for the generated password. [Default: Enabled] -.IP "-n --numeric" +.IP "-n, --numeric" Uses numbers characters for the generated password. [Default: Enabled] -.IP "-s --special" +.IP "-s, --special" Uses special characters for the generated password. [Default: Disabled] -.IP "-e --extended" +.IP "-e, --extended" Uses extended ASCII characters for the generated password. [Default: Disabled] -.IP "-x --exclude " +.IP "-x, --exclude " Comma-separated list of characters to exclude from the generated password. None is excluded by default. .IP "--exclude-similar" From 8bac8a7163272efb067d388954119c9309f05f41 Mon Sep 17 00:00:00 2001 From: Shun Sakai Date: Thu, 16 Jan 2020 20:11:48 +0900 Subject: [PATCH 026/215] Change command and option names to bold in man pages --- share/docs/man/keepassxc-cli.1 | 120 ++++++++++++++++----------------- share/docs/man/keepassxc.1 | 14 ++-- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index d925d46bca..1fefda104d 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -15,118 +15,118 @@ keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager .SH COMMANDS -.IP "add [options] " +.IP "\fBadd\fP [options] " Adds a new entry to a database. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set. -.IP "analyze [options] " +.IP "\fBanalyze\fP [options] " Analyzes passwords in a database for weaknesses. -.IP "clip [options] [timeout]" +.IP "\fBclip\fP [options] [timeout]" Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. -.IP "close" +.IP "\fBclose\fP" In interactive mode, closes the currently opened database (see \fIopen\fP). -.IP "create [options] " +.IP "\fBcreate\fP [options] " Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. -.IP "diceware [options]" +.IP "\fBdiceware\fP [options]" Generates a random diceware passphrase. -.IP "edit [options] " +.IP "\fBedit\fP [options] " Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set. -.IP "estimate [options] [password]" +.IP "\fBestimate\fP [options] [password]" Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. -.IP "exit" +.IP "\fBexit\fP" Exits interactive mode. Synonymous with \fIquit\fP. -.IP "export [options] " +.IP "\fBexport\fP [options] " Exports the content of a database to standard output in the specified format (defaults to XML). -.IP "generate [options]" +.IP "\fBgenerate\fP [options]" Generates a random password. -.IP "help [command]" +.IP "\fBhelp\fP [command]" Displays a list of available commands, or detailed information about the specified command. -.IP "import [options] " +.IP "\fBimport\fP [options] " Imports the contents of an XML database to the target database. -.IP "locate [options] " +.IP "\fBlocate\fP [options] " Locates all the entries that match a specific search term in a database. -.IP "ls [options] [group]" +.IP "\fBls\fP [options] [group]" Lists the contents of a group in a database. If no group is specified, it will default to the root group. -.IP "merge [options] " +.IP "\fBmerge\fP [options] " Merges two databases together. The first database file is going to be replaced by the result of the merge, for that reason it is advisable to keep a backup of the two database files before attempting a merge. In the case that both databases make use of the same credentials, the \fI--same-credentials\fP or \fI-s\fP option can be used. -.IP "mkdir [options] " +.IP "\fBmkdir\fP [options] " Adds a new group to a database. -.IP "mv [options] " +.IP "\fBmv\fP [options] " Moves an entry to a new group. -.IP "open [options] " +.IP "\fBopen\fP [options] " Opens the given database in a shell-style interactive mode. This is useful for performing multiple operations on a single database (e.g. \fIls\fP followed by \fIshow\fP). -.IP "quit" +.IP "\fBquit\fP" Exits interactive mode. Synonymous with \fIexit\fP. -.IP "rm [options] " +.IP "\fBrm\fP [options] " Removes an entry from a database. If the database has a recycle bin, the entry will be moved there. If the entry is already in the recycle bin, it will be removed permanently. -.IP "rmdir [options] " +.IP "\fBrmdir\fP [options] " Removes a group from a database. If the database has a recycle bin, the group will be moved there. If the group is already in the recycle bin, it will be removed permanently. -.IP "show [options] " +.IP "\fBshow\fP [options] " Shows the title, username, password, URL and notes of a database entry. Can also show the current TOTP. Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the \fIclip\fP command section also applies here. .SH OPTIONS .SS "General options" -.IP "--debug-info" +.IP "\fB--debug-info\fP" Displays debugging information. -.IP "-k, --key-file " +.IP "\fB-k\fP, \fB--key-file\fP " Specifies a path to a key file for unlocking the database. In a merge operation this option, is used to specify the key file path for the first database. -.IP "--no-password" +.IP "\fB--no-password\fP" Deactivates the password key for the database. -.IP "-y, --yubikey " +.IP "\fB-y\fP, \fB--yubikey\fP " Specifies a yubikey slot for unlocking the database. In a merge operation this option is used to specify the yubikey slot for the first database. -.IP "-q, --quiet " +.IP "\fB-q\fP, \fB--quiet\fP " Silences password prompt and other secondary outputs. -.IP "-h, --help" +.IP "\fB-h\fP, \fB--help\fP" Displays help information. -.IP "-v, --version" +.IP "\fB-v\fP, \fB--version\fP" Displays the program version. .SS "Merge options" -.IP "-d, --dry-run " +.IP "\fB-d\fP, \fB--dry-run\fP " Prints the changes detected by the merge operation without making any changes to the database. -.IP "--key-file-from " +.IP "\fB--key-file-from\fP " Sets the path of the key file for the second database. -.IP "--no-password-from" +.IP "\fB--no-password-from\fP" Deactivates password key for the database to merge from. -.IP "--yubikey-from " +.IP "\fB--yubikey-from\fP " Yubikey slot for the second database. -.IP "-s, --same-credentials" +.IP "\fB-s\fP, \fB--same-credentials\fP" Uses the same credentials for unlocking both databases. @@ -134,34 +134,34 @@ Uses the same credentials for unlocking both databases. The same password generation options as documented for the generate command can be used with those 2 commands when the -g option is set. -.IP "-u, --username " +.IP "\fB-u\fP, \fB--username\fP " Specifies the username of the entry. -.IP "--url " +.IP "\fB--url\fP " Specifies the URL of the entry. -.IP "-p, --password-prompt" +.IP "\fB-p\fP, \fB--password-prompt\fP" Uses a password prompt for the entry's password. -.IP "-g, --generate" +.IP "\fB-g\fP, \fB--generate\fP" Generates a new password for the entry. .SS "Edit options" -.IP "-t, --title " +.IP "\fB-t\fP, \fB--title\fP <title>" Specifies the title of the entry. .SS "Estimate options" -.IP "-a, --advanced" +.IP "\fB-a\fP, \fB--advanced\fP" Performs advanced analysis on the password. .SS "Analyze options" -.IP "-H, --hibp <filename>" +.IP "\fB-H\fP, \fB--hibp\fP <filename>" Checks if any passwords have been publicly leaked, by comparing against the given list of password SHA-1 hashes, which must be in "Have I Been Pwned" format. Such files are available from https://haveibeenpwned.com/Passwords; note that they @@ -171,33 +171,33 @@ hour or so). .SS "Clip options" -.IP "-t, --totp" +.IP "\fB-t\fP, \fB--totp\fP" Copies the current TOTP instead of current password to clipboard. Will report an error if no TOTP is configured for the entry. .SS "Show options" -.IP "-a, --attributes <attribute>..." +.IP "\fB-a\fP, \fB--attributes\fP <attribute>..." Shows the named attributes. This option can be specified more than once, with each attribute shown one-per-line in the given order. If no attributes are specified and \fI-t\fP is not specified, a summary of the default attributes is given. Protected attributes will be displayed in clear text if specified explicitly by this option. -.IP "-s, --show-protected" +.IP "\fB-s\fP, \fB--show-protected\fP" Shows the protected attributes in clear text. -.IP "-t, --totp" +.IP "\fB-t\fP, \fB--totp\fP" Also shows the current TOTP, reporting an error if no TOTP is configured for the entry. .SS "Diceware options" -.IP "-W, --words <count>" +.IP "\fB-W\fP, \fB--words\fP <count>" Sets the desired number of words for the generated passphrase. [Default: 7] -.IP "-w, --word-list <path>" +.IP "\fB-w\fP, \fB--word-list\fP <path>" Sets the Path of the wordlist for the diceware generator. The wordlist must have > 1000 words, otherwise the program will fail. If the wordlist has < 4000 words a warning will be printed to STDERR. @@ -205,45 +205,45 @@ words a warning will be printed to STDERR. .SS "Export options" -.IP "-f, --format" +.IP "\fB-f\fP, \fB--format\fP" Format to use when exporting. Available choices are xml or csv. Defaults to xml. .SS "List options" -.IP "-R, --recursive" +.IP "\fB-R\fP, \fB--recursive\fP" Recursively lists the elements of the group. -.IP "-f, --flatten" +.IP "\fB-f\fP, \fB--flatten\fP" Flattens the output to single lines. When this option is enabled, subgroups and subentries will be displayed with a relative group path instead of indentation. .SS "Generate options" -.IP "-L, --length <length>" +.IP "\fB-L\fP, \fB--length\fP <length>" Sets the desired length for the generated password. [Default: 16] -.IP "-l, --lower" +.IP "\fB-l\fP, \fB--lower\fP" Uses lowercase characters for the generated password. [Default: Enabled] -.IP "-U, --upper" +.IP "\fB-U\fP, \fB--upper\fP" Uses uppercase characters for the generated password. [Default: Enabled] -.IP "-n, --numeric" +.IP "\fB-n\fP, \fB--numeric\fP" Uses numbers characters for the generated password. [Default: Enabled] -.IP "-s, --special" +.IP "\fB-s\fP, \fB--special\fP" Uses special characters for the generated password. [Default: Disabled] -.IP "-e, --extended" +.IP "\fB-e\fP, \fB--extended\fP" Uses extended ASCII characters for the generated password. [Default: Disabled] -.IP "-x, --exclude <chars>" +.IP "\fB-x\fP, \fB--exclude\fP <chars>" Comma-separated list of characters to exclude from the generated password. None is excluded by default. -.IP "--exclude-similar" +.IP "\fB--exclude-similar\fP" Exclude similar looking characters. [Default: Disabled] -.IP "--every-group" +.IP "\fB--every-group\fP" Include characters from every selected group. [Default: Disabled] diff --git a/share/docs/man/keepassxc.1 b/share/docs/man/keepassxc.1 index 74a9b02a68..fd2bbced45 100644 --- a/share/docs/man/keepassxc.1 +++ b/share/docs/man/keepassxc.1 @@ -14,25 +14,25 @@ keepassxc \- password manager \fBKeePassXC\fP is a free/open-source password manager or safe which helps you to manage your passwords in a secure way. The complete database is always encrypted with the industry-standard AES (alias Rijndael) encryption algorithm using a 256 bit key. KeePassXC uses a database format that is compatible with KeePass Password Safe. Your wallet works offline and requires no Internet connection. .SH OPTIONS -.IP "-h, --help" +.IP "\fB-h\fP, \fB--help\fP" Displays this help. -.IP "-v, --version" +.IP "\fB-v\fP, \fB--version\fP" Displays version information. -.IP "--config <config>" +.IP "\fB--config\fP <config>" Path to a custom config file -.IP "--keyfile <keyfile>" +.IP "\fB--keyfile\fP <keyfile>" Key file of the database -.IP "--pw-stdin" +.IP "\fB--pw-stdin\fP" Read password of the database from stdin -.IP "--pw, --parent-window <handle>" +.IP "\fB--pw\fP, \fB--parent-window\fP <handle>" Parent window handle -.IP "--debug-info" +.IP "\fB--debug-info\fP" Displays debugging information. .SH AUTHOR From c8ab3b5f4f0bea29e704afb05180365bc9f2c8c9 Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Sun, 26 Jan 2020 21:38:43 -0500 Subject: [PATCH 027/215] Removing QColor (from Qt::Widgets) from core modules. (#4247) --- src/core/Compare.cpp | 21 --------------------- src/core/Compare.h | 8 -------- src/core/Entry.cpp | 12 ++++++------ src/core/Entry.h | 13 ++++++------- src/core/Metadata.cpp | 4 ++-- src/core/Metadata.h | 7 +++---- src/format/KdbxXmlReader.cpp | 19 +++++-------------- src/format/KdbxXmlReader.h | 2 +- src/format/KdbxXmlWriter.cpp | 18 +++--------------- src/format/KdbxXmlWriter.h | 2 -- src/gui/entry/EditEntryWidget.cpp | 8 ++++---- src/gui/entry/EntryModel.cpp | 12 ++++++++---- tests/TestKeePass2Format.cpp | 10 +++++----- tests/TestModified.cpp | 4 ++-- tests/gui/TestGui.cpp | 4 ++-- 15 files changed, 47 insertions(+), 97 deletions(-) diff --git a/src/core/Compare.cpp b/src/core/Compare.cpp index 12e5029b76..5dccdd7819 100644 --- a/src/core/Compare.cpp +++ b/src/core/Compare.cpp @@ -15,24 +15,3 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ #include "Compare.h" - -#include <QColor> - -bool operator<(const QColor& lhs, const QColor& rhs) -{ - const QColor adaptedLhs = lhs.toCmyk(); - const QColor adaptedRhs = rhs.toCmyk(); - const int iCyan = compare(adaptedLhs.cyanF(), adaptedRhs.cyanF()); - if (iCyan != 0) { - return iCyan; - } - const int iMagenta = compare(adaptedLhs.magentaF(), adaptedRhs.magentaF()); - if (iMagenta != 0) { - return iMagenta; - } - const int iYellow = compare(adaptedLhs.yellowF(), adaptedRhs.yellowF()); - if (iYellow != 0) { - return iYellow; - } - return compare(adaptedLhs.blackF(), adaptedRhs.blackF()) < 0; -} diff --git a/src/core/Compare.h b/src/core/Compare.h index 5124caf6e9..921893859a 100644 --- a/src/core/Compare.h +++ b/src/core/Compare.h @@ -34,14 +34,6 @@ enum CompareItemOption Q_DECLARE_FLAGS(CompareItemOptions, CompareItemOption) Q_DECLARE_OPERATORS_FOR_FLAGS(CompareItemOptions) -class QColor; -/*! - * \return true when both color match - * - * Comparison converts both into the cmyk-model - */ -bool operator<(const QColor& lhs, const QColor& rhs); - template <typename Type> inline short compareGeneric(const Type& lhs, const Type& rhs, CompareItemOptions) { if (lhs != rhs) { diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 1b05b9e6e5..4e6911c37a 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -208,12 +208,12 @@ const QUuid& Entry::iconUuid() const return m_data.customIcon; } -QColor Entry::foregroundColor() const +QString Entry::foregroundColor() const { return m_data.foregroundColor; } -QColor Entry::backgroundColor() const +QString Entry::backgroundColor() const { return m_data.backgroundColor; } @@ -508,14 +508,14 @@ void Entry::setIcon(const QUuid& uuid) } } -void Entry::setForegroundColor(const QColor& color) +void Entry::setForegroundColor(const QString& colorStr) { - set(m_data.foregroundColor, color); + set(m_data.foregroundColor, colorStr); } -void Entry::setBackgroundColor(const QColor& color) +void Entry::setBackgroundColor(const QString& colorStr) { - set(m_data.backgroundColor, color); + set(m_data.backgroundColor, colorStr); } void Entry::setOverrideUrl(const QString& url) diff --git a/src/core/Entry.h b/src/core/Entry.h index 49ec6f0276..8b52b51092 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -19,7 +19,6 @@ #ifndef KEEPASSX_ENTRY_H #define KEEPASSX_ENTRY_H -#include <QColor> #include <QImage> #include <QMap> #include <QPixmap> @@ -57,8 +56,8 @@ struct EntryData { int iconNumber; QUuid customIcon; - QColor foregroundColor; - QColor backgroundColor; + QString foregroundColor; + QString backgroundColor; QString overrideUrl; QString tags; bool autoTypeEnabled; @@ -86,8 +85,8 @@ class Entry : public QObject QPixmap iconScaledPixmap() const; int iconNumber() const; const QUuid& iconUuid() const; - QColor foregroundColor() const; - QColor backgroundColor() const; + QString foregroundColor() const; + QString backgroundColor() const; QString overrideUrl() const; QString tags() const; const TimeInfo& timeInfo() const; @@ -132,8 +131,8 @@ class Entry : public QObject void setUuid(const QUuid& uuid); void setIcon(int iconNumber); void setIcon(const QUuid& uuid); - void setForegroundColor(const QColor& color); - void setBackgroundColor(const QColor& color); + void setForegroundColor(const QString& color); + void setBackgroundColor(const QString& color); void setOverrideUrl(const QString& url); void setTags(const QString& tags); void setTimeInfo(const TimeInfo& timeInfo); diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index ff1ee71e7d..fb18f711b9 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -131,7 +131,7 @@ int Metadata::maintenanceHistoryDays() const return m_data.maintenanceHistoryDays; } -QColor Metadata::color() const +QString Metadata::color() const { return m_data.color; } @@ -347,7 +347,7 @@ void Metadata::setMaintenanceHistoryDays(int value) set(m_data.maintenanceHistoryDays, value); } -void Metadata::setColor(const QColor& value) +void Metadata::setColor(const QString& value) { set(m_data.color, value); } diff --git a/src/core/Metadata.h b/src/core/Metadata.h index 01abcb809d..b39dafaf02 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -18,7 +18,6 @@ #ifndef KEEPASSX_METADATA_H #define KEEPASSX_METADATA_H -#include <QColor> #include <QDateTime> #include <QHash> #include <QImage> @@ -49,7 +48,7 @@ class Metadata : public QObject QString defaultUserName; QDateTime defaultUserNameChanged; int maintenanceHistoryDays; - QColor color; + QString color; bool recycleBinEnabled; int historyMaxItems; int historyMaxSize; @@ -72,7 +71,7 @@ class Metadata : public QObject QDateTime defaultUserNameChanged() const; QDateTime settingsChanged() const; int maintenanceHistoryDays() const; - QColor color() const; + QString color() const; bool protectTitle() const; bool protectUsername() const; bool protectPassword() const; @@ -113,7 +112,7 @@ class Metadata : public QObject void setDefaultUserNameChanged(const QDateTime& value); void setSettingsChanged(const QDateTime& value); void setMaintenanceHistoryDays(int value); - void setColor(const QColor& value); + void setColor(const QString& value); void setProtectTitle(bool value); void setProtectUsername(bool value); void setProtectPassword(bool value); diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index ab2b9aeb7f..f4109a4dfa 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -1047,22 +1047,21 @@ QDateTime KdbxXmlReader::readDateTime() return Clock::currentDateTimeUtc(); } -QColor KdbxXmlReader::readColor() +QString KdbxXmlReader::readColor() { QString colorStr = readString(); if (colorStr.isEmpty()) { - return {}; + return colorStr; } if (colorStr.length() != 7 || colorStr[0] != '#') { if (m_strictMode) { raiseError(tr("Invalid color value")); } - return {}; + return colorStr; } - QColor color; for (int i = 0; i <= 2; ++i) { QString rgbPartStr = colorStr.mid(1 + 2 * i, 2); bool ok; @@ -1071,19 +1070,11 @@ QColor KdbxXmlReader::readColor() if (m_strictMode) { raiseError(tr("Invalid color rgb part")); } - return {}; - } - - if (i == 0) { - color.setRed(rgbPart); - } else if (i == 1) { - color.setGreen(rgbPart); - } else { - color.setBlue(rgbPart); + return colorStr; } } - return color; + return colorStr; } int KdbxXmlReader::readNumber() diff --git a/src/format/KdbxXmlReader.h b/src/format/KdbxXmlReader.h index 2ec9c9f668..5623439713 100644 --- a/src/format/KdbxXmlReader.h +++ b/src/format/KdbxXmlReader.h @@ -83,7 +83,7 @@ class KdbxXmlReader virtual QString readString(bool& isProtected, bool& protectInMemory); virtual bool readBool(); virtual QDateTime readDateTime(); - virtual QColor readColor(); + virtual QString readColor(); virtual int readNumber(); virtual QUuid readUuid(); virtual QByteArray readBinary(); diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp index 7aa79c47d1..14d9204391 100644 --- a/src/format/KdbxXmlWriter.cpp +++ b/src/format/KdbxXmlWriter.cpp @@ -111,7 +111,7 @@ void KdbxXmlWriter::writeMetadata() writeString("DefaultUserName", m_meta->defaultUserName()); writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged()); writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays()); - writeColor("Color", m_meta->color()); + writeString("Color", m_meta->color()); writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged()); writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec()); writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce()); @@ -346,8 +346,8 @@ void KdbxXmlWriter::writeEntry(const Entry* entry) if (!entry->iconUuid().isNull()) { writeUuid("CustomIconUUID", entry->iconUuid()); } - writeColor("ForegroundColor", entry->foregroundColor()); - writeColor("BackgroundColor", entry->backgroundColor()); + writeString("ForegroundColor", entry->foregroundColor()); + writeString("BackgroundColor", entry->backgroundColor()); writeString("OverrideURL", entry->overrideUrl()); writeString("Tags", entry->tags()); writeTimes(entry->timeInfo()); @@ -532,18 +532,6 @@ void KdbxXmlWriter::writeBinary(const QString& qualifiedName, const QByteArray& writeString(qualifiedName, QString::fromLatin1(ba.toBase64())); } -void KdbxXmlWriter::writeColor(const QString& qualifiedName, const QColor& color) -{ - QString colorStr; - - if (color.isValid()) { - colorStr = QString("#%1%2%3").arg( - colorPartToString(color.red()), colorPartToString(color.green()), colorPartToString(color.blue())); - } - - writeString(qualifiedName, colorStr); -} - void KdbxXmlWriter::writeTriState(const QString& qualifiedName, Group::TriState triState) { QString value; diff --git a/src/format/KdbxXmlWriter.h b/src/format/KdbxXmlWriter.h index 1a367a263d..eaad9f2110 100644 --- a/src/format/KdbxXmlWriter.h +++ b/src/format/KdbxXmlWriter.h @@ -18,7 +18,6 @@ #ifndef KEEPASSX_KDBXXMLWRITER_H #define KEEPASSX_KDBXXMLWRITER_H -#include <QColor> #include <QDateTime> #include <QImage> #include <QXmlStreamWriter> @@ -74,7 +73,6 @@ class KdbxXmlWriter void writeUuid(const QString& qualifiedName, const Group* group); void writeUuid(const QString& qualifiedName, const Entry* entry); void writeBinary(const QString& qualifiedName, const QByteArray& ba); - void writeColor(const QString& qualifiedName, const QColor& color); void writeTriState(const QString& qualifiedName, Group::TriState triState); QString colorPartToString(int value); QString stripInvalidXml10Chars(QString str); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 02b9f3a59c..a3ed302973 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -1123,15 +1123,15 @@ void EditEntryWidget::updateEntryData(Entry* entry) const entry->setNotes(m_mainUi->notesEdit->toPlainText()); 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(); diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index bf7eca0c7e..b4c5840cfd 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -271,6 +271,8 @@ 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 @@ -279,12 +281,14 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } #endif return QVariant(p.color(QPalette::Active, QPalette::Mid)); - } else if (entry->foregroundColor().isValid()) { - return QVariant(entry->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) { diff --git a/tests/TestKeePass2Format.cpp b/tests/TestKeePass2Format.cpp index ce4f63fedf..df414f5c0a 100644 --- a/tests/TestKeePass2Format.cpp +++ b/tests/TestKeePass2Format.cpp @@ -86,7 +86,7 @@ void TestKeePass2Format::testXmlMetadata() QCOMPARE(m_xmlDb->metadata()->defaultUserName(), QString("DEFUSERNAME")); QCOMPARE(m_xmlDb->metadata()->defaultUserNameChanged(), MockClock::datetimeUtc(2010, 8, 8, 17, 27, 45)); QCOMPARE(m_xmlDb->metadata()->maintenanceHistoryDays(), 127); - QCOMPARE(m_xmlDb->metadata()->color(), QColor(0xff, 0xef, 0x00)); + QCOMPARE(m_xmlDb->metadata()->color(), QString("#FFEF00")); QCOMPARE(m_xmlDb->metadata()->masterKeyChanged(), MockClock::datetimeUtc(2012, 4, 5, 17, 9, 34)); QCOMPARE(m_xmlDb->metadata()->masterKeyChangeRec(), 101); QCOMPARE(m_xmlDb->metadata()->masterKeyChangeForce(), -1); @@ -200,8 +200,8 @@ void TestKeePass2Format::testXmlEntry1() QCOMPARE(entry->historyItems().size(), 2); QCOMPARE(entry->iconNumber(), 0); QCOMPARE(entry->iconUuid(), QUuid()); - QVERIFY(!entry->foregroundColor().isValid()); - QVERIFY(!entry->backgroundColor().isValid()); + QVERIFY(entry->foregroundColor().isEmpty()); + QVERIFY(entry->backgroundColor().isEmpty()); QCOMPARE(entry->overrideUrl(), QString("")); QCOMPARE(entry->tags(), QString("a b c")); @@ -262,8 +262,8 @@ void TestKeePass2Format::testXmlEntry2() QCOMPARE(entry->iconNumber(), 0); QCOMPARE(entry->iconUuid(), QUuid::fromRfc4122(QByteArray::fromBase64("++vyI+daLk6omox4a6kQGA=="))); // TODO: test entry->icon() - QCOMPARE(entry->foregroundColor(), QColor(255, 0, 0)); - QCOMPARE(entry->backgroundColor(), QColor(255, 255, 0)); + QCOMPARE(entry->foregroundColor(), QString("#FF0000")); + QCOMPARE(entry->backgroundColor(), QString("#FFFF00")); QCOMPARE(entry->overrideUrl(), QString("http://override.net/")); QCOMPARE(entry->tags(), QString("")); diff --git a/tests/TestModified.cpp b/tests/TestModified.cpp index 254db37968..dcaeca8ffa 100644 --- a/tests/TestModified.cpp +++ b/tests/TestModified.cpp @@ -309,13 +309,13 @@ void TestModified::testEntrySets() entry->setDefaultAutoTypeSequence(entry->defaultAutoTypeSequence()); QTRY_COMPARE(spyModified.count(), spyCount); - entry->setForegroundColor(Qt::red); + entry->setForegroundColor(QString("#FF0000")); ++spyCount; QTRY_COMPARE(spyModified.count(), spyCount); entry->setForegroundColor(entry->foregroundColor()); QTRY_COMPARE(spyModified.count(), spyCount); - entry->setBackgroundColor(Qt::red); + entry->setBackgroundColor(QString("#FF0000")); ++spyCount; QTRY_COMPARE(spyModified.count(), spyCount); entry->setBackgroundColor(entry->backgroundColor()); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 9118d3e214..64e913c76f 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -432,8 +432,8 @@ void TestGui::testEditEntry() // Test entry colors (simulate choosing a color) editEntryWidget->setCurrentPage(1); - auto fgColor = QColor(Qt::red); - auto bgColor = QColor(Qt::blue); + auto fgColor = QString("#FF0000"); + auto bgColor = QString("#0000FF"); // Set foreground color auto colorButton = editEntryWidget->findChild<QPushButton*>("fgColorButton"); auto colorCheckBox = editEntryWidget->findChild<QCheckBox*>("fgColorCheckBox"); From 04be724614460d5d0e60a741f93a99c15dec4ee3 Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Sat, 25 Jan 2020 16:41:15 -0500 Subject: [PATCH 028/215] Remove extraneous readme section It's a convention that the first text block after the title is the general description of the project, so we don't need the explicit section there. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 5fad0cc835..50a1b45024 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # <img src="https://keepassxc.org/logo.png" width="40" height="40"/> KeePassXC [![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(project:KeepassXC\)/statusIcon)](https://ci.keepassxc.org/?guest=1) [![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc) -## About KeePassXC [KeePassXC](https://keepassxc.org) is a cross-platform community fork of [KeePassX](https://www.keepassx.org/). Our goal is to extend and improve it with new features and bugfixes From b78ca924fdf2b6d5937d87bab65104cba629382f Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Sun, 26 Jan 2020 23:44:31 -0500 Subject: [PATCH 029/215] Adding db-info CLI command. (#4231) This adds a basic db-show CLI command, to display the information related to a database. --- CHANGELOG.md | 8 +++++ share/docs/man/keepassxc-cli.1 | 5 ++- src/cli/CMakeLists.txt | 1 + src/cli/Command.cpp | 4 ++- src/cli/Info.cpp | 53 +++++++++++++++++++++++++++++++ src/cli/Info.h | 31 ++++++++++++++++++ src/crypto/kdf/AesKdf.cpp | 5 +++ src/crypto/kdf/AesKdf.h | 1 + src/crypto/kdf/Argon2Kdf.cpp | 5 +++ src/crypto/kdf/Argon2Kdf.h | 1 + src/crypto/kdf/Kdf.h | 2 ++ src/format/KeePass2.cpp | 6 ++-- tests/TestCli.cpp | 58 ++++++++++++++++++++++++++++------ tests/TestCli.h | 1 + 14 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 src/cli/Info.cpp create mode 100644 src/cli/Info.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b5a51e2b..68ec8f1f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.6 (unreleased) + +### Added +- Added CLI db-info command [#4231] + +### Changed +- Renamed CLI create command to db-create [#4231] + ## 2.5.3 (2020-01-19) ### Fixed diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index 1fefda104d..cba3ddb75b 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -28,9 +28,12 @@ Copies the password or the current TOTP (\fI-t\fP option) of a database entry to .IP "\fBclose\fP" In interactive mode, closes the currently opened database (see \fIopen\fP). -.IP "\fBcreate\fP [options] <database>" +.IP "\fBdb-create\fP [options] <database>" Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. +.IP "\fBdb-info\fP [options] <database>" +Show a database's information. + .IP "\fBdiceware\fP [options]" Generates a random diceware passphrase. diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index f5c90df8d1..5959bf3288 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -30,6 +30,7 @@ set(cli_SOURCES Generate.cpp Help.cpp Import.cpp + Info.cpp List.cpp Locate.cpp Merge.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index 4d3bf82706..e76f633ef2 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -37,6 +37,7 @@ #include "Generate.h" #include "Help.h" #include "Import.h" +#include "Info.h" #include "List.h" #include "Locate.h" #include "Merge.h" @@ -160,7 +161,8 @@ namespace Commands s_commands.insert(QStringLiteral("analyze"), QSharedPointer<Command>(new Analyze())); s_commands.insert(QStringLiteral("clip"), QSharedPointer<Command>(new Clip())); s_commands.insert(QStringLiteral("close"), QSharedPointer<Command>(new Close())); - s_commands.insert(QStringLiteral("create"), QSharedPointer<Command>(new Create())); + s_commands.insert(QStringLiteral("db-create"), QSharedPointer<Command>(new Create())); + s_commands.insert(QStringLiteral("db-info"), QSharedPointer<Command>(new Info())); s_commands.insert(QStringLiteral("diceware"), QSharedPointer<Command>(new Diceware())); s_commands.insert(QStringLiteral("edit"), QSharedPointer<Command>(new Edit())); s_commands.insert(QStringLiteral("estimate"), QSharedPointer<Command>(new Estimate())); diff --git a/src/cli/Info.cpp b/src/cli/Info.cpp new file mode 100644 index 0000000000..4e80b75b41 --- /dev/null +++ b/src/cli/Info.cpp @@ -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/>. + */ +#include <cstdlib> +#include <stdio.h> + +#include "Info.h" + +#include "core/Database.h" +#include "core/Metadata.h" +#include "format/KeePass2.h" + +#include "Utils.h" + +Info::Info() +{ + name = QString("db-show"); + description = QObject::tr("Show a database's information."); +} + +int Info::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser>) +{ + TextStream out(Utils::STDOUT, QIODevice::WriteOnly); + + out << QObject::tr("UUID: ") << database->uuid().toString() << endl; + out << QObject::tr("Name: ") << database->metadata()->name() << endl; + out << QObject::tr("Description: ") << database->metadata()->description() << endl; + for (auto& cipher : asConst(KeePass2::CIPHERS)) { + if (cipher.first == database->cipher()) { + out << QObject::tr("Cipher: ") << cipher.second << endl; + } + } + out << QObject::tr("KDF: ") << database->kdf()->toString() << endl; + if (database->metadata()->recycleBinEnabled()) { + out << QObject::tr("Recycle bin is enabled.") << endl; + } else { + out << QObject::tr("Recycle bin is not enabled.") << endl; + } + return EXIT_SUCCESS; +} diff --git a/src/cli/Info.h b/src/cli/Info.h new file mode 100644 index 0000000000..1961a7b5f7 --- /dev/null +++ b/src/cli/Info.h @@ -0,0 +1,31 @@ +/* + * 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_INFO_H +#define KEEPASSXC_INFO_H + +#include "DatabaseCommand.h" + +class Info : public DatabaseCommand +{ +public: + Info(); + + int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser); +}; + +#endif // KEEPASSXC_INFO_H diff --git a/src/crypto/kdf/AesKdf.cpp b/src/crypto/kdf/AesKdf.cpp index 0b2130cfe7..d1daf1e5d0 100644 --- a/src/crypto/kdf/AesKdf.cpp +++ b/src/crypto/kdf/AesKdf.cpp @@ -125,3 +125,8 @@ int AesKdf::benchmarkImpl(int msec) const return static_cast<int>(rounds * (static_cast<float>(msec) / timer.elapsed())); } + +QString AesKdf::toString() const +{ + return QObject::tr("AES (%1 rounds)").arg(QString::number(rounds())); +} diff --git a/src/crypto/kdf/AesKdf.h b/src/crypto/kdf/AesKdf.h index 84156e6fb9..d71fbb1d14 100644 --- a/src/crypto/kdf/AesKdf.h +++ b/src/crypto/kdf/AesKdf.h @@ -30,6 +30,7 @@ class AesKdf : public Kdf QVariantMap writeParameters() override; bool transform(const QByteArray& raw, QByteArray& result) const override; QSharedPointer<Kdf> clone() const override; + QString toString() const override; protected: int benchmarkImpl(int msec) const override; diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp index 0d449b5b52..31995fdd09 100644 --- a/src/crypto/kdf/Argon2Kdf.cpp +++ b/src/crypto/kdf/Argon2Kdf.cpp @@ -211,3 +211,8 @@ int Argon2Kdf::benchmarkImpl(int msec) const return 1; } + +QString Argon2Kdf::toString() const +{ + return QObject::tr("Argon2 (%1 rounds, %2 KB)").arg(QString::number(rounds()), QString::number(memory())); +} diff --git a/src/crypto/kdf/Argon2Kdf.h b/src/crypto/kdf/Argon2Kdf.h index 73b7f85296..6a16ee96e5 100644 --- a/src/crypto/kdf/Argon2Kdf.h +++ b/src/crypto/kdf/Argon2Kdf.h @@ -36,6 +36,7 @@ class Argon2Kdf : public Kdf bool setMemory(quint64 kibibytes); quint32 parallelism() const; bool setParallelism(quint32 threads); + QString toString() const override; protected: int benchmarkImpl(int msec) const override; diff --git a/src/crypto/kdf/Kdf.h b/src/crypto/kdf/Kdf.h index 368fb16f71..4e6455eded 100644 --- a/src/crypto/kdf/Kdf.h +++ b/src/crypto/kdf/Kdf.h @@ -44,6 +44,8 @@ class Kdf virtual bool transform(const QByteArray& raw, QByteArray& result) const = 0; virtual QSharedPointer<Kdf> clone() const = 0; + virtual QString toString() const = 0; + int benchmark(int msec) const; protected: diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index fbc3930308..dc50ca0016 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -48,9 +48,9 @@ const QString KeePass2::KDFPARAM_ARGON2_SECRET("K"); const QString KeePass2::KDFPARAM_ARGON2_ASSOCDATA("A"); const QList<QPair<QUuid, QString>> KeePass2::CIPHERS{ - qMakePair(KeePass2::CIPHER_AES256, QObject::tr("AES: 256-bit")), - qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")), - qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20: 256-bit"))}; + qMakePair(KeePass2::CIPHER_AES256, QObject::tr("AES 256-bit")), + qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish 256-bit")), + qMakePair(KeePass2::CIPHER_CHACHA20, QObject::tr("ChaCha20 256-bit"))}; const QList<QPair<QUuid, QString>> KeePass2::KDFS{ qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2 (KDBX 4 – recommended)")), diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 076f7f74ed..d73cdedddb 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -44,6 +44,7 @@ #include "cli/Generate.h" #include "cli/Help.h" #include "cli/Import.h" +#include "cli/Info.h" #include "cli/List.h" #include "cli/Locate.h" #include "cli/Merge.h" @@ -192,7 +193,8 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("close")); - QVERIFY(Commands::getCommand("create")); + QVERIFY(Commands::getCommand("db-create")); + QVERIFY(Commands::getCommand("db-info")); QVERIFY(Commands::getCommand("diceware")); QVERIFY(Commands::getCommand("edit")); QVERIFY(Commands::getCommand("estimate")); @@ -210,7 +212,7 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("rmdir")); QVERIFY(Commands::getCommand("show")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 21); + QCOMPARE(Commands::getCommands().size(), 22); } void TestCli::testInteractiveCommands() @@ -220,7 +222,8 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("close")); - QVERIFY(Commands::getCommand("create")); + QVERIFY(Commands::getCommand("db-create")); + QVERIFY(Commands::getCommand("db-info")); QVERIFY(Commands::getCommand("diceware")); QVERIFY(Commands::getCommand("edit")); QVERIFY(Commands::getCommand("estimate")); @@ -238,7 +241,7 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("rmdir")); QVERIFY(Commands::getCommand("show")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 21); + QCOMPARE(Commands::getCommands().size(), 22); } void TestCli::testAdd() @@ -548,7 +551,7 @@ void TestCli::testCreate() QString databaseFilename = testDir->path() + "/testCreate1.kdbx"; // Password Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename}); + createCmd.execute({"db-create", databaseFilename}); m_stderrFile->reset(); m_stdoutFile->reset(); @@ -563,7 +566,7 @@ void TestCli::testCreate() // Should refuse to create the database if it already exists. qint64 pos = m_stdoutFile->pos(); qint64 errPos = m_stderrFile->pos(); - createCmd.execute({"create", databaseFilename}); + createCmd.execute({"db-create", databaseFilename}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); // Output should be empty when there is an error. @@ -577,7 +580,7 @@ void TestCli::testCreate() pos = m_stdoutFile->pos(); errPos = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename2, "-k", keyfilePath}); + createCmd.execute({"db-create", databaseFilename2, "-k", keyfilePath}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); @@ -594,7 +597,7 @@ void TestCli::testCreate() pos = m_stdoutFile->pos(); errPos = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", databaseFilename3, "-k", keyfilePath}); + createCmd.execute({"db-create", databaseFilename3, "-k", keyfilePath}); m_stdoutFile->seek(pos); m_stderrFile->seek(errPos); @@ -607,6 +610,41 @@ void TestCli::testCreate() QVERIFY(db3); } +void TestCli::testInfo() +{ + Info infoCmd; + QVERIFY(!infoCmd.name.isEmpty()); + QVERIFY(infoCmd.getDescriptionLine().contains(infoCmd.name)); + + Utils::Test::setNextPassword("a"); + infoCmd.execute({"db-info", m_dbFile->fileName()}); + m_stdoutFile->reset(); + m_stderrFile->reset(); + m_stdoutFile->readLine(); // skip prompt line + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QVERIFY(m_stdoutFile->readLine().contains(QByteArray("UUID: "))); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Name: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Description: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Cipher: AES 256-bit\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("KDF: AES (6000 rounds)\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Recycle bin is enabled.\n")); + + // Test with quiet option. + qint64 pos = m_stdoutFile->pos(); + qint64 errPos = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + infoCmd.execute({"db-info", "-q", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QVERIFY(m_stdoutFile->readLine().contains(QByteArray("UUID: "))); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Name: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Description: \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Cipher: AES 256-bit\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("KDF: AES (6000 rounds)\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Recycle bin is enabled.\n")); +} + void TestCli::testDiceware() { Diceware dicewareCmd; @@ -1446,10 +1484,10 @@ void TestCli::testMergeWithKeys() qint64 pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); - createCmd.execute({"create", sourceDatabaseFilename, "-k", sourceKeyfilePath}); + createCmd.execute({"db-create", sourceDatabaseFilename, "-k", sourceKeyfilePath}); Utils::Test::setNextPassword("b"); - createCmd.execute({"create", targetDatabaseFilename, "-k", targetKeyfilePath}); + createCmd.execute({"db-create", targetDatabaseFilename, "-k", targetKeyfilePath}); Utils::Test::setNextPassword("a"); auto sourceDatabase = QSharedPointer<Database>( diff --git a/tests/TestCli.h b/tests/TestCli.h index 4947ee472b..44420d580c 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -59,6 +59,7 @@ private slots: void testGenerate_data(); void testGenerate(); void testImport(); + void testInfo(); void testKeyFileOption(); void testNoPasswordOption(); void testHelp(); From c97ee5395bf0239a130908d7354e6efd94395399 Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Thu, 23 Jan 2020 20:54:36 -0500 Subject: [PATCH 030/215] Small cleanup in cli/Show.cpp --- src/cli/Show.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index 646d5d90d7..f7bf8d54b5 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -78,7 +78,7 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< } // If no attributes specified, output the default attribute set. - bool showAttributeNames = attributes.isEmpty() && !showTotp; + bool showDefaultAttributes = attributes.isEmpty() && !showTotp; if (attributes.isEmpty() && !showTotp) { attributes = EntryAttributes::DefaultAttributes; } @@ -91,10 +91,10 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< errorTextStream << QObject::tr("ERROR: unknown attribute %1.").arg(attributeName) << endl; continue; } - if (showAttributeNames) { + if (showDefaultAttributes) { outputTextStream << attributeName << ": "; } - if (entry->attributes()->isProtected(attributeName) && showAttributeNames && !showProtectedAttributes) { + if (entry->attributes()->isProtected(attributeName) && showDefaultAttributes && !showProtectedAttributes) { outputTextStream << "PROTECTED" << endl; } else { outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attributeName)) << endl; @@ -102,9 +102,6 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< } if (showTotp) { - if (showAttributeNames) { - outputTextStream << "TOTP: "; - } outputTextStream << entry->totp() << endl; } From e24a858f39f7c1ad762b2f860788ff65e5dddf10 Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Sat, 9 Nov 2019 20:45:08 +0200 Subject: [PATCH 031/215] SSH Agent: Refactor entry and agent key management - Remove duplicate code to load a key (EditEntryWidget & SSHAgent) - Refactor all key loading and saving to KeeAgentSettings - Depend only on Entry to allow future CLI expansion --- src/gui/entry/EditEntryWidget.cpp | 112 +++++---------------- src/gui/entry/EditEntryWidget.h | 2 +- src/sshagent/KeeAgentSettings.cpp | 160 +++++++++++++++++++++++++++--- src/sshagent/KeeAgentSettings.h | 18 +++- src/sshagent/SSHAgent.cpp | 56 ++--------- 5 files changed, 196 insertions(+), 152 deletions(-) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index a3ed302973..27dc8b10ee 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -106,12 +106,8 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) setupAutoType(); #ifdef WITH_XC_SSHAGENT - if (config()->get("SSHAgent", false).toBool()) { - setupSSHAgent(); - m_sshAgentEnabled = true; - } else { - m_sshAgentEnabled = false; - } + setupSSHAgent(); + m_sshAgentEnabled = config()->get("SSHAgent", false).toBool(); #endif #ifdef WITH_XC_BROWSER @@ -544,7 +540,7 @@ void EditEntryWidget::setupSSHAgent() void EditEntryWidget::updateSSHAgent() { KeeAgentSettings settings; - settings.fromXml(m_advancedUi->attachmentsWidget->getAttachment("KeeAgent.settings")); + settings.fromEntry(m_entry); m_sshAgentUi->addKeyToAgentCheckBox->setChecked(settings.addAtDatabaseOpen()); m_sshAgentUi->removeKeyFromAgentCheckBox->setChecked(settings.removeAtDatabaseClose()); @@ -557,15 +553,8 @@ void EditEntryWidget::updateSSHAgent() m_sshAgentUi->copyToClipboardButton->setEnabled(false); m_sshAgentSettings = settings; - updateSSHAgentAttachments(); - - if (settings.selectedType() == "attachment") { - m_sshAgentUi->attachmentRadioButton->setChecked(true); - } else { - m_sshAgentUi->externalFileRadioButton->setChecked(true); - } - updateSSHAgentKeyInfo(); + updateSSHAgentAttachments(); } void EditEntryWidget::updateSSHAgentAttachment() @@ -590,6 +579,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() @@ -639,10 +636,8 @@ void EditEntryWidget::updateSSHAgentKeyInfo() } } -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 +657,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 +671,18 @@ 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 (!settings.keyConfigured()) { return false; } - if (!key.parsePKCS1PEM(privateKeyData)) { - showMessage(key.errorString(), MessageWidget::Error); + if (!settings.toOpenSSHKey(m_entry, 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,11 +698,7 @@ void EditEntryWidget::addKeyToAgent() m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey()); KeeAgentSettings 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()); + toKeeAgentSettings(settings); if (!SSHAgent::instance()->addIdentity(key, settings)) { showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error); @@ -1064,9 +1007,7 @@ bool EditEntryWidget::commitEntry() m_autoTypeAssoc->removeEmpty(); #ifdef WITH_XC_SSHAGENT - if (m_sshAgentEnabled) { - saveSSHAgentConfig(); - } + toKeeAgentSettings(m_sshAgentSettings); #endif #ifdef WITH_XC_BROWSER @@ -1085,13 +1026,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); @@ -1152,6 +1088,12 @@ void EditEntryWidget::updateEntryData(Entry* entry) const } entry->autoTypeAssociations()->copyDataFrom(m_autoTypeAssoc); + +#ifdef WITH_XC_SSHAGENT + if (m_sshAgentEnabled) { + m_sshAgentSettings.toEntry(entry); + } +#endif } void EditEntryWidget::cancel() diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 300220cd0b..783bb3cb94 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -112,6 +112,7 @@ private slots: void toggleHideNotes(bool visible); void pickColor(); #ifdef WITH_XC_SSHAGENT + void toKeeAgentSettings(KeeAgentSettings& settings) const; void updateSSHAgent(); void updateSSHAgentAttachment(); void updateSSHAgentAttachments(); @@ -153,7 +154,6 @@ private slots: 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); diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index 8c22780058..b914263c15 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -19,20 +19,12 @@ #include "KeeAgentSettings.h" KeeAgentSettings::KeeAgentSettings() - : m_allowUseOfSshKey(false) - , m_addAtDatabaseOpen(false) - , m_removeAtDatabaseClose(false) - , m_useConfirmConstraintWhenAdding(false) - , m_useLifetimeConstraintWhenAdding(false) - , m_lifetimeConstraintDuration(600) + : m_lifetimeConstraintDuration(600) , m_selectedType(QString("file")) - , m_attachmentName(QString()) - , m_saveAttachmentToTempFile(false) - , m_fileName(QString()) { } -bool KeeAgentSettings::operator==(KeeAgentSettings& other) +bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const { // clang-format off return (m_allowUseOfSshKey == other.m_allowUseOfSshKey && m_addAtDatabaseOpen == other.m_addAtDatabaseOpen @@ -47,17 +39,32 @@ bool KeeAgentSettings::operator==(KeeAgentSettings& other) // clang-format on } -bool KeeAgentSettings::operator!=(KeeAgentSettings& other) +bool KeeAgentSettings::operator!=(const KeeAgentSettings& other) const { return !(*this == other); } -bool KeeAgentSettings::isDefault() +/** + * Test if this instance is at default settings. + * + * @return true if is at default settings + */ +bool KeeAgentSettings::isDefault() const { KeeAgentSettings defaultSettings; return (*this == defaultSettings); } +/** + * Get last error as a QString. + * + * @return translated error message + */ +const QString KeeAgentSettings::errorString() const +{ + return m_error; +} + bool KeeAgentSettings::allowUseOfSshKey() const { return m_allowUseOfSshKey; @@ -174,16 +181,26 @@ int KeeAgentSettings::readInt(QXmlStreamReader& reader) return ret; } +/** + * Read settings from an XML document. + * + * Sets error string on error. + * + * @param ba XML document + * @return success + */ bool KeeAgentSettings::fromXml(const QByteArray& ba) { QXmlStreamReader reader; reader.addData(ba); if (reader.error() || !reader.readNextStartElement()) { + m_error = reader.errorString(); return false; } if (reader.qualifiedName() != "EntrySettings") { + m_error = QCoreApplication::translate("KeeAgentSettings", "Invalid KeeAgent settings file structure."); return false; } @@ -230,7 +247,12 @@ bool KeeAgentSettings::fromXml(const QByteArray& ba) return true; } -QByteArray KeeAgentSettings::toXml() +/** + * Write settings to an XML document. + * + * @return XML document + */ +QByteArray KeeAgentSettings::toXml() const { QByteArray ba; QXmlStreamWriter writer(&ba); @@ -276,3 +298,115 @@ QByteArray KeeAgentSettings::toXml() return ba; } + +/** + * Read settings from an entry as an XML attachment. + * + * Sets error string on error. + * + * @param entry Entry to read the attachment from + * @return true if XML document was loaded + */ +bool KeeAgentSettings::fromEntry(const Entry* entry) +{ + return fromXml(entry->attachments()->value("KeeAgent.settings")); +} + +/** + * Write settings to an entry as an XML attachment. + * + * @param entry Entry to create the attachment to + */ +void KeeAgentSettings::toEntry(Entry* entry) const +{ + if (isDefault()) { + if (entry->attachments()->hasKey("KeeAgent.settings")) { + entry->attachments()->remove("KeeAgent.settings"); + } + } else { + entry->attachments()->set("KeeAgent.settings", toXml()); + } +} + +/** + * Test if a SSH key is currently set to be used + * + * @return true if key is configured + */ +bool KeeAgentSettings::keyConfigured() const +{ + if (m_selectedType == "attachment") { + return !m_attachmentName.isEmpty(); + } else { + return !m_fileName.isEmpty(); + } +} + +/** + * Read a SSH key based on settings from entry to key. + * + * Sets error string on error. + * + * @param entry input entry to read attachment and decryption key + * @param key output key object + * @param decrypt avoid private key decryption if possible (old RSA keys are always decrypted) + * @return true if key was properly opened + */ +bool KeeAgentSettings::toOpenSSHKey(const Entry* entry, OpenSSHKey& key, bool decrypt) +{ + QString fileName; + QByteArray privateKeyData; + + if (m_selectedType == "attachment") { + fileName = m_attachmentName; + privateKeyData = entry->attachments()->value(fileName); + } else { + QFile localFile(m_fileName); + QFileInfo localFileInfo(localFile); + fileName = localFileInfo.fileName(); + + if (localFile.fileName().isEmpty()) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Private key is empty"); + return false; + } + + if (localFile.size() > 1024 * 1024) { + m_error = QCoreApplication::translate("KeeAgentSettings", "File too large to be a private key"); + return false; + } + + if (!localFile.open(QIODevice::ReadOnly)) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Failed to open private key"); + return false; + } + + privateKeyData = localFile.readAll(); + } + + if (privateKeyData.isEmpty()) { + m_error = QCoreApplication::translate("KeeAgentSettings", "Private key is empty"); + return false; + } + + if (!key.parsePKCS1PEM(privateKeyData)) { + m_error = key.errorString(); + return false; + } + + if (key.encrypted() && (decrypt || key.publicParts().isEmpty())) { + if (!key.openKey(entry->password())) { + m_error = key.errorString(); + return false; + } + } + + if (key.comment().isEmpty()) { + key.setComment(entry->username()); + } + + if (key.comment().isEmpty()) { + key.setComment(fileName); + } + + return true; +} diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index 484dee88d2..357c695285 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -19,6 +19,8 @@ #ifndef KEEAGENTSETTINGS_H #define KEEAGENTSETTINGS_H +#include "core/Entry.h" +#include "crypto/ssh/OpenSSHKey.h" #include <QXmlStreamReader> #include <QtCore> @@ -27,12 +29,19 @@ class KeeAgentSettings public: KeeAgentSettings(); - bool operator==(KeeAgentSettings& other); - bool operator!=(KeeAgentSettings& other); - bool isDefault(); + bool operator==(const KeeAgentSettings& other) const; + bool operator!=(const KeeAgentSettings& other) const; + bool isDefault() const; bool fromXml(const QByteArray& ba); - QByteArray toXml(); + QByteArray toXml() const; + + bool fromEntry(const Entry* entry); + void toEntry(Entry* entry) const; + bool keyConfigured() const; + bool toOpenSSHKey(const Entry* entry, OpenSSHKey& key, bool decrypt); + + const QString errorString() const; bool allowUseOfSshKey() const; bool addAtDatabaseOpen() const; @@ -74,6 +83,7 @@ class KeeAgentSettings QString m_attachmentName; bool m_saveAttachmentToTempFile; QString m_fileName; + QString m_error; }; #endif // KEEAGENTSETTINGS_H diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index ac25a7066c..c7c174fdc7 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -308,73 +308,31 @@ void SSHAgent::databaseModeChanged() } for (Entry* e : widget->database()->rootGroup()->entriesRecursive()) { - if (widget->database()->metadata()->recycleBinEnabled() && e->group() == widget->database()->metadata()->recycleBin()) { continue; } - if (!e->attachments()->hasKey("KeeAgent.settings")) { - continue; - } - KeeAgentSettings settings; - settings.fromXml(e->attachments()->value("KeeAgent.settings")); - if (!settings.allowUseOfSshKey()) { + if (!settings.fromEntry(e)) { continue; } - QByteArray keyData; - QString fileName; - if (settings.selectedType() == "attachment") { - fileName = settings.attachmentName(); - keyData = e->attachments()->value(fileName); - } else if (!settings.fileName().isEmpty()) { - QFile file(settings.fileName()); - QFileInfo fileInfo(file); - - fileName = fileInfo.fileName(); - - if (file.size() > 1024 * 1024) { - continue; - } - - if (!file.open(QIODevice::ReadOnly)) { - continue; - } - - keyData = file.readAll(); - } - - if (keyData.isEmpty()) { + if (!settings.allowUseOfSshKey() || !settings.addAtDatabaseOpen()) { continue; } OpenSSHKey key; - if (!key.parsePKCS1PEM(keyData)) { - continue; - } - - if (!key.openKey(e->password())) { + if (!settings.toOpenSSHKey(e, key, true)) { continue; } - if (key.comment().isEmpty()) { - key.setComment(e->username()); - } - - if (key.comment().isEmpty()) { - key.setComment(fileName); - } - - if (settings.addAtDatabaseOpen()) { - // Add key to agent; ignore errors if we have previously added the key - bool known_key = m_addedKeys.contains(key); - if (!addIdentity(key, settings) && !known_key) { - emit error(m_error); - } + // Add key to agent; ignore errors if we have previously added the key + bool known_key = m_addedKeys.contains(key); + if (!addIdentity(key, settings) && !known_key) { + emit error(m_error); } } } From 4939179b9feccf9b7ce089769268cde0667deefb Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Sat, 25 Jan 2020 16:52:26 -0500 Subject: [PATCH 032/215] Adding release badge to README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50a1b45024..a24f18e633 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # <img src="https://keepassxc.org/logo.png" width="40" height="40"/> KeePassXC -[![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(project:KeepassXC\)/statusIcon)](https://ci.keepassxc.org/?guest=1) [![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc) +[![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(project:KeepassXC\)/statusIcon)](https://ci.keepassxc.org/?guest=1) +[![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc) +[![GitHub release](https://img.shields.io/github/release/keepassxreboot/keepassxc)](https://github.com/keepassxreboot/keepassxc/releases/) [KeePassXC](https://keepassxc.org) is a cross-platform community fork of [KeePassX](https://www.keepassx.org/). From 06e5f19fabeca49f69f703284aa8741a8f02c5d4 Mon Sep 17 00:00:00 2001 From: JulianVolodia <julianvolodia@gmail.com> Date: Fri, 27 Dec 2019 13:19:52 +0100 Subject: [PATCH 033/215] Enable browser-like DbTab experience (Alt + Nums) * Pressing ALT+1-9 goes to 1-9 tab * Pressing ALT+0 goes to the last tab --- src/gui/MainWindow.cpp | 38 ++++++++++++++++++++++++++++++++++++++ src/gui/MainWindow.h | 2 ++ 2 files changed, 40 insertions(+) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e4a6278912..e9c150dd5c 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -302,6 +302,27 @@ MainWindow::MainWindow() 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())); + new QShortcut(Qt::ALT + Qt::Key_0, this, SLOT(selectLastDatabaseTab())); + + auto shortcut = new QShortcut(Qt::ALT + Qt::Key_1, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(1); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_2, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(2); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_3, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(3); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_4, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(4); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_5, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(5); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_6, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(6); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_7, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(7); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_8, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(8); }); + shortcut = new QShortcut(Qt::ALT + Qt::Key_9, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(9); }); + // 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())); @@ -974,6 +995,23 @@ void MainWindow::selectPreviousDatabaseTab() } } +void MainWindow::selectDatabaseTab(int tabIndex) +{ + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + if (tabIndex <= m_ui->tabWidget->count()) { + m_ui->tabWidget->setCurrentIndex(--tabIndex); + } + } +} + +void MainWindow::selectLastDatabaseTab() +{ + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->count() - 1; + m_ui->tabWidget->setCurrentIndex(index); + } +} + void MainWindow::databaseTabChanged(int tabIndex) { if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 89501eff37..888b5747c1 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -122,6 +122,8 @@ private slots: void showErrorMessage(const QString& message); void selectNextDatabaseTab(); void selectPreviousDatabaseTab(); + void selectDatabaseTab(int tabIndex); + void selectLastDatabaseTab(); void togglePasswordsHidden(); void toggleUsernamesHidden(); void obtainContextFocusLock(); From 0c252b6ed4c5f54abcdbb81c7ecb124a905138f7 Mon Sep 17 00:00:00 2001 From: Julian Einwag <jeinwag@users.noreply.github.com> Date: Wed, 22 Jan 2020 19:14:49 +0100 Subject: [PATCH 034/215] add challenge-response recovery tool (see keepassxreboot/keepassxc#1734) --- utils/keepassxc-cr-recovery/.gitignore | 1 + utils/keepassxc-cr-recovery/README.md | 20 +++ utils/keepassxc-cr-recovery/go.mod | 5 + utils/keepassxc-cr-recovery/go.sum | 8 ++ utils/keepassxc-cr-recovery/main.go | 182 +++++++++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 utils/keepassxc-cr-recovery/.gitignore create mode 100644 utils/keepassxc-cr-recovery/README.md create mode 100644 utils/keepassxc-cr-recovery/go.mod create mode 100644 utils/keepassxc-cr-recovery/go.sum create mode 100644 utils/keepassxc-cr-recovery/main.go diff --git a/utils/keepassxc-cr-recovery/.gitignore b/utils/keepassxc-cr-recovery/.gitignore new file mode 100644 index 0000000000..01d743bff6 --- /dev/null +++ b/utils/keepassxc-cr-recovery/.gitignore @@ -0,0 +1 @@ +keepass-cr-recovery diff --git a/utils/keepassxc-cr-recovery/README.md b/utils/keepassxc-cr-recovery/README.md new file mode 100644 index 0000000000..d6e3fef11e --- /dev/null +++ b/utils/keepassxc-cr-recovery/README.md @@ -0,0 +1,20 @@ +# keepassxc-cr-recovery + +A small tool that helps you regain access to your KeePassXC password database in case you have it protected with YubiKey challenge-response and lost your key. +Currently supports KDBX4 databases with Argon2 hashing. + +## Building + +Tested with Go 1.13. Just run `go build`. + +## Usage + +What you need: +* your KeePassXC database +* your challenge-response secret. This cannot be retrieved from the YubiKey, it needs to be saved upon initial configuration of the key. + +Then just run +```shell +keepass-cr-recovery path-to-your-password-database path-of-the-new-keyfile +``` +It will prompt for the challenge-response secret. You will get a keyfile at the specified destination path. Then, to unlock your database in KeePassXC, you need to check "key file" instead of "challenge response" and load the file. \ No newline at end of file diff --git a/utils/keepassxc-cr-recovery/go.mod b/utils/keepassxc-cr-recovery/go.mod new file mode 100644 index 0000000000..89afe5e32d --- /dev/null +++ b/utils/keepassxc-cr-recovery/go.mod @@ -0,0 +1,5 @@ +module github.com/keepassxreboot/keepassxc/keepassxc-cr-recovery + +go 1.13 + +require golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 diff --git a/utils/keepassxc-cr-recovery/go.sum b/utils/keepassxc-cr-recovery/go.sum new file mode 100644 index 0000000000..452e5b0ad0 --- /dev/null +++ b/utils/keepassxc-cr-recovery/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/utils/keepassxc-cr-recovery/main.go b/utils/keepassxc-cr-recovery/main.go new file mode 100644 index 0000000000..b9e64d3ed0 --- /dev/null +++ b/utils/keepassxc-cr-recovery/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "syscall" + + "encoding/binary" + "encoding/hex" + + "golang.org/x/crypto/ssh/terminal" +) + +const fileVersionCriticalMask uint32 = 0xFFFF0000 +const argon2Salt = "S" +const endOfHeader = 0 +const endOfVariantMap = 0 +const kdfParameters = 11 + +func readSecret() (string, error) { + fmt.Print("Secret: ") + byteSecret, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + secret := string(byteSecret) + return secret, err + +} +func readHeaderField(reader io.Reader) (bool, byte, []byte, error) { + var fieldID byte + err := binary.Read(reader, binary.LittleEndian, &fieldID) + if err != nil { + return true, 0, nil, err + } + + if fieldID == endOfHeader { + return false, 0, nil, nil + } + + var fieldLength uint32 + err = binary.Read(reader, binary.LittleEndian, &fieldLength) + if err != nil { + return true, fieldID, nil, err + } + + fieldData := make([]byte, fieldLength) + err = binary.Read(reader, binary.LittleEndian, &fieldData) + if err != nil { + return true, fieldID, fieldData, err + } + return true, fieldID, fieldData, nil +} +func readVariantMap(reader io.Reader) ([]byte, error) { + var version uint16 + err := binary.Read(reader, binary.LittleEndian, &version) + if err != nil { + return nil, err + } + + var fieldType byte + for err = binary.Read(reader, binary.LittleEndian, &fieldType); fieldType != endOfVariantMap && err == nil; err = binary.Read(reader, binary.LittleEndian, &fieldType) { + + var nameLen uint32 + err = binary.Read(reader, binary.LittleEndian, &nameLen) + if err != nil { + return nil, err + } + + nameBytes := make([]byte, nameLen) + err = binary.Read(reader, binary.LittleEndian, &nameBytes) + if err != nil { + return nil, err + } + + name := string(nameBytes) + + var valueLen uint32 + err = binary.Read(reader, binary.LittleEndian, &valueLen) + if err != nil { + return nil, err + } + + value := make([]byte, valueLen) + err = binary.Read(reader, binary.LittleEndian, &value) + if err != nil { + return nil, err + } + + if name == argon2Salt { + return value, nil + } + } + return nil, nil +} +func readKeepassHeader(keepassFilename string) ([]byte, error) { + dbFile, err := os.Open(keepassFilename) + defer dbFile.Close() + if err != nil { + return nil, err + } + + var sig1, sig2, version uint32 + err = binary.Read(dbFile, binary.LittleEndian, &sig1) + if err != nil { + return nil, err + } + + err = binary.Read(dbFile, binary.LittleEndian, &sig2) + if err != nil { + return nil, err + } + + err = binary.Read(dbFile, binary.LittleEndian, &version) + if err != nil { + return nil, err + } + + version &= fileVersionCriticalMask + + var fieldData []byte + var fieldID byte + var moreFields bool + + for moreFields, fieldID, fieldData, err = readHeaderField(dbFile); moreFields && err == nil && fieldID != kdfParameters; moreFields, fieldID, fieldData, err = readHeaderField(dbFile) { + } + if err != nil { + return nil, err + } + + fieldReader := bytes.NewReader(fieldData) + seed, err := readVariantMap(fieldReader) + if err != nil { + return nil, err + } + return seed, nil + +} +func main() { + log.SetFlags(0) + args := os.Args + + if len(args) != 3 { + log.Fatalf("usage: %s keepassxc-database keyfile", args[0]) + } + + dbFilename := args[1] + keyFilename := args[2] + + if _, err := os.Stat(keyFilename); err == nil { + log.Fatalf("keyfile already exists, exiting") + } + secretHex, err := readSecret() + if err != nil { + log.Fatalf("couldn't read secret from stdin: %s", err) + } + secret, err := hex.DecodeString(secretHex) + + if err != nil { + log.Fatalf("couldn't decode secret: %s", err) + } + + challenge, err := readKeepassHeader(dbFilename) + if err != nil { + log.Fatalf("couldn't read challenge: %s", err) + } + + mac := hmac.New(sha1.New, secret) + mac.Write(challenge) + + hash := mac.Sum(nil) + + err = ioutil.WriteFile(keyFilename, hash, 0644) + if err != nil { + log.Fatalf("couldn't write keyfile: %s", err) + } + +} From 91755fa83ae70013b3f5e10ab19c843424df1c68 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Mon, 27 Jan 2020 20:49:52 -0500 Subject: [PATCH 035/215] Fix compile error on certain platforms --- src/cli/Info.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/Info.cpp b/src/cli/Info.cpp index 4e80b75b41..2aed1803c1 100644 --- a/src/cli/Info.cpp +++ b/src/cli/Info.cpp @@ -18,13 +18,13 @@ #include <stdio.h> #include "Info.h" +#include "Utils.h" #include "core/Database.h" +#include "core/Global.h" #include "core/Metadata.h" #include "format/KeePass2.h" -#include "Utils.h" - Info::Info() { name = QString("db-show"); From b0ad4a50d9f5bfbfa6d806a860a7569d7ee9b63b Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Mon, 27 Jan 2020 21:02:50 -0500 Subject: [PATCH 036/215] Fix GUI test failures --- tests/gui/TestGui.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 64e913c76f..8ce9b05879 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -882,6 +882,7 @@ void TestGui::testSearch() QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier); QCOMPARE(searchedEntry->password(), clipboard->text()); // Ensure Down focuses on entry view when search text is selected + QTest::keyClick(searchTextEdit, Qt::Key_A, Qt::ControlModifier); QTest::keyClick(searchTextEdit, Qt::Key_Down); QTRY_VERIFY(entryView->hasFocus()); QCOMPARE(entryView->currentEntry(), searchedEntry); From 47ce81c9a6b6c22427e239973e7168362fd657af Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Mon, 27 Jan 2020 21:28:14 -0500 Subject: [PATCH 037/215] Update FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 71d4a5e1a1..aa6a9c4381 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,5 @@ github: ["droidmonkey", "phoerious"] patreon: keepassxc +open_collective: keepassxc liberapay: keepassxc custom: ["https://keepassxc.org/donate"] From 6fc7be78eae3de21d94ff49465ac03246cd3164b Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Sat, 2 Nov 2019 11:25:13 +0200 Subject: [PATCH 038/215] Implement SSH key file path env substitution Supports all platforms, including Windows with %FOO% syntax. Fixes #3523 --- src/core/Tools.cpp | 23 +++++++++++++++++++++++ src/core/Tools.h | 3 +++ src/sshagent/KeeAgentSettings.cpp | 8 +++++++- src/sshagent/KeeAgentSettings.h | 1 + tests/TestTools.cpp | 21 +++++++++++++++++++++ tests/TestTools.h | 1 + 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 5d42bc799c..5d8889fae3 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -324,6 +324,29 @@ namespace Tools return QUuid::fromRfc4122(QByteArray::fromHex(uuid.toLatin1())); } + QString envSubstitute(const QString& filepath, QProcessEnvironment environment) + { + QString subbed = filepath; + +#if defined(Q_OS_WIN) + QRegularExpression varRe("\\%([A-Za-z][A-Za-z0-9_]*)\\%"); +#else + QRegularExpression varRe("\\$([A-Za-z][A-Za-z0-9_]*)"); + subbed.replace("~", environment.value("HOME")); +#endif + + QRegularExpressionMatch match; + + do { + match = varRe.match(subbed); + if (match.hasMatch()) { + subbed.replace(match.capturedStart(), match.capturedLength(), environment.value(match.captured(1))); + } + } while (match.hasMatch()); + + return subbed; + } + Buffer::Buffer() : raw(nullptr) , size(0) diff --git a/src/core/Tools.h b/src/core/Tools.h index 455b879c25..e56a25189e 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -22,6 +22,7 @@ #include "core/Global.h" #include <QObject> +#include <QProcessEnvironment> #include <QString> #include <QUuid> @@ -48,6 +49,8 @@ namespace Tools bool useWildcards = false, bool exactMatch = false, bool caseSensitive = false); + QString envSubstitute(const QString& filepath, + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment()); template <typename RandomAccessIterator, typename T> RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp index b914263c15..72a2f66da4 100644 --- a/src/sshagent/KeeAgentSettings.cpp +++ b/src/sshagent/KeeAgentSettings.cpp @@ -17,6 +17,7 @@ */ #include "KeeAgentSettings.h" +#include "core/Tools.h" KeeAgentSettings::KeeAgentSettings() : m_lifetimeConstraintDuration(600) @@ -115,6 +116,11 @@ const QString KeeAgentSettings::fileName() const return m_fileName; } +const QString KeeAgentSettings::fileNameEnvSubst(QProcessEnvironment environment) const +{ + return Tools::envSubstitute(m_fileName, environment); +} + void KeeAgentSettings::setAllowUseOfSshKey(bool allowUseOfSshKey) { m_allowUseOfSshKey = allowUseOfSshKey; @@ -361,7 +367,7 @@ bool KeeAgentSettings::toOpenSSHKey(const Entry* entry, OpenSSHKey& key, bool de fileName = m_attachmentName; privateKeyData = entry->attachments()->value(fileName); } else { - QFile localFile(m_fileName); + QFile localFile(fileNameEnvSubst()); QFileInfo localFileInfo(localFile); fileName = localFileInfo.fileName(); diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h index 357c695285..ed776e7412 100644 --- a/src/sshagent/KeeAgentSettings.h +++ b/src/sshagent/KeeAgentSettings.h @@ -54,6 +54,7 @@ class KeeAgentSettings const QString attachmentName() const; bool saveAttachmentToTempFile() const; const QString fileName() const; + const QString fileNameEnvSubst(QProcessEnvironment environment = QProcessEnvironment::systemEnvironment()) const; void setAllowUseOfSshKey(bool allowUseOfSshKey); void setAddAtDatabaseOpen(bool addAtDatabaseOpen); diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp index 100eb63064..4809a8bc95 100644 --- a/tests/TestTools.cpp +++ b/tests/TestTools.cpp @@ -64,3 +64,24 @@ void TestTools::testIsBase64() QVERIFY(not Tools::isBase64(QByteArray("abc_"))); QVERIFY(not Tools::isBase64(QByteArray("123"))); } + +void TestTools::testEnvSubstitute() +{ + QProcessEnvironment environment; + +#if defined(Q_OS_WIN) + environment.insert("HOMEDRIVE", "C:"); + environment.insert("HOMEPATH", "\\Users\\User"); + + QCOMPARE(Tools::envSubstitute("%HOMEDRIVE%%HOMEPATH%\\.ssh\\id_rsa", environment), + QString("C:\\Users\\User\\.ssh\\id_rsa")); + QCOMPARE(Tools::envSubstitute("start%EMPTY%%EMPTY%%%HOMEDRIVE%%end", environment), QString("start%C:%end")); +#else + environment.insert("HOME", QString("/home/user")); + environment.insert("USER", QString("user")); + + QCOMPARE(Tools::envSubstitute("~/.ssh/id_rsa", environment), QString("/home/user/.ssh/id_rsa")); + QCOMPARE(Tools::envSubstitute("$HOME/.ssh/id_rsa", environment), QString("/home/user/.ssh/id_rsa")); + QCOMPARE(Tools::envSubstitute("start/$EMPTY$$EMPTY$HOME/end", environment), QString("start/$/home/user/end")); +#endif +} diff --git a/tests/TestTools.h b/tests/TestTools.h index 56d354eca0..dd646fcc40 100644 --- a/tests/TestTools.h +++ b/tests/TestTools.h @@ -27,6 +27,7 @@ private slots: void testHumanReadableFileSize(); void testIsHex(); void testIsBase64(); + void testEnvSubstitute(); }; #endif // KEEPASSX_TESTTOOLS_H From 4dee16c9faa2a3305c3b5d09aeb320b0af06be51 Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Tue, 5 Nov 2019 21:30:34 +0200 Subject: [PATCH 039/215] SSH Agent: SSH_AUTH_SOCK override and conn test Fixes #3795 --- src/sshagent/AgentSettingsWidget.cpp | 37 ++++++++++++++++- src/sshagent/AgentSettingsWidget.ui | 61 ++++++++++++++++++++++++++++ src/sshagent/SSHAgent.cpp | 35 +++++++++++++++- src/sshagent/SSHAgent.h | 1 + 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp index be23c6906b..f95a198455 100644 --- a/src/sshagent/AgentSettingsWidget.cpp +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -17,9 +17,11 @@ */ #include "AgentSettingsWidget.h" +#include "SSHAgent.h" #include "ui_AgentSettingsWidget.h" #include "core/Config.h" +#include <QProcessEnvironment> AgentSettingsWidget::AgentSettingsWidget(QWidget* parent) : QWidget(parent) @@ -28,7 +30,13 @@ AgentSettingsWidget::AgentSettingsWidget(QWidget* parent) m_ui->setupUi(this); #ifndef Q_OS_WIN m_ui->useOpenSSHCheckBox->setVisible(false); +#else + m_ui->sshAuthSockWidget->setVisible(false); #endif + auto sshAgentEnabled = config()->get("SSHAgent", false).toBool(); + m_ui->sshAuthSockMessageWidget->setVisible(sshAgentEnabled); + m_ui->sshAuthSockMessageWidget->setCloseButtonVisible(false); + m_ui->sshAuthSockMessageWidget->setAutoHideTimeout(-1); } AgentSettingsWidget::~AgentSettingsWidget() @@ -37,15 +45,42 @@ AgentSettingsWidget::~AgentSettingsWidget() void AgentSettingsWidget::loadSettings() { - m_ui->enableSSHAgentCheckBox->setChecked(config()->get("SSHAgent", false).toBool()); + auto sshAgentEnabled = config()->get("SSHAgent", false).toBool(); + m_ui->enableSSHAgentCheckBox->setChecked(sshAgentEnabled); #ifdef Q_OS_WIN m_ui->useOpenSSHCheckBox->setChecked(config()->get("SSHAgentOpenSSH", false).toBool()); +#else + auto sshAuthSock = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); + auto sshAuthSockOverride = config()->get("SSHAuthSockOverride", "").toString(); + m_ui->sshAuthSockLabel->setText(sshAuthSock.isEmpty() ? tr("(empty)") : sshAuthSock); + m_ui->sshAuthSockOverrideEdit->setText(sshAuthSockOverride); #endif + + if (sshAgentEnabled) { + m_ui->sshAuthSockMessageWidget->setVisible(true); + +#ifndef Q_OS_WIN + if (sshAuthSock.isEmpty() && sshAuthSockOverride.isEmpty()) { + m_ui->sshAuthSockMessageWidget->showMessage( + tr("No SSH Agent socket available. Either make sure SSH_AUTH_SOCK environment variable exists or set " + "an override."), + MessageWidget::Warning); + return; + } +#endif + if (SSHAgent::instance()->testConnection()) { + m_ui->sshAuthSockMessageWidget->showMessage(tr("SSH Agent connection is working!"), + MessageWidget::Positive); + } else { + m_ui->sshAuthSockMessageWidget->showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error); + } + } } void AgentSettingsWidget::saveSettings() { config()->set("SSHAgent", m_ui->enableSSHAgentCheckBox->isChecked()); + config()->set("SSHAuthSockOverride", m_ui->sshAuthSockOverrideEdit->text()); #ifdef Q_OS_WIN config()->set("SSHAgentOpenSSH", m_ui->useOpenSSHCheckBox->isChecked()); #endif diff --git a/src/sshagent/AgentSettingsWidget.ui b/src/sshagent/AgentSettingsWidget.ui index ff7435abe0..20142f1c9d 100644 --- a/src/sshagent/AgentSettingsWidget.ui +++ b/src/sshagent/AgentSettingsWidget.ui @@ -37,6 +37,59 @@ </property> </widget> </item> + <item> + <widget class="QWidget" name="sshAuthSockWidget" native="true"> + <layout class="QGridLayout" name="sshAuthSockOverrideLayout"> + <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="4" column="0"> + <widget class="QLabel" name="sshAuthSockValueLabel"> + <property name="text"> + <string>SSH_AUTH_SOCK value</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="sshAuthSockLabel"> + <property name="font"> + <font> + <family>Monospace</family> + </font> + </property> + <property name="text"> + <string>(empty)</string> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="sshAuthSockOverrideLabel"> + <property name="text"> + <string>SSH_AUTH_SOCK override</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLineEdit" name="sshAuthSockOverrideEdit"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="MessageWidget" name="sshAuthSockMessageWidget" native="true"/> + </item> <item> <spacer name="verticalSpacer"> <property name="orientation"> @@ -52,6 +105,14 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>MessageWidget</class> + <extends>QWidget</extends> + <header>gui/MessageWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> <connections/> </ui> diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index c7c174fdc7..571f7b99f0 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -35,7 +35,10 @@ SSHAgent::SSHAgent(QObject* parent) : QObject(parent) { #ifndef Q_OS_WIN - m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); + m_socketPath = config()->get("SSHAuthSockOverride", "").toString(); + if (m_socketPath.isEmpty()) { + m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); + } #else m_socketPath = "\\\\.\\pipe\\openssh-ssh-agent"; #endif @@ -181,6 +184,36 @@ bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out) } #endif +/** + * Test if connection to SSH agent is working. + * + * @return true on success + */ +bool SSHAgent::testConnection() +{ + if (!isAgentRunning()) { + m_error = tr("No agent running, cannot test connection."); + return false; + } + + QByteArray requestData; + BinaryStream request(&requestData); + + request.write(SSH_AGENTC_REQUEST_IDENTITIES); + + QByteArray responseData; + if (!sendMessage(requestData, responseData)) { + return false; + } + + if (responseData.length() < 1 || static_cast<quint8>(responseData[0]) != SSH_AGENT_IDENTITIES_ANSWER) { + m_error = tr("Agent protocol error."); + return false; + } + + return true; +} + /** * Add the identity to the SSH agent. * diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index 940d8c5546..92389112f6 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -37,6 +37,7 @@ class SSHAgent : public QObject const QString errorString() const; bool isAgentRunning() const; + bool testConnection(); bool addIdentity(OpenSSHKey& key, KeeAgentSettings& settings); bool removeIdentity(OpenSSHKey& key); void setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove); From a41c26e9cdab5050181354c1fc3c67bc51d49a26 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 10 Nov 2019 08:34:59 -0500 Subject: [PATCH 040/215] Cleanup UI files Removes unnecessary & from strings in settings widgets. These cause confusion and complicate translation. They are unnecessary as all dialogs allow efficient tabbing between elements. Also add colons after several settings with input boxes and remove a hard stop. Improve wording of strings based on translator feedback. Fix case sensitive matching of CLI Export. --- src/browser/BrowserOptionDialog.ui | 44 +++++++++---------- src/cli/Export.cpp | 14 +++--- src/cli/Import.cpp | 2 +- .../DatabaseSettingsWidgetFdoSecrets.cpp | 3 +- .../DatabaseSettingsWidgetFdoSecrets.ui | 4 +- .../widgets/SettingsWidgetFdoSecrets.ui | 2 +- src/gui/ApplicationSettingsWidgetGeneral.ui | 16 +++---- src/gui/EditWidgetIcons.ui | 6 +-- src/gui/PasswordGeneratorWidget.ui | 2 +- .../DatabaseSettingsWidgetBrowser.ui | 6 +-- .../DatabaseSettingsWidgetGeneral.ui | 2 +- src/gui/entry/EditEntryWidget.cpp | 6 +-- src/gui/entry/EditEntryWidgetAutoType.ui | 4 +- src/gui/group/EditGroupWidgetMain.ui | 4 +- src/gui/wizard/NewDatabaseWizardPage.ui | 2 +- src/keeshare/group/EditGroupWidgetKeeShare.ui | 4 +- 16 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui index 1c00da148e..9dabde948a 100755 --- a/src/browser/BrowserOptionDialog.ui +++ b/src/browser/BrowserOptionDialog.ui @@ -110,7 +110,7 @@ <item row="0" column="0"> <widget class="QCheckBox" name="chromeSupport"> <property name="text"> - <string>&Google Chrome</string> + <string>Google Chrome</string> </property> <property name="checked"> <bool>false</bool> @@ -120,7 +120,7 @@ <item row="0" column="1"> <widget class="QCheckBox" name="firefoxSupport"> <property name="text"> - <string>&Firefox</string> + <string>Firefox</string> </property> <property name="checked"> <bool>false</bool> @@ -130,7 +130,7 @@ <item row="1" column="0"> <widget class="QCheckBox" name="chromiumSupport"> <property name="text"> - <string>&Chromium</string> + <string>Chromium</string> </property> <property name="checked"> <bool>false</bool> @@ -140,7 +140,7 @@ <item row="1" column="1"> <widget class="QCheckBox" name="vivaldiSupport"> <property name="text"> - <string>&Vivaldi</string> + <string>Vivaldi</string> </property> <property name="checked"> <bool>false</bool> @@ -150,7 +150,7 @@ <item row="0" column="2"> <widget class="QCheckBox" name="torBrowserSupport"> <property name="text"> - <string>&Tor Browser</string> + <string>Tor Browser</string> </property> <property name="checked"> <bool>false</bool> @@ -160,7 +160,7 @@ <item row="1" column="2"> <widget class="QCheckBox" name="braveSupport"> <property name="text"> - <string>&Brave</string> + <string>Brave</string> </property> <property name="checked"> <bool>false</bool> @@ -199,7 +199,7 @@ <item> <widget class="QCheckBox" name="showNotification"> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Show a &notification when credentials are requested</string> + <string extracomment="Credentials mean login data requested via browser extension">Show a notification when credentials are requested</string> </property> <property name="checked"> <bool>true</bool> @@ -209,7 +209,7 @@ <item> <widget class="QCheckBox" name="unlockDatabase"> <property name="text"> - <string>Re&quest to unlock the database if it is locked</string> + <string>Request to unlock the database if it is locked</string> </property> <property name="checked"> <bool>true</bool> @@ -222,7 +222,7 @@ <string>Only entries with the same scheme (http://, https://, ...) are returned.</string> </property> <property name="text"> - <string>&Match URL scheme (e.g., https://...)</string> + <string>Match URL scheme (e.g., https://...)</string> </property> </widget> </item> @@ -232,7 +232,7 @@ <string>Only returns the best matches for a specific URL instead of all entries for the whole domain.</string> </property> <property name="text"> - <string>&Return only best-matching credentials</string> + <string>Return only best-matching credentials</string> </property> </widget> </item> @@ -242,21 +242,21 @@ <string>Returns expired credentials. String [expired] is added to the title.</string> </property> <property name="text"> - <string>&Allow returning expired credentials.</string> + <string>Allow returning expired credentials</string> </property> </widget> </item> <item> <widget class="QRadioButton" name="sortByTitle"> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Sort &matching credentials by title</string> + <string extracomment="Credentials mean login data requested via browser extension">Sort matching credentials by title</string> </property> </widget> </item> <item> <widget class="QRadioButton" name="sortByUsername"> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Sort matching credentials by &username</string> + <string extracomment="Credentials mean login data requested via browser extension">Sort matching credentials by username</string> </property> </widget> </item> @@ -293,21 +293,21 @@ <item> <widget class="QCheckBox" name="alwaysAllowAccess"> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Never &ask before accessing credentials</string> + <string extracomment="Credentials mean login data requested via browser extension">Never ask before accessing credentials</string> </property> </widget> </item> <item> <widget class="QCheckBox" name="alwaysAllowUpdate"> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Never ask before &updating credentials</string> + <string extracomment="Credentials mean login data requested via browser extension">Never ask before updating credentials</string> </property> </widget> </item> <item> <widget class="QCheckBox" name="httpAuthPermission"> <property name="text"> - <string extracomment="An extra HTTP Basic Auth setting">Do not ask permission for HTTP &Basic Auth</string> + <string extracomment="An extra HTTP Basic Auth setting">Do not ask permission for HTTP Basic Auth</string> </property> </widget> </item> @@ -317,7 +317,7 @@ <string>All databases connected to the extension will return matching credentials.</string> </property> <property name="text"> - <string extracomment="Credentials mean login data requested via browser extension">Searc&h in all opened databases for matching credentials</string> + <string extracomment="Credentials mean login data requested via browser extension">Search in all opened databases for matching credentials</string> </property> </widget> </item> @@ -327,7 +327,7 @@ <string>Automatically creating or updating string fields is not supported.</string> </property> <property name="text"> - <string>&Return advanced string fields which start with "KPH: "</string> + <string>Return advanced string fields which start with "KPH: "</string> </property> </widget> </item> @@ -337,7 +337,7 @@ <string>Don't display the popup suggesting migration of legacy KeePassHTTP settings.</string> </property> <property name="text"> - <string>&Do not prompt for KeePassHTTP settings migration.</string> + <string>Do not prompt for KeePassHTTP settings migration.</string> </property> </widget> </item> @@ -347,7 +347,7 @@ <string>Updates KeePassXC or keepassxc-proxy binary path automatically to native messaging scripts on startup.</string> </property> <property name="text"> - <string>Update &native messaging manifest files at startup</string> + <string>Update native messaging manifest files at startup</string> </property> </widget> </item> @@ -357,7 +357,7 @@ <string>Support a proxy application between KeePassXC and browser extension.</string> </property> <property name="text"> - <string>Use a &proxy application between KeePassXC and browser extension</string> + <string>Use a proxy application between KeePassXC and browser extension</string> </property> </widget> </item> @@ -367,7 +367,7 @@ <string>Use a custom proxy location if you installed a proxy manually.</string> </property> <property name="text"> - <string comment="Meant is the proxy for KeePassXC-Browser">Use a &custom proxy location</string> + <string comment="Meant is the proxy for KeePassXC-Browser">Use a custom proxy location</string> </property> </widget> </item> diff --git a/src/cli/Export.cpp b/src/cli/Export.cpp index 8f63323d7b..e856f53325 100644 --- a/src/cli/Export.cpp +++ b/src/cli/Export.cpp @@ -25,11 +25,11 @@ #include "core/Database.h" #include "format/CsvExporter.h" -const QCommandLineOption Export::FormatOption = - QCommandLineOption(QStringList() << "f" - << "format", - QObject::tr("Format to use when exporting. Available choices are xml or csv. Defaults to xml."), - QStringLiteral("xml|csv")); +const QCommandLineOption Export::FormatOption = QCommandLineOption( + QStringList() << "f" + << "format", + QObject::tr("Format to use when exporting. Available choices are 'xml' or 'csv'. Defaults to 'xml'."), + QStringLiteral("xml|csv")); Export::Export() { @@ -44,7 +44,7 @@ int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointe TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); QString format = parser->value(Export::FormatOption); - if (format.isEmpty() || format == QStringLiteral("xml")) { + if (format.isEmpty() || format.startsWith(QStringLiteral("xml"), Qt::CaseInsensitive)) { QByteArray xmlData; QString errorMessage; if (!database->extract(xmlData, &errorMessage)) { @@ -52,7 +52,7 @@ int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointe return EXIT_FAILURE; } outputTextStream << xmlData.constData() << endl; - } else if (format == QStringLiteral("csv")) { + } else if (format.startsWith(QStringLiteral("csv"), Qt::CaseInsensitive)) { CsvExporter csvExporter; outputTextStream << csvExporter.exportDatabase(database); } else { diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index 0907f00abc..dd7b12c641 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -86,7 +86,7 @@ int Import::execute(const QStringList& arguments) db.setKey(key); if (!db.import(xmlExportPath, &errorMessage)) { - errorTextStream << QObject::tr("Unable to import XML database export %1").arg(errorMessage) << endl; + errorTextStream << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp index 16a780de7e..7c7e3abe5c 100644 --- a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp @@ -168,8 +168,7 @@ void DatabaseSettingsWidgetFdoSecrets::settingsWarning() m_ui->warningWidget->hideMessage(); } else { m_ui->groupBox->setEnabled(false); - m_ui->warningWidget->showMessage(tr("Enable fd.o Secret Service to access these settings."), - MessageWidget::Warning); + m_ui->warningWidget->showMessage(tr("Enable Secret Service to access these settings."), MessageWidget::Warning); m_ui->warningWidget->setCloseButtonVisible(false); m_ui->warningWidget->setAutoHideTimeout(-1); } diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui index 6bacb32b62..7eb21705a1 100644 --- a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui @@ -23,7 +23,7 @@ <item> <widget class="QRadioButton" name="radioDonotExpose"> <property name="text"> - <string>Don't e&xpose this database</string> + <string>Don't expose this database</string> </property> <property name="checked"> <bool>true</bool> @@ -36,7 +36,7 @@ <item> <widget class="QRadioButton" name="radioExpose"> <property name="text"> - <string>Expose entries &under this group:</string> + <string>Expose entries under this group:</string> </property> <attribute name="buttonGroup"> <string notr="true">buttonGroup</string> diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index 660181f5d1..cfbeaa210a 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -47,7 +47,7 @@ <string><html><head/><body><p>If recycle bin is enabled for the database, entries will be moved to recycle bin directly. Otherwise, they will be deleted without confirmation.</p><p>You will still be prompted if any entries are referenced by others.</p></body></html></string> </property> <property name="text"> - <string>Don't confirm when entries are deleted by clients.</string> + <string>Don't confirm when entries are deleted by clients</string> </property> </widget> </item> diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 3c6de499ab..fa4da2acc9 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -57,11 +57,11 @@ </widget> </item> <item> - <widget class="QCheckBox" name="minimizeAfterUnlockCheckBox"> - <property name="text"> - <string>Minimize window after unlocking database</string> - </property> - </widget> + <widget class="QCheckBox" name="minimizeAfterUnlockCheckBox"> + <property name="text"> + <string>Minimize window after unlocking database</string> + </property> + </widget> </item> <item> <widget class="QCheckBox" name="rememberLastDatabasesCheckBox"> @@ -806,7 +806,7 @@ <item row="1" column="0"> <widget class="QLabel" name="autoTypeShortcutLabel"> <property name="text"> - <string>Global Auto-Type shortcut</string> + <string>Global Auto-Type shortcut:</string> </property> </widget> </item> @@ -826,7 +826,7 @@ <item row="3" column="0"> <widget class="QLabel" name="autoTypeDelayLabel"> <property name="text"> - <string>Auto-Type typing delay</string> + <string>Auto-Type typing delay:</string> </property> </widget> </item> @@ -858,7 +858,7 @@ <item row="2" column="0"> <widget class="QLabel" name="autoTypeStartDelayLabel"> <property name="text"> - <string>Auto-Type start delay</string> + <string>Auto-Type start delay:</string> </property> </widget> </item> diff --git a/src/gui/EditWidgetIcons.ui b/src/gui/EditWidgetIcons.ui index 9648cca084..2d3d448179 100644 --- a/src/gui/EditWidgetIcons.ui +++ b/src/gui/EditWidgetIcons.ui @@ -26,7 +26,7 @@ <item> <widget class="QRadioButton" name="defaultIconsRadio"> <property name="text"> - <string>&Use default icon</string> + <string>Use default icon</string> </property> </widget> </item> @@ -58,7 +58,7 @@ <item> <widget class="QRadioButton" name="customIconsRadio"> <property name="text"> - <string>Use custo&m icon</string> + <string>Use custom icon</string> </property> </widget> </item> @@ -151,7 +151,7 @@ <string notr="true">padding: 4px 10px</string> </property> <property name="text"> - <string>Apply icon &to ...</string> + <string>Apply icon to...</string> </property> </widget> </item> diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index a30077015b..38af84b75c 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -1091,7 +1091,7 @@ QProgressBar::chunk { <item row="1" column="0" alignment="Qt::AlignRight"> <widget class="QLabel" name="labelWordCount"> <property name="text"> - <string>Word Co&unt:</string> + <string>Word Count:</string> </property> <property name="buddy"> <cstring>spinBoxLength</cstring> diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui index 463f572d57..34553f97d1 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.ui @@ -62,7 +62,7 @@ </sizepolicy> </property> <property name="text"> - <string>&Disconnect all browsers</string> + <string>Disconnect all browsers</string> </property> </widget> </item> @@ -75,7 +75,7 @@ </sizepolicy> </property> <property name="text"> - <string>Forg&et all site-specific settings on entries</string> + <string>Forget all site-specific settings on entries</string> </property> </widget> </item> @@ -92,7 +92,7 @@ </sizepolicy> </property> <property name="text"> - <string>Move KeePassHTTP attributes to KeePassXC-Browser &custom data</string> + <string>Move KeePassHTTP attributes to KeePassXC-Browser custom data</string> </property> </widget> </item> diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui b/src/gui/dbsettings/DatabaseSettingsWidgetGeneral.ui index 02f07952bf..29349282b9 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 &compression (recommended)</string> + <string>Enable compression (recommended)</string> </property> <property name="checked"> <bool>true</bool> diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 27dc8b10ee..2880e8ba07 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -1112,8 +1112,8 @@ void EditEntryWidget::cancel() 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) { @@ -1260,7 +1260,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/EditEntryWidgetAutoType.ui b/src/gui/entry/EditEntryWidgetAutoType.ui index d987e80475..0008a70110 100644 --- a/src/gui/entry/EditEntryWidgetAutoType.ui +++ b/src/gui/entry/EditEntryWidgetAutoType.ui @@ -49,14 +49,14 @@ <item> <widget class="QRadioButton" name="inheritSequenceButton"> <property name="text"> - <string>Inherit default Auto-Type sequence from the &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>&Use custom Auto-Type sequence:</string> + <string>Use custom Auto-Type sequence:</string> </property> </widget> </item> diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index 486e408b6b..f23e43f19f 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -117,14 +117,14 @@ <item row="5" column="1"> <widget class="QRadioButton" name="autoTypeSequenceInherit"> <property name="text"> - <string>&Use default Auto-Type sequence of parent group</string> + <string>Use default Auto-Type sequence of parent group</string> </property> </widget> </item> <item row="6" column="1"> <widget class="QRadioButton" name="autoTypeSequenceCustomRadio"> <property name="text"> - <string>Set default Auto-Type se&quence</string> + <string>Set default Auto-Type sequence</string> </property> </widget> </item> diff --git a/src/gui/wizard/NewDatabaseWizardPage.ui b/src/gui/wizard/NewDatabaseWizardPage.ui index 6b69e85b5e..e920b26ed5 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&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/keeshare/group/EditGroupWidgetKeeShare.ui b/src/keeshare/group/EditGroupWidgetKeeShare.ui index b64195c64f..ad3f6dbe47 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.ui +++ b/src/keeshare/group/EditGroupWidgetKeeShare.ui @@ -64,10 +64,10 @@ <item> <widget class="QToolButton" name="pathSelectionButton"> <property name="accessibleName"> - <string>Browser for share file</string> + <string>Browse for share file</string> </property> <property name="text"> - <string>...</string> + <string>Browse...</string> </property> </widget> </item> From 0b6d9cb4725d059ae8150c3eaa32268dd52d0f5f Mon Sep 17 00:00:00 2001 From: louib <L0U13@protonmail.com> Date: Mon, 6 Jan 2020 21:00:39 -0500 Subject: [PATCH 041/215] CLI: set decryption time on create. Added an option to set the target decryption time on database creation for the CLI create command. This required some refactoring, in particular the extraction of the min, max and defaut decryption times in the `Kdf` module. Some work was done to allow changing those constant only in the `Kdf` module, should we ever want to change them. --- share/docs/man/keepassxc-cli.1 | 8 ++- src/cli/Create.cpp | 49 +++++++++++++++++-- src/cli/Create.h | 2 + src/crypto/kdf/Kdf.h | 13 +++++ .../DatabaseSettingsWidgetEncryption.cpp | 25 +++++++--- .../DatabaseSettingsWidgetEncryption.h | 6 ++- .../DatabaseSettingsWidgetEncryption.ui | 17 ++----- tests/TestCli.cpp | 42 ++++++++++++++++ 8 files changed, 138 insertions(+), 24 deletions(-) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index cba3ddb75b..d7ab9cdd75 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -1,4 +1,4 @@ -.TH KEEPASSXC-CLI 1 "June 15, 2019" +.TH KEEPASSXC-CLI 1 "Jan 04, 2020" .SH NAME keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager. @@ -179,6 +179,12 @@ Copies the current TOTP instead of current password to clipboard. Will report an error if no TOTP is configured for the entry. +.SS "Create options" + +.IP "\fB-t\fP, \fB--decryption-time\fP <time>" +Target decryption time in MS for the database. + + .SS "Show options" .IP "\fB-a\fP, \fB--attributes\fP <attribute>..." diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index fbdebaf477..b65970facc 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -30,12 +30,19 @@ #include "keys/CompositeKey.h" #include "keys/Key.h" +const QCommandLineOption Create::DecryptionTimeOption = + QCommandLineOption(QStringList() << "t" + << "decryption-time", + QObject::tr("Target decryption time in MS for the database."), + QObject::tr("time")); + Create::Create() { name = QString("create"); description = QObject::tr("Create a new database."); positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")}); options.append(Command::KeyFileOption); + options.append(Create::DecryptionTimeOption); } /** @@ -53,14 +60,16 @@ Create::Create() */ int Create::execute(const QStringList& arguments) { - QTextStream out(Utils::STDOUT, QIODevice::WriteOnly); - QTextStream err(Utils::STDERR, QIODevice::WriteOnly); - QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments); if (parser.isNull()) { return EXIT_FAILURE; } + bool quiet = parser->isSet(Command::QuietOption); + + QTextStream out(quiet ? Utils::DEVNULL : Utils::STDOUT, QIODevice::WriteOnly); + QTextStream err(Utils::STDERR, QIODevice::WriteOnly); + const QStringList args = parser->positionalArguments(); const QString& databaseFilename = args.at(0); @@ -69,6 +78,23 @@ int Create::execute(const QStringList& arguments) return EXIT_FAILURE; } + // Validate the decryption time before asking for a password. + QString decryptionTimeValue = parser->value(Create::DecryptionTimeOption); + int decryptionTime = 0; + if (decryptionTimeValue.length() != 0) { + decryptionTime = decryptionTimeValue.toInt(); + if (decryptionTime <= 0) { + err << QObject::tr("Invalid decryption time %1.").arg(decryptionTimeValue) << endl; + return EXIT_FAILURE; + } + if (decryptionTime < Kdf::MIN_ENCRYPTION_TIME || decryptionTime > Kdf::MAX_ENCRYPTION_TIME) { + err << QObject::tr("Target decryption time must be between %1 and %2.") + .arg(QString::number(Kdf::MIN_ENCRYPTION_TIME), QString::number(Kdf::MAX_ENCRYPTION_TIME)) + << endl; + return EXIT_FAILURE; + } + } + auto key = QSharedPointer<CompositeKey>::create(); auto password = Utils::getPasswordFromStdin(); @@ -96,6 +122,23 @@ int Create::execute(const QStringList& arguments) QSharedPointer<Database> db(new Database); db->setKey(key); + if (decryptionTime != 0) { + auto kdf = db->kdf(); + Q_ASSERT(kdf); + + out << QObject::tr("Benchmarking key derivation function for %1ms delay.").arg(decryptionTimeValue) << endl; + int rounds = kdf->benchmark(decryptionTime); + out << QObject::tr("Setting %1 rounds for key derivation function.").arg(QString::number(rounds)) << endl; + kdf->setRounds(rounds); + + bool ok = db->changeKdf(kdf); + + if (!ok) { + err << QObject::tr("Error while setting database key derivation settings.") << endl; + return EXIT_FAILURE; + } + } + QString errorMessage; if (!db->saveAs(databaseFilename, &errorMessage, true, false)) { err << QObject::tr("Failed to save the database: %1.").arg(errorMessage) << endl; diff --git a/src/cli/Create.h b/src/cli/Create.h index 1c5696a6e8..47e2e34ab9 100644 --- a/src/cli/Create.h +++ b/src/cli/Create.h @@ -28,6 +28,8 @@ class Create : public Command Create(); int execute(const QStringList& arguments) override; + static const QCommandLineOption DecryptionTimeOption; + private: bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey); }; diff --git a/src/crypto/kdf/Kdf.h b/src/crypto/kdf/Kdf.h index 4e6455eded..da9a2526cb 100644 --- a/src/crypto/kdf/Kdf.h +++ b/src/crypto/kdf/Kdf.h @@ -48,6 +48,19 @@ class Kdf int benchmark(int msec) const; + /* + * Default target encryption time, in MS. + */ + static const int DEFAULT_ENCRYPTION_TIME = 1000; + /* + * Minimum target encryption time, in MS. + */ + static const int MIN_ENCRYPTION_TIME = 100; + /* + * Maximum target encryption time, in MS. + */ + static const int MAX_ENCRYPTION_TIME = 5000; + protected: virtual int benchmarkImpl(int msec) const = 0; diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp index e2a8cdafe8..cc57e453af 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 986a33b6ae..69388da6b8 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 f8ba579dc7..1e0bb383f6 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> diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index d73cdedddb..bc96de9740 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -608,6 +608,48 @@ void TestCli::testCreate() auto db3 = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename3, true, keyfilePath, "", Utils::DEVNULL)); QVERIFY(db3); + + // Invalid decryption time (format). + QString databaseFilename4 = testDir->path() + "/testCreate4.kdbx"; + pos = m_stdoutFile->pos(); + errPos = m_stderrFile->pos(); + createCmd.execute({"create", databaseFilename4, "-t", "NAN"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE(m_stderrFile->readAll(), QByteArray("Invalid decryption time NAN.\n")); + + // Invalid decryption time (range). + pos = m_stdoutFile->pos(); + errPos = m_stderrFile->pos(); + createCmd.execute({"create", databaseFilename4, "-t", "10"}); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QVERIFY(m_stderrFile->readAll().contains(QByteArray("Target decryption time must be between"))); + + int encryptionTime = 500; + // Custom encryption time + pos = m_stdoutFile->pos(); + errPos = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + int epochBefore = QDateTime::currentMSecsSinceEpoch(); + createCmd.execute({"create", databaseFilename4, "-t", QString::number(encryptionTime)}); + // Removing 100ms to make sure we account for changes in computation time. + QVERIFY(QDateTime::currentMSecsSinceEpoch() > (epochBefore + encryptionTime - 100)); + m_stdoutFile->seek(pos); + m_stderrFile->seek(errPos); + + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Benchmarking key derivation function for 500ms delay.\n")); + QVERIFY(m_stdoutFile->readLine().contains(QByteArray("rounds for key derivation function.\n"))); + + Utils::Test::setNextPassword("a"); + auto db4 = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilename4, true, "", "", Utils::DEVNULL)); + QVERIFY(db4); } void TestCli::testInfo() From f170022fa305d1e948716d5e1694ddc9af8943d6 Mon Sep 17 00:00:00 2001 From: Shun Sakai <sorairolake@protonmail.ch> Date: Wed, 22 Jan 2020 22:57:48 +0900 Subject: [PATCH 042/215] Remove Boost Software License 1.0 `cmake/GetGitRevisionDescription.cmake*` were removed at commit 21d1e981. So, remove the LICENSE file. --- COPYING | 4 ---- LICENSE.BOOST-1.0 | 23 ----------------------- 2 files changed, 27 deletions(-) delete mode 100644 LICENSE.BOOST-1.0 diff --git a/COPYING b/COPYING index ee8c24f211..cbba43f349 100644 --- a/COPYING +++ b/COPYING @@ -216,10 +216,6 @@ Files: src/streams/qtiocompressor.* Copyright: 2009-2012, Nokia Corporation and/or its subsidiary(-ies) License: LGPL-2.1 or GPL-3 -Files: cmake/GetGitRevisionDescription.cmake* -Copyright: 2009-2010, Iowa State University -License: Boost-1.0 - Files: src/zxcvbn/zxcvbn.* Copyright: 2015-2017, Tony Evans License: MIT diff --git a/LICENSE.BOOST-1.0 b/LICENSE.BOOST-1.0 deleted file mode 100644 index 36b7cd93cd..0000000000 --- a/LICENSE.BOOST-1.0 +++ /dev/null @@ -1,23 +0,0 @@ -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -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, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. From 4968d95cabc2b6646dc498bb47a6d22c613307d9 Mon Sep 17 00:00:00 2001 From: Shun Sakai <sorairolake@protonmail.ch> Date: Wed, 22 Jan 2020 23:41:21 +0900 Subject: [PATCH 043/215] Cleanup COPYING - `cmake/GNUInstallDirs.cmake` was removed at commit ef3c2dae. So, remove description related to this. - Fix typo at commit c2ead0e2. --- COPYING | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/COPYING b/COPYING index cbba43f349..9202a00235 100644 --- a/COPYING +++ b/COPYING @@ -38,11 +38,6 @@ Comment: The "KeePassXC Team" in every copyright notice is formed by the followi - weslly Every other contributor is listed on https://github.com/keepassxreboot/keepassxc/graphs/contributors -Files: cmake/GNUInstallDirs.cmake -Copyright: 2011 Nikita Krupen'ko <krnekit@gmail.com> - 2011 Kitware, Inc. -License: BSD-3-clause - Files: cmake/CodeCoverage.cmake Copyright: 2012 - 2015, Lars Bilke License: BSD-3-clause @@ -226,7 +221,7 @@ Copyright: 2011 Aurélien Gâteau <agateau@kde.org> 2014 Dominik Haumann <dhaumann@kde.org> License: LGPL-2.1 -Files: share/macosx/dmg-background.tiff +Files: share/macosx/background.tiff Copyright: 2008-2014, Andrey Tarantsov License: MIT From d2a19f3e86b5640ef7d47ab979b5f9cec28c3ad0 Mon Sep 17 00:00:00 2001 From: Shun Sakai <sorairolake@protonmail.ch> Date: Thu, 23 Jan 2020 00:01:53 +0900 Subject: [PATCH 044/215] Add OFL-1.1 text The LICENSE text was not added at commit 36f92b76, so add it. --- LICENSE.OFL | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 LICENSE.OFL diff --git a/LICENSE.OFL b/LICENSE.OFL new file mode 100644 index 0000000000..244ef81c34 --- /dev/null +++ b/LICENSE.OFL @@ -0,0 +1,96 @@ +Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), +with Reserved Font Name Material Design Icons. +Copyright (c) 2014, Google (http://www.google.com/design/) +uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. From 94b45ea16fcf149adc7240b35a91d811b247ec20 Mon Sep 17 00:00:00 2001 From: James Ring <sjr@jdns.org> Date: Wed, 29 Jan 2020 12:18:48 -0800 Subject: [PATCH 045/215] Use db-create as Create::name (#4263) Fixes a name mismatch introduced in b78ca924fdf2b6d5937d87bab65104cba629382f. --- src/cli/Create.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index b65970facc..c8e3b771fc 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -38,7 +38,7 @@ const QCommandLineOption Create::DecryptionTimeOption = Create::Create() { - name = QString("create"); + name = QString("db-create"); description = QObject::tr("Create a new database."); positionalArguments.append({QString("database"), QObject::tr("Path of the database."), QString("")}); options.append(Command::KeyFileOption); From 97b034dbcbd4e3758a1dcbcb1be71cd9da1128fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= <wolfram@roesler-ac.de> Date: Sat, 11 Jan 2020 19:52:47 +0100 Subject: [PATCH 046/215] Ignore system icon theme, always use our own icons With the Material Design icons, any other icons brought in through the system icon theme will look inconsistent. Also remove the KEEPASSXC_IGNORE_ICON_THEME environment variable (which was introduced during development of the new icons to disable the system icon theme and is thus no longer needed). Fixes #475 --- src/core/FilePath.cpp | 6 +----- src/core/FilePath.h | 2 +- src/fdosecrets/widgets/SettingsModels.cpp | 4 ++-- src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp | 7 +++---- src/gui/KMessageWidget.cpp | 2 +- src/gui/LineEdit.cpp | 11 +---------- src/gui/masterkey/PasswordEditWidget.cpp | 2 +- src/keeshare/group/EditGroupWidgetKeeShare.cpp | 2 +- utils/makeicons.sh | 7 ++----- 9 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 2725f4671d..62db3929d6 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -133,7 +133,7 @@ QIcon FilePath::trayIconUnlocked() #endif } -QIcon FilePath::icon(const QString& category, const QString& name, bool fromTheme) +QIcon FilePath::icon(const QString& category, const QString& name) { QString combinedName = category + "/" + name; @@ -143,10 +143,6 @@ QIcon FilePath::icon(const QString& category, const QString& name, bool fromThem return icon; } - if (fromTheme && !getenv("KEEPASSXC_IGNORE_ICON_THEME")) { - icon = QIcon::fromTheme(name); - } - if (icon.isNull()) { const QList<int> pngSizes = {16, 22, 24, 32, 48, 64, 128}; QString filename; diff --git a/src/core/FilePath.h b/src/core/FilePath.h index b304b5f141..ceb9582378 100644 --- a/src/core/FilePath.h +++ b/src/core/FilePath.h @@ -32,7 +32,7 @@ class FilePath QIcon trayIcon(); QIcon trayIconLocked(); QIcon trayIconUnlocked(); - QIcon icon(const QString& category, const QString& name, bool fromTheme = true); + QIcon icon(const QString& category, const QString& name); QIcon onOffIcon(const QString& category, const QString& name); static FilePath* instance(); diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index edcb275c8b..2921182c5f 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -130,7 +130,7 @@ namespace FdoSecrets case Qt::DisplayRole: return tr("Unlock to show"); case Qt::DecorationRole: - return filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked"), true); + return filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked")); case Qt::FontRole: { QFont font; font.setItalic(true); @@ -165,7 +165,7 @@ namespace FdoSecrets case Qt::DisplayRole: return tr("None"); case Qt::DecorationRole: - return filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none"), true); + return filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none")); default: return {}; } diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 59399cdec0..5ae267a122 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -75,7 +75,7 @@ namespace // unlock/lock m_lockAct = new QAction(tr("Unlock database"), this); - m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), false)); + m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); connect(m_lockAct, &QAction::triggered, this, [this]() { if (!m_dbWidget) { @@ -133,14 +133,13 @@ namespace } connect(m_dbWidget, &DatabaseWidget::databaseLocked, this, [this]() { m_lockAct->setText(tr("Unlock database")); - m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), false)); + m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); m_dbSettingsAct->setEnabled(false); }); connect(m_dbWidget, &DatabaseWidget::databaseUnlocked, this, [this]() { m_lockAct->setText(tr("Lock database")); - m_lockAct->setIcon( - filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"), false)); + m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"))); m_lockAct->setToolTip(tr("Lock database")); m_dbSettingsAct->setEnabled(true); }); diff --git a/src/gui/KMessageWidget.cpp b/src/gui/KMessageWidget.cpp index 80f302858a..8df7b63846 100644 --- a/src/gui/KMessageWidget.cpp +++ b/src/gui/KMessageWidget.cpp @@ -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(FilePath::instance()->icon("actions", "message-close")); QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide())); diff --git a/src/gui/LineEdit.cpp b/src/gui/LineEdit.cpp index 654f9a3a7b..98a5c09e86 100644 --- a/src/gui/LineEdit.cpp +++ b/src/gui/LineEdit.cpp @@ -33,16 +33,7 @@ LineEdit::LineEdit(QWidget* parent) QString iconNameDirected = QString("edit-clear-locationbar-").append((layoutDirection() == Qt::LeftToRight) ? "rtl" : "ltr"); - QIcon icon; - if (!getenv("KEEPASSXC_IGNORE_ICON_THEME")) { - icon = QIcon::fromTheme(iconNameDirected); - if (icon.isNull()) { - icon = QIcon::fromTheme("edit-clear"); - } - } - if (icon.isNull()) { - icon = filePath()->icon("actions", iconNameDirected); - } + const auto icon = filePath()->icon("actions", iconNameDirected); m_clearButton->setIcon(icon); m_clearButton->setCursor(Qt::ArrowCursor); diff --git a/src/gui/masterkey/PasswordEditWidget.cpp b/src/gui/masterkey/PasswordEditWidget.cpp index 9353cbe7a2..96b5fd3056 100644 --- a/src/gui/masterkey/PasswordEditWidget.cpp +++ b/src/gui/masterkey/PasswordEditWidget.cpp @@ -74,7 +74,7 @@ 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->passwordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator")); m_compUi->repeatPasswordEdit->enableVerifyMode(m_compUi->enterPasswordEdit); connect(m_compUi->togglePasswordButton, diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 5df9f13ee7..43f32b3436 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -37,7 +37,7 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) m_ui->setupUi(this); m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); - m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); + m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator")); m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); m_ui->passwordGenerator->hide(); diff --git a/utils/makeicons.sh b/utils/makeicons.sh index 8ce4354769..6efc608eed 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -32,12 +32,9 @@ # 3. Create the icons: # $ bash ../../utils/makeicons.sh ~/src/MaterialDesign # -# 4. Re-build KeePassXC: +# 4. Re-build and run KeePassXC: # $ cd ~/keepassxc/build -# $ make keepassxc -# -# 5. Check icons by disabling the OS icon theme: -# $ KEEPASSXC_IGNORE_ICON_THEME=1 src/keepassxc +# $ make keepassxc && src/keepassxc # # Material icons: https://materialdesignicons.com/ From 7d8072bf8fa957ecb4f7359d6ec1737c206f7201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Lindh=C3=A9?= <andreas@lindhe.io> Date: Sun, 19 Jan 2020 17:19:25 +0100 Subject: [PATCH 047/215] Use entry action icons with circle frame --- share/icons/application/scalable/actions/entry-clone.svg | 2 +- share/icons/application/scalable/actions/entry-delete.svg | 2 +- share/icons/application/scalable/actions/entry-edit.svg | 2 +- share/icons/application/scalable/actions/entry-new.svg | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/share/icons/application/scalable/actions/entry-clone.svg b/share/icons/application/scalable/actions/entry-clone.svg index 1886d76211..001d982425 100644 --- a/share/icons/application/scalable/actions/entry-clone.svg +++ b/share/icons/application/scalable/actions/entry-clone.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-comment-multiple-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M12,23A1,1 0 0,1 11,22V19H7A2,2 0 0,1 5,17V7C5,5.89 5.9,5 7,5H21A2,2 0 0,1 23,7V17A2,2 0 0,1 21,19H16.9L13.2,22.71C13,22.9 12.75,23 12.5,23V23H12M13,17V20.08L16.08,17H21V7H7V17H13M3,15H1V3A2,2 0 0,1 3,1H19V3H3V15Z" /></svg> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,8H14V11H11V13H14V16H16V13H19V11H16M2,12C2,9.21 3.64,6.8 6,5.68V3.5C2.5,4.76 0,8.09 0,12C0,15.91 2.5,19.24 6,20.5V18.32C3.64,17.2 2,14.79 2,12M15,3C10.04,3 6,7.04 6,12C6,16.96 10.04,21 15,21C19.96,21 24,16.96 24,12C24,7.04 19.96,3 15,3M15,19C11.14,19 8,15.86 8,12C8,8.14 11.14,5 15,5C18.86,5 22,8.14 22,12C22,15.86 18.86,19 15,19Z" /></svg> \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-delete.svg b/share/icons/application/scalable/actions/entry-delete.svg index 1e42b6dafc..dad58cf89b 100644 --- a/share/icons/application/scalable/actions/entry-delete.svg +++ b/share/icons/application/scalable/actions/entry-delete.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-comment-remove-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M9.41,6L12,8.59L14.59,6L16,7.41L13.41,10L16,12.59L14.59,14L12,11.41L9.41,14L8,12.59L10.59,10L8,7.41L9.41,6Z" /></svg> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z" /></svg> \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-edit.svg b/share/icons/application/scalable/actions/entry-edit.svg index 82b72366f4..6e0b0afa87 100644 --- a/share/icons/application/scalable/actions/entry-edit.svg +++ b/share/icons/application/scalable/actions/entry-edit.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-comment-edit-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M9 22C8.45 22 8 21.55 8 21V18H4C2.9 18 2 17.11 2 16V4C2 2.89 2.9 2 4 2H20C21.11 2 22 2.9 22 4V16C22 17.11 21.11 18 20 18H13.9L10.2 21.71C10 21.9 9.75 22 9.5 22H9M10 16V19.08L13.08 16H20V4H4V16H10M15.84 8.2L14.83 9.21L12.76 7.18L13.77 6.16C13.97 5.95 14.31 5.94 14.55 6.16L15.84 7.41C16.05 7.62 16.06 7.96 15.84 8.2M8 11.91L12.17 7.72L14.24 9.8L10.08 14H8V11.91Z" /></svg> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7,14.94L13.06,8.88L15.12,10.94L9.06,17H7V14.94M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M16.7,9.35L15.7,10.35L13.65,8.3L14.65,7.3C14.86,7.08 15.21,7.08 15.42,7.3L16.7,8.58C16.92,8.79 16.92,9.14 16.7,9.35M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2" /></svg> \ No newline at end of file diff --git a/share/icons/application/scalable/actions/entry-new.svg b/share/icons/application/scalable/actions/entry-new.svg index d04166069d..3fe6bec7eb 100644 --- a/share/icons/application/scalable/actions/entry-new.svg +++ b/share/icons/application/scalable/actions/entry-new.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-comment-plus-outline" width="24" height="24" viewBox="0 0 24 24"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M11,6H13V9H16V11H13V14H11V11H8V9H11V6Z" /></svg> \ No newline at end of file +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z" /></svg> \ No newline at end of file From 0383aa104c350ae0125e9aa6c598b46e445973ae Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 5 Jan 2020 12:07:18 -0500 Subject: [PATCH 048/215] Improvements to confirm access dialog * Disable access to entries immediately within the dialog * Use checkboxes instead of row selection * Add button to deny all access immediately --- src/browser/BrowserAccessControlDialog.cpp | 71 ++++++++++++++++----- src/browser/BrowserAccessControlDialog.h | 14 ++-- src/browser/BrowserAccessControlDialog.ui | 68 +++++++++++++++----- src/browser/BrowserService.cpp | 74 ++++++++++++---------- src/browser/BrowserService.h | 12 ++-- 5 files changed, 162 insertions(+), 77 deletions(-) diff --git a/src/browser/BrowserAccessControlDialog.cpp b/src/browser/BrowserAccessControlDialog.cpp index 2571610eb3..f73fe04c22 100644 --- a/src/browser/BrowserAccessControlDialog.cpp +++ b/src/browser/BrowserAccessControlDialog.cpp @@ -25,29 +25,54 @@ BrowserAccessControlDialog::BrowserAccessControlDialog(QWidget* parent) : QDialog(parent) , m_ui(new Ui::BrowserAccessControlDialog()) { - this->setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); m_ui->setupUi(this); - connect(m_ui->allowButton, SIGNAL(clicked()), this, SLOT(accept())); - connect(m_ui->denyButton, SIGNAL(clicked()), this, SLOT(reject())); + + connect(m_ui->allowButton, SIGNAL(clicked()), SLOT(accept())); + connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject())); } BrowserAccessControlDialog::~BrowserAccessControlDialog() { } -void BrowserAccessControlDialog::setUrl(const QString& url) +void BrowserAccessControlDialog::setItems(const QList<Entry*>& items, const QString& hostname, bool httpAuth) { - m_ui->label->setText(QString(tr("%1 has requested access to passwords for the following item(s).\n" - "Please select whether you want to allow access.")) - .arg(QUrl(url).host())); -} + m_ui->siteLabel->setText(m_ui->siteLabel->text().arg(hostname)); -void BrowserAccessControlDialog::setItems(const QList<Entry*>& items) -{ - for (Entry* entry : items) { - m_ui->itemsList->addItem(entry->title() + " - " + entry->username()); + m_ui->rememberDecisionCheckBox->setVisible(!httpAuth); + m_ui->rememberDecisionCheckBox->setChecked(false); + + m_ui->itemsTable->setRowCount(items.count()); + m_ui->itemsTable->setColumnCount(2); + + int row = 0; + for (const auto& entry : items) { + auto item = new QTableWidgetItem(); + item->setText(entry->title() + " - " + entry->username()); + item->setData(Qt::UserRole, row); + item->setCheckState(Qt::Checked); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + m_ui->itemsTable->setItem(row, 0, item); + + auto disableButton = new QPushButton(tr("Disable for this site")); + connect(disableButton, &QAbstractButton::pressed, [&, item] { + emit disableAccess(item); + m_ui->itemsTable->removeRow(item->row()); + if (m_ui->itemsTable->rowCount() == 0) { + reject(); + } + }); + m_ui->itemsTable->setCellWidget(row, 1, disableButton); + + ++row; } + + m_ui->itemsTable->resizeColumnsToContents(); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + + m_ui->allowButton->setFocus(); } bool BrowserAccessControlDialog::remember() const @@ -55,12 +80,26 @@ bool BrowserAccessControlDialog::remember() const return m_ui->rememberDecisionCheckBox->isChecked(); } -void BrowserAccessControlDialog::setRemember(bool r) +QList<QTableWidgetItem*> BrowserAccessControlDialog::getSelectedEntries() const { - m_ui->rememberDecisionCheckBox->setChecked(r); + QList<QTableWidgetItem*> selected; + for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) { + auto item = m_ui->itemsTable->item(i, 0); + if (item->checkState() == Qt::Checked) { + selected.append(item); + } + } + return selected; } -void BrowserAccessControlDialog::setHTTPAuth(bool httpAuth) +QList<QTableWidgetItem*> BrowserAccessControlDialog::getNonSelectedEntries() const { - m_ui->rememberDecisionCheckBox->setVisible(!httpAuth); + QList<QTableWidgetItem*> notSelected; + for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) { + auto item = m_ui->itemsTable->item(i, 0); + if (item->checkState() != Qt::Checked) { + notSelected.append(item); + } + } + return notSelected; } diff --git a/src/browser/BrowserAccessControlDialog.h b/src/browser/BrowserAccessControlDialog.h index 79aba9c4bb..1d42cf5096 100644 --- a/src/browser/BrowserAccessControlDialog.h +++ b/src/browser/BrowserAccessControlDialog.h @@ -21,6 +21,7 @@ #include <QDialog> #include <QScopedPointer> +#include <QTableWidgetItem> class Entry; @@ -35,13 +36,16 @@ class BrowserAccessControlDialog : public QDialog public: explicit BrowserAccessControlDialog(QWidget* parent = nullptr); - ~BrowserAccessControlDialog(); + ~BrowserAccessControlDialog() override; - void setUrl(const QString& url); - void setItems(const QList<Entry*>& items); + void setItems(const QList<Entry*>& items, const QString& hostname, bool httpAuth); bool remember() const; - void setRemember(bool r); - void setHTTPAuth(bool httpAuth); + + QList<QTableWidgetItem*> getSelectedEntries() const; + QList<QTableWidgetItem*> getNonSelectedEntries() const; + +signals: + void disableAccess(QTableWidgetItem* item); private: QScopedPointer<Ui::BrowserAccessControlDialog> m_ui; diff --git a/src/browser/BrowserAccessControlDialog.ui b/src/browser/BrowserAccessControlDialog.ui index 55914bfec4..a9bec9086c 100755 --- a/src/browser/BrowserAccessControlDialog.ui +++ b/src/browser/BrowserAccessControlDialog.ui @@ -6,29 +6,50 @@ <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>221</height> + <width>405</width> + <height>200</height> </rect> </property> <property name="windowTitle"> - <string>KeePassXC-Browser Confirm Access</string> + <string>KeePassXC - Browser Access Request</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QLabel" name="label"> + <widget class="QLabel" name="siteLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> <property name="text"> - <string/> + <string>%1 is requesting access to the following entries:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> </property> </widget> </item> <item> - <widget class="QListWidget" name="itemsList"/> - </item> - <item> - <widget class="QCheckBox" name="rememberDecisionCheckBox"> - <property name="text"> - <string>Remember this decision</string> + <widget class="QTableWidget" name="itemsTable"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::NoSelection</enum> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <attribute name="horizontalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> </widget> </item> <item> @@ -47,22 +68,35 @@ </spacer> </item> <item> - <widget class="QPushButton" name="allowButton"> + <widget class="QCheckBox" name="rememberDecisionCheckBox"> + <property name="toolTip"> + <string>Remember access to checked entries</string> + </property> <property name="accessibleName"> - <string>Allow access</string> + <string>Remember access to checked entries</string> </property> <property name="text"> - <string>Allow</string> + <string>Remember</string> </property> </widget> </item> <item> - <widget class="QPushButton" name="denyButton"> + <widget class="QPushButton" name="allowButton"> <property name="accessibleName"> - <string>Deny access</string> + <string>Allow access to entries</string> + </property> + <property name="text"> + <string>Allow Selected</string> </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="cancelButton"> <property name="text"> - <string>Deny</string> + <string>Deny All</string> + </property> + <property name="autoDefault"> + <bool>false</bool> </property> </widget> </item> diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 42def203a2..b182b535ea 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -417,8 +417,9 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, } // Confirm entries - if (confirmEntries(pwEntriesToConfirm, url, host, submitUrl, realm, httpAuth)) { - pwEntries.append(pwEntriesToConfirm); + QList<Entry*> selectedEntriesToConfirm = confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm, httpAuth); + if (!selectedEntriesToConfirm.isEmpty()) { + pwEntries.append(selectedEntriesToConfirm); } if (pwEntries.isEmpty()) { @@ -788,59 +789,66 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QStrin return results; } -bool BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, - const QString& url, - const QString& host, - const QString& submitUrl, - const QString& realm, - const bool httpAuth) +QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm, + const QString& url, + const QString& host, + const QString& submitHost, + const QString& realm, + const bool httpAuth) { if (pwEntriesToConfirm.isEmpty() || m_dialogActive) { - return false; + return {}; } m_dialogActive = true; BrowserAccessControlDialog accessControlDialog; + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), &accessControlDialog, SLOT(reject())); - accessControlDialog.setUrl(!submitUrl.isEmpty() ? submitUrl : url); - accessControlDialog.setItems(pwEntriesToConfirm); - accessControlDialog.setHTTPAuth(httpAuth); + connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) { + auto entry = pwEntriesToConfirm[item->row()]; + BrowserEntryConfig config; + config.load(entry); + config.deny(host); + if (!submitHost.isEmpty() && host != submitHost) { + config.deny(submitHost); + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); + }); + + accessControlDialog.setItems(pwEntriesToConfirm, !submitHost.isEmpty() ? submitHost : url, httpAuth); raiseWindow(); accessControlDialog.show(); accessControlDialog.activateWindow(); accessControlDialog.raise(); - const QString submitHost = QUrl(submitUrl).host(); - int res = accessControlDialog.exec(); - if (accessControlDialog.remember()) { - for (auto* entry : pwEntriesToConfirm) { - BrowserEntryConfig config; - config.load(entry); - if (res == QDialog::Accepted) { + QList<Entry*> allowedEntries; + if (accessControlDialog.exec() == QDialog::Accepted) { + const auto selectedEntries = accessControlDialog.getSelectedEntries(); + for (auto item : accessControlDialog.getSelectedEntries()) { + auto entry = pwEntriesToConfirm[item->row()]; + if (accessControlDialog.remember()) { + BrowserEntryConfig config; + config.load(entry); config.allow(host); - if (!submitHost.isEmpty() && host != submitHost) - config.allow(submitHost); - } else if (res == QDialog::Rejected) { - config.deny(host); if (!submitHost.isEmpty() && host != submitHost) { - config.deny(submitHost); + config.allow(submitHost); } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); } - if (!realm.isEmpty()) { - config.setRealm(realm); - } - config.save(entry); + allowedEntries.append(entry); } } m_dialogActive = false; hideWindow(); - if (res == QDialog::Accepted) { - return true; - } - - return false; + return allowedEntries; } QJsonObject BrowserService::prepareEntry(const Entry* entry) diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 495c9ac258..3157df61f4 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -118,12 +118,12 @@ public slots: private: QList<Entry*> sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& submitUrl); - bool confirmEntries(QList<Entry*>& pwEntriesToConfirm, - const QString& url, - const QString& host, - const QString& submitUrl, - const QString& realm, - const bool httpAuth); + QList<Entry*> confirmEntries(QList<Entry*>& pwEntriesToConfirm, + const QString& url, + const QString& host, + const QString& submitUrl, + const QString& realm, + const bool httpAuth); QJsonObject prepareEntry(const Entry* entry); Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm); Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {}); From 792c1c94f7f0569af0682b3e73248c5c0c52d3e9 Mon Sep 17 00:00:00 2001 From: Aetf <aetf@unlimitedcodeworks.xyz> Date: Sun, 1 Dec 2019 21:13:07 -0500 Subject: [PATCH 049/215] FdoSecrets: check and show PID and executable for existing secret service process --- src/fdosecrets/FdoSecretsPlugin.cpp | 30 ++++++++++- src/fdosecrets/FdoSecretsPlugin.h | 6 +++ src/fdosecrets/objects/Service.cpp | 5 +- .../widgets/SettingsWidgetFdoSecrets.cpp | 54 +++++++++++++++++++ .../widgets/SettingsWidgetFdoSecrets.h | 12 +++++ .../widgets/SettingsWidgetFdoSecrets.ui | 10 +++- 6 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp index 646f853018..6c77e5dc67 100644 --- a/src/fdosecrets/FdoSecretsPlugin.cpp +++ b/src/fdosecrets/FdoSecretsPlugin.cpp @@ -24,6 +24,8 @@ #include "gui/DatabaseTabWidget.h" +#include <QFile> + #include <utility> using FdoSecrets::Service; @@ -56,7 +58,7 @@ void FdoSecretsPlugin::updateServiceState() if (!m_secretService && m_dbTabs) { m_secretService.reset(new Service(this, m_dbTabs)); connect(m_secretService.data(), &Service::error, this, [this](const QString& msg) { - emit error(tr("Fdo Secret Service: %1").arg(msg)); + emit error(tr("<b>Fdo Secret Service:</b> %1").arg(msg)); }); if (!m_secretService->initialize()) { m_secretService.reset(); @@ -95,3 +97,29 @@ void FdoSecretsPlugin::emitRequestShowNotification(const QString& msg, const QSt } emit requestShowNotification(msg, title, 10000); } + +QString FdoSecretsPlugin::reportExistingService() const +{ + auto pidStr = tr("Unknown", "Unknown PID"); + auto exeStr = tr("Unknown", "Unknown executable path"); + + // try get pid + auto pid = QDBusConnection::sessionBus().interface()->servicePid(DBUS_SERVICE_SECRET); + if (pid.isValid()) { + pidStr = QString::number(pid.value()); + + // try get the first part of the cmdline, which usually is the executable name/path + QFile proc(QStringLiteral("/proc/%1/cmdline").arg(pid.value())); + if (proc.open(QFile::ReadOnly)) { + auto parts = proc.readAll().split('\0'); + if (parts.length() >= 1) { + exeStr = QString::fromLocal8Bit(parts[0]).trimmed(); + } + } + } + auto otherService = tr("<i>PID: %1, Executable: %2</i>", "<i>PID: 1234, Executable: /path/to/exe</i>") + .arg(pidStr, exeStr.toHtmlEscaped()); + return tr("Another secret service is running (%1).<br/>" + "Please stop/remove it before re-enabling the Secret Service Integration.") + .arg(otherService); +} diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h index 828c0bd764..4f284f4692 100644 --- a/src/fdosecrets/FdoSecretsPlugin.h +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -64,6 +64,12 @@ class FdoSecretsPlugin : public QObject, public ISettingsPage */ DatabaseTabWidget* dbTabs() const; + /** + * Check the running secret service and returns info about it + * @return html string suitable to be shown in the UI + */ + QString reportExistingService() const; + public slots: void emitRequestSwitchToDatabases(); void emitRequestShowNotification(const QString& msg, const QString& title = {}); diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index 5408f4cb29..59f0861077 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -59,9 +59,8 @@ namespace FdoSecrets bool Service::initialize() { if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) { - qDebug() << "Another secret service is running"; - emit error(tr("Failed to register DBus service at %1: another secret service is running.") - .arg(QLatin1Literal(DBUS_SERVICE_SECRET))); + emit error(tr("Failed to register DBus service at %1.<br/>").arg(QLatin1String(DBUS_SERVICE_SECRET)) + + m_plugin->reportExistingService()); return false; } diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 5ae267a122..29b01c67b3 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -27,6 +27,8 @@ #include "gui/DatabaseWidget.h" #include <QAction> +#include <QDBusConnection> +#include <QDBusConnectionInterface> #include <QHeaderView> #include <QItemEditorFactory> #include <QStyledItemDelegate> @@ -236,6 +238,8 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi , m_plugin(plugin) { m_ui->setupUi(this); + m_ui->warningMsg->setHidden(true); + m_ui->warningMsg->setCloseButtonVisible(false); auto sessModel = new SettingsSessionModel(plugin, this); m_ui->tableSessions->setModel(sessModel); @@ -262,6 +266,12 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked()); connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled); + + // background checking + m_checkTimer.setInterval(2000); + connect(&m_checkTimer, &QTimer::timeout, this, &SettingsWidgetFdoSecrets::checkDBusName); + connect(m_plugin, &FdoSecretsPlugin::secretServiceStarted, &m_checkTimer, &QTimer::stop); + connect(m_plugin, SIGNAL(secretServiceStopped()), &m_checkTimer, SLOT(start())); } void SettingsWidgetFdoSecrets::setupView(QAbstractItemView* view, @@ -300,4 +310,48 @@ void SettingsWidgetFdoSecrets::saveSettings() FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); } +void SettingsWidgetFdoSecrets::showMessage(const QString& text, MessageWidget::MessageType type) +{ + // Show error messages for a longer time to make sure the user can read them + if (type == MessageWidget::Error) { + m_ui->warningMsg->setCloseButtonVisible(true); + m_ui->warningMsg->showMessage(text, type, -1); + } else { + m_ui->warningMsg->setCloseButtonVisible(false); + m_ui->warningMsg->showMessage(text, type, 2000); + } +} + +void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + QTimer::singleShot(0, this, &SettingsWidgetFdoSecrets::checkDBusName); + m_checkTimer.start(); +} + +void SettingsWidgetFdoSecrets::hideEvent(QHideEvent* event) +{ + QWidget::hideEvent(event); + m_checkTimer.stop(); +} + +void SettingsWidgetFdoSecrets::checkDBusName() +{ + if (m_plugin->serviceInstance()) { + // only need checking if the service is not started or failed to start. + return; + } + + auto reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral(DBUS_SERVICE_SECRET)); + if (!reply.isValid()) { + showMessage(tr("<b>Error:</b> Failed to connect to DBus. Please check your DBus setup."), MessageWidget::Error); + return; + } + if (reply.value()) { + showMessage(tr("<b>Warning:</b> ") + m_plugin->reportExistingService(), MessageWidget::Warning); + return; + } + m_ui->warningMsg->hideMessage(); +} + #include "SettingsWidgetFdoSecrets.moc" diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index 2bf58f826e..f6147cc246 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -18,7 +18,10 @@ #ifndef KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H #define KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H +#include "gui/MessageWidget.h" + #include <QScopedPointer> +#include <QTimer> #include <QWidget> class QAbstractItemView; @@ -50,6 +53,14 @@ public slots: void loadSettings(); void saveSettings(); +private slots: + void checkDBusName(); + void showMessage(const QString& text, MessageWidget::MessageType type); + +protected: + void showEvent(QShowEvent* event) override; + void hideEvent(QHideEvent* event) override; + private: void setupView(QAbstractItemView* view, int manageColumn, int editorTypeId, QItemEditorCreatorBase* creator); @@ -57,6 +68,7 @@ public slots: QScopedPointer<Ui::SettingsWidgetFdoSecrets> m_ui; QScopedPointer<QItemEditorFactory> m_factory; FdoSecretsPlugin* m_plugin; + QTimer m_checkTimer; }; #endif // KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index cfbeaa210a..ee7d494319 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -15,7 +15,7 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QWidget" name="warningMsg" native="true"/> + <widget class="MessageWidget" name="warningMsg" native="true"/> </item> <item> <widget class="QCheckBox" name="enableFdoSecretService"> @@ -132,6 +132,14 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>MessageWidget</class> + <extends>QWidget</extends> + <header>gui/MessageWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> <connections/> </ui> From 06e0f385231df060bc3f00584670096e5fe2302f Mon Sep 17 00:00:00 2001 From: Balazs Gyurak <ba32107@gmail.com> Date: Fri, 29 Nov 2019 19:12:50 +0000 Subject: [PATCH 050/215] CLI: Fix XML encoding when export database Add write function to TextStream Fix #3900 --- src/cli/Export.cpp | 2 +- src/cli/TextStream.cpp | 9 +++++++++ src/cli/TextStream.h | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/cli/Export.cpp b/src/cli/Export.cpp index e856f53325..2f2ee65e59 100644 --- a/src/cli/Export.cpp +++ b/src/cli/Export.cpp @@ -51,7 +51,7 @@ int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointe errorTextStream << QObject::tr("Unable to export database to XML: %1").arg(errorMessage) << endl; return EXIT_FAILURE; } - outputTextStream << xmlData.constData() << endl; + outputTextStream.write(xmlData.constData()); } else if (format.startsWith(QStringLiteral("csv"), Qt::CaseInsensitive)) { CsvExporter csvExporter; outputTextStream << csvExporter.exportDatabase(database); diff --git a/src/cli/TextStream.cpp b/src/cli/TextStream.cpp index 938fd62924..5757f90e97 100644 --- a/src/cli/TextStream.cpp +++ b/src/cli/TextStream.cpp @@ -58,6 +58,15 @@ TextStream::TextStream(const QByteArray& array, QIODevice::OpenMode openMode) detectCodec(); } +void TextStream::write(const char* str) +{ + // Workaround for an issue with QTextStream. Its operator<<(const char *string) will encode the + // string with a non-UTF-8 encoding. We work around this by wrapping the input string into + // a QString, thus enforcing UTF-8. More info: + // https://code.qt.io/cgit/qt/qtbase.git/commit?id=cec8cdba4d1b856e17c8743ba8803349d42dc701 + *this << QString(str); +} + void TextStream::detectCodec() { QString codecName = "UTF-8"; diff --git a/src/cli/TextStream.h b/src/cli/TextStream.h index 2dc116352a..0091ec1087 100644 --- a/src/cli/TextStream.h +++ b/src/cli/TextStream.h @@ -43,6 +43,8 @@ class TextStream : public QTextStream explicit TextStream(QByteArray* array, QIODevice::OpenMode openMode = QIODevice::ReadWrite); explicit TextStream(const QByteArray& array, QIODevice::OpenMode openMode = QIODevice::ReadOnly); + void write(const char* str); + private: void detectCodec(); }; From 71a39c37eca9b080a2f06e768ad2e83fe6ff6cb8 Mon Sep 17 00:00:00 2001 From: James Ring <sjr@jdns.org> Date: Thu, 30 Jan 2020 12:46:48 -0800 Subject: [PATCH 051/215] Add --username option to Clip command. (#3947) * make Clip accept an attribute name This allows users to copy arbitrary attributes (e.g. username, notes, URL) to the clipboard in addition to the password and TOTP values. * update Clip manpage * Add findAttributes to CLI utils * Use case-insensitive search in Show command. * Use case-insensitive search in Clip command. Co-authored-by: louib <L0U13@protonmail.com> --- share/docs/man/keepassxc-cli.1 | 12 +++++--- src/cli/Clip.cpp | 52 ++++++++++++++++++++++++--------- src/cli/Clip.h | 1 + src/cli/Show.cpp | 26 ++++++++++++----- src/cli/Utils.cpp | 17 +++++++++++ src/cli/Utils.h | 9 ++++++ tests/TestCli.cpp | 44 +++++++++++++++++++++++++++- tests/data/NewDatabase.kdbx | Bin 15006 -> 20334 bytes 8 files changed, 135 insertions(+), 26 deletions(-) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index d7ab9cdd75..d1360cd65c 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -23,7 +23,7 @@ The same password generation options as documented for the generate command can Analyzes passwords in a database for weaknesses. .IP "\fBclip\fP [options] <database> <entry> [timeout]" -Copies the password or the current TOTP (\fI-t\fP option) of a database entry to the clipboard. If multiple entries with the same name exist in different groups, only the password for the first one is going to be copied. For copying the password of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. +Copies an attribute or the current TOTP (if the \fI-t\fP option is specified) of a database entry to the clipboard. If no attribute name is specified using the \fI-a\fP option, the password is copied. If multiple entries with the same name exist in different groups, only the attribute for the first one is copied. For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name. Optionally, a timeout in seconds can be specified to automatically clear the clipboard. .IP "\fBclose\fP" In interactive mode, closes the currently opened database (see \fIopen\fP). @@ -174,10 +174,14 @@ hour or so). .SS "Clip options" -.IP "\fB-t\fP, \fB--totp\fP" -Copies the current TOTP instead of current password to clipboard. Will report -an error if no TOTP is configured for the entry. +.IP "\fB-a\fP, \fB--attribute\fP" +Copies the specified attribute to the clipboard. If no attribute is specified, +the password attribute is the default. For example, "\fI-a\fP username" would +copy the username to the clipboard. [Default: password] +.IP "\fB-t\fP, \fB--totp\fP" +Copies the current TOTP instead of the specified attribute to the clipboard. +Will report an error if no TOTP is configured for the entry. .SS "Create options" diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 482ad8a13f..ccb7c0e534 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -17,7 +17,6 @@ #include <chrono> #include <cstdlib> -#include <stdio.h> #include <thread> #include "Clip.h" @@ -28,14 +27,23 @@ #include "core/Entry.h" #include "core/Group.h" -const QCommandLineOption Clip::TotpOption = QCommandLineOption(QStringList() << "t" - << "totp", - QObject::tr("Copy the current TOTP to the clipboard.")); +const QCommandLineOption Clip::AttributeOption = QCommandLineOption( + QStringList() << "a" + << "attribute", + QObject::tr("Copy the given attribute to the clipboard. Defaults to \"password\" if not specified."), + "attr", + "password"); + +const QCommandLineOption Clip::TotpOption = + QCommandLineOption(QStringList() << "t" + << "totp", + QObject::tr("Copy the current TOTP to the clipboard (equivalent to \"-a totp\").")); Clip::Clip() { name = QString("clip"); - description = QObject::tr("Copy an entry's password to the clipboard."); + description = QObject::tr("Copy an entry's attribute to the clipboard."); + options.append(Clip::AttributeOption); options.append(Clip::TotpOption); positionalArguments.append( {QString("entry"), QObject::tr("Path of the entry to clip.", "clip = copy to clipboard"), QString("")}); @@ -51,7 +59,6 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< if (args.size() == 3) { timeout = args.at(2); } - bool clipTotp = parser->isSet(Clip::TotpOption); TextStream errorTextStream(Utils::STDERR); int timeoutSeconds = 0; @@ -70,16 +77,39 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< return EXIT_FAILURE; } + if (parser->isSet(AttributeOption) && parser->isSet(TotpOption)) { + errorTextStream << QObject::tr("ERROR: Please specify one of --attribute or --totp, not both.") << endl; + return EXIT_FAILURE; + } + + QString selectedAttribute = parser->value(AttributeOption); QString value; - if (clipTotp) { + bool found = false; + if (parser->isSet(TotpOption) || selectedAttribute == "totp") { if (!entry->hasTotp()) { errorTextStream << QObject::tr("Entry with path %1 has no TOTP set up.").arg(entryPath) << endl; return EXIT_FAILURE; } + found = true; value = entry->totp(); } else { - value = entry->password(); + QStringList attrs = Utils::findAttributes(*entry->attributes(), selectedAttribute); + if (attrs.size() > 1) { + errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.") + .arg(selectedAttribute, QLocale().createSeparatedList(attrs)) + << endl; + return EXIT_FAILURE; + } else if (attrs.size() == 1) { + found = true; + selectedAttribute = attrs[0]; + value = entry->attributes()->value(selectedAttribute); + } + } + + if (!found) { + outputTextStream << QObject::tr("Attribute \"%1\" not found.").arg(selectedAttribute) << endl; + return EXIT_FAILURE; } int exitCode = Utils::clipText(value); @@ -87,11 +117,7 @@ int Clip::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< return exitCode; } - if (clipTotp) { - outputTextStream << QObject::tr("Entry's current TOTP copied to the clipboard!") << endl; - } else { - outputTextStream << QObject::tr("Entry's password copied to the clipboard!") << endl; - } + outputTextStream << QObject::tr("Entry's \"%1\" attribute copied to the clipboard!").arg(selectedAttribute) << endl; if (!timeoutSeconds) { return exitCode; diff --git a/src/cli/Clip.h b/src/cli/Clip.h index b171c8689a..291e63295a 100644 --- a/src/cli/Clip.h +++ b/src/cli/Clip.h @@ -27,6 +27,7 @@ class Clip : public DatabaseCommand int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override; + static const QCommandLineOption AttributeOption; static const QCommandLineOption TotpOption; }; diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index f7bf8d54b5..12b2a835f6 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -27,6 +27,8 @@ #include "core/Global.h" #include "core/Group.h" +#include <QLocale> + const QCommandLineOption Show::TotpOption = QCommandLineOption(QStringList() << "t" << "totp", QObject::tr("Show the entry's current TOTP.")); @@ -79,25 +81,33 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< // If no attributes specified, output the default attribute set. bool showDefaultAttributes = attributes.isEmpty() && !showTotp; - if (attributes.isEmpty() && !showTotp) { + if (showDefaultAttributes) { attributes = EntryAttributes::DefaultAttributes; } // Iterate over the attributes and output them line-by-line. - bool sawUnknownAttribute = false; + bool encounteredError = false; for (const QString& attributeName : asConst(attributes)) { - if (!entry->attributes()->contains(attributeName)) { - sawUnknownAttribute = true; + QStringList attrs = Utils::findAttributes(*entry->attributes(), attributeName); + if (attrs.isEmpty()) { + encounteredError = true; errorTextStream << QObject::tr("ERROR: unknown attribute %1.").arg(attributeName) << endl; continue; + } else if (attrs.size() > 1) { + encounteredError = true; + errorTextStream << QObject::tr("ERROR: attribute %1 is ambiguous, it matches %2.") + .arg(attributeName, QLocale().createSeparatedList(attrs)) + << endl; + continue; } + QString canonicalName = attrs[0]; if (showDefaultAttributes) { - outputTextStream << attributeName << ": "; + outputTextStream << canonicalName << ": "; } - if (entry->attributes()->isProtected(attributeName) && showDefaultAttributes && !showProtectedAttributes) { + if (entry->attributes()->isProtected(canonicalName) && showDefaultAttributes && !showProtectedAttributes) { outputTextStream << "PROTECTED" << endl; } else { - outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attributeName)) << endl; + outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(canonicalName)) << endl; } } @@ -105,5 +115,5 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer< outputTextStream << entry->totp() << endl; } - return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS; + return encounteredError ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index 9988b60f9a..88d70466be 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -331,4 +331,21 @@ namespace Utils return result; } + QStringList findAttributes(const EntryAttributes& attributes, const QString& name) + { + QStringList result; + if (attributes.hasKey(name)) { + result.append(name); + return result; + } + + for (const QString& key : attributes.keys()) { + if (key.compare(name, Qt::CaseSensitivity::CaseInsensitive) == 0) { + result.append(key); + } + } + + return result; + } + } // namespace Utils diff --git a/src/cli/Utils.h b/src/cli/Utils.h index b7fa63acf2..d96e260c4e 100644 --- a/src/cli/Utils.h +++ b/src/cli/Utils.h @@ -20,6 +20,7 @@ #include "cli/TextStream.h" #include "core/Database.h" +#include "core/EntryAttributes.h" #include "keys/CompositeKey.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" @@ -51,6 +52,14 @@ namespace Utils QStringList splitCommandString(const QString& command); + /** + * If `attributes` contains an attribute named `name` (case-sensitive), + * returns a list containing only `name`. Otherwise, returns the list of + * all attribute names in `attributes` matching the given name + * (case-insensitive). + */ + QStringList findAttributes(const EntryAttributes& attributes, const QString& name); + namespace Test { void setNextPassword(const QString& password); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index bc96de9740..efaff18319 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -480,7 +480,7 @@ void TestCli::testClip() QCOMPARE(clipboard->text(), QString("Password")); m_stdoutFile->readLine(); // skip prompt line - QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's password copied to the clipboard!\n")); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("Entry's \"Password\" attribute copied to the clipboard!\n")); // Quiet option qint64 pos = m_stdoutFile->pos(); @@ -491,6 +491,11 @@ void TestCli::testClip() QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(clipboard->text(), QString("Password")); + // Username + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "-a", "username"}); + QCOMPARE(clipboard->text(), QString("User Name")); + // TOTP Utils::Test::setNextPassword("a"); clipCmd.execute({"clip", m_dbFile->fileName(), "/Sample Entry", "--totp"}); @@ -538,6 +543,20 @@ void TestCli::testClip() clipCmd.execute({"clip", m_dbFile2->fileName(), "--totp", "/Sample Entry"}); m_stderrFile->seek(posErr); QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n")); + + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile->fileName(), "-a", "TESTAttribute1", "/Sample Entry"}); + m_stderrFile->seek(posErr); + QCOMPARE( + m_stderrFile->readAll(), + QByteArray("ERROR: attribute TESTAttribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n")); + + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + clipCmd.execute({"clip", m_dbFile2->fileName(), "--attribute", "Username", "--totp", "/Sample Entry"}); + m_stderrFile->seek(posErr); + QCOMPARE(m_stderrFile->readAll(), QByteArray("ERROR: Please specify one of --attribute or --totp, not both.\n")); } void TestCli::testCreate() @@ -1913,6 +1932,16 @@ void TestCli::testShow() QByteArray("Sample Entry\n" "http://www.somesite.com/\n")); + // Test case insensitivity + pos = m_stdoutFile->pos(); + Utils::Test::setNextPassword("a"); + showCmd.execute({"show", "-a", "TITLE", "-a", "URL", m_dbFile->fileName(), "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + QCOMPARE(m_stdoutFile->readAll(), + QByteArray("Sample Entry\n" + "http://www.somesite.com/\n")); + pos = m_stdoutFile->pos(); Utils::Test::setNextPassword("a"); showCmd.execute({"show", "-a", "DoesNotExist", m_dbFile->fileName(), "/Sample Entry"}); @@ -1946,6 +1975,19 @@ void TestCli::testShow() m_stderrFile->seek(posErr); QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stderrFile->readAll(), QByteArray("Entry with path /Sample Entry has no TOTP set up.\n")); + + // Show with ambiguous attributes + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + showCmd.execute({"show", m_dbFile->fileName(), "-a", "Testattribute1", "/Sample Entry"}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip password prompt + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); + QCOMPARE( + m_stderrFile->readAll(), + QByteArray("ERROR: attribute Testattribute1 is ambiguous, it matches TestAttribute1 and testattribute1.\n")); } void TestCli::testInvalidDbFiles() diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index 3008cce7c9fcb53524f2bd500afd2615b7bb1afc..a6d6adb1707f66cae69a44f51c092070d50a7e71 100644 GIT binary patch literal 20334 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 RISi-m9{B9lEugwR{*sO{VyFNB literal 15006 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 From a81c6469a87783abdd4434b03f616465c830b57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= <wolfram@roesler-ac.de> Date: Sat, 1 Feb 2020 08:42:34 -0500 Subject: [PATCH 052/215] Implement Password Health Report Introduce a password health check to the application that evaluates every entry in a database. Entries that fail various tests are listed for user review and action. Also moves the statistics panel to the new Database -> Reports widget. Recycled entries are excluded from the results. We now have two classes, PasswordHealth to deal with a single password and HealthChecker to deal with all passwords of a database. Tests include passwords that are expired, re-used, and weak. * Closes #551 * Move zxcvbn usage to a centralized class (PasswordHealth) and replace its usages across the application to ensure standardized interpretation of entropy calculations. * Add new icons for the database reports view * Updated the demo database to show off the reports --- share/demo.kdbx | Bin 25109 -> 38965 bytes .../application/scalable/actions/health.svg | 1 + src/CMakeLists.txt | 9 +- src/browser/BrowserSettings.cpp | 3 +- src/cli/Estimate.cpp | 6 +- src/core/PasswordGenerator.cpp | 6 - src/core/PasswordGenerator.h | 1 - src/core/PasswordHealth.cpp | 188 ++++++++++++++ src/core/PasswordHealth.h | 113 +++++++++ src/gui/AboutDialog.cpp | 2 +- src/gui/DatabaseTabWidget.cpp | 5 + src/gui/DatabaseTabWidget.h | 1 + src/gui/DatabaseWidget.cpp | 11 + src/gui/DatabaseWidget.h | 3 + src/gui/MainWindow.cpp | 5 + src/gui/MainWindow.ui | 15 ++ src/gui/PasswordGeneratorWidget.cpp | 38 +-- src/gui/PasswordGeneratorWidget.h | 3 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 3 - src/gui/reports/ReportsDialog.cpp | 128 ++++++++++ src/gui/reports/ReportsDialog.h | 85 +++++++ src/gui/reports/ReportsDialog.ui | 43 ++++ src/gui/reports/ReportsPageHealthcheck.cpp | 55 ++++ src/gui/reports/ReportsPageHealthcheck.h | 41 +++ .../ReportsPageStatistics.cpp} | 22 +- .../ReportsPageStatistics.h} | 10 +- src/gui/reports/ReportsWidget.cpp | 44 ++++ src/gui/reports/ReportsWidget.h | 53 ++++ src/gui/reports/ReportsWidgetHealthcheck.cpp | 237 ++++++++++++++++++ src/gui/reports/ReportsWidgetHealthcheck.h | 70 ++++++ src/gui/reports/ReportsWidgetHealthcheck.ui | 79 ++++++ .../ReportsWidgetStatistics.cpp} | 38 +-- .../ReportsWidgetStatistics.h} | 16 +- .../ReportsWidgetStatistics.ui} | 4 +- tests/CMakeLists.txt | 3 + tests/TestPasswordHealth.cpp | 65 +++++ tests/TestPasswordHealth.h | 32 +++ utils/makeicons.sh | 1 + 38 files changed, 1364 insertions(+), 75 deletions(-) create mode 100644 share/icons/application/scalable/actions/health.svg create mode 100644 src/core/PasswordHealth.cpp create mode 100644 src/core/PasswordHealth.h create mode 100644 src/gui/reports/ReportsDialog.cpp create mode 100644 src/gui/reports/ReportsDialog.h create mode 100644 src/gui/reports/ReportsDialog.ui create mode 100644 src/gui/reports/ReportsPageHealthcheck.cpp create mode 100644 src/gui/reports/ReportsPageHealthcheck.h rename src/gui/{dbsettings/DatabaseSettingsPageStatistics.cpp => reports/ReportsPageStatistics.cpp} (57%) rename src/gui/{dbsettings/DatabaseSettingsPageStatistics.h => reports/ReportsPageStatistics.h} (78%) create mode 100644 src/gui/reports/ReportsWidget.cpp create mode 100644 src/gui/reports/ReportsWidget.h create mode 100644 src/gui/reports/ReportsWidgetHealthcheck.cpp create mode 100644 src/gui/reports/ReportsWidgetHealthcheck.h create mode 100644 src/gui/reports/ReportsWidgetHealthcheck.ui rename src/gui/{dbsettings/DatabaseSettingsWidgetStatistics.cpp => reports/ReportsWidgetStatistics.cpp} (86%) rename src/gui/{dbsettings/DatabaseSettingsWidgetStatistics.h => reports/ReportsWidgetStatistics.h} (74%) rename src/gui/{dbsettings/DatabaseSettingsWidgetStatistics.ui => reports/ReportsWidgetStatistics.ui} (94%) create mode 100644 tests/TestPasswordHealth.cpp create mode 100644 tests/TestPasswordHealth.h diff --git a/share/demo.kdbx b/share/demo.kdbx index 71795676a953bfa2f88364f8ca050801955b61e6..1f372710486e39a33ec9aceef80be303f1e60f07 100644 GIT binary patch literal 38965 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 literal 25109 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 G0000tmd2g{ diff --git a/share/icons/application/scalable/actions/health.svg b/share/icons/application/scalable/actions/health.svg new file mode 100644 index 0000000000..4cd5fa0917 --- /dev/null +++ b/share/icons/application/scalable/actions/health.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="mdi-heart-pulse" width="24" height="24" viewBox="0 0 24 24"><path d="M7.5,4A5.5,5.5 0 0,0 2,9.5C2,10 2.09,10.5 2.22,11H6.3L7.57,7.63C7.87,6.83 9.05,6.75 9.43,7.63L11.5,13L12.09,11.58C12.22,11.25 12.57,11 13,11H21.78C21.91,10.5 22,10 22,9.5A5.5,5.5 0 0,0 16.5,4C14.64,4 13,4.93 12,6.34C11,4.93 9.36,4 7.5,4V4M3,12.5A1,1 0 0,0 2,13.5A1,1 0 0,0 3,14.5H5.44L11,20C12,20.9 12,20.9 13,20L18.56,14.5H21A1,1 0 0,0 22,13.5A1,1 0 0,0 21,12.5H13.4L12.47,14.8C12.07,15.81 10.92,15.67 10.55,14.83L8.5,9.5L7.54,11.83C7.39,12.21 7.05,12.5 6.6,12.5H3Z" /></svg> \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index af9b9bb586..6b3d9abfab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -48,6 +48,7 @@ set(keepassx_SOURCES core/Merger.cpp core/Metadata.cpp core/PasswordGenerator.cpp + core/PasswordHealth.cpp core/PassphraseGenerator.cpp core/SignalMultiplexer.cpp core/ScreenLockListener.cpp @@ -149,8 +150,12 @@ set(keepassx_SOURCES gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp - gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp - gui/dbsettings/DatabaseSettingsPageStatistics.cpp + gui/reports/ReportsWidget.cpp + gui/reports/ReportsDialog.cpp + gui/reports/ReportsWidgetHealthcheck.cpp + gui/reports/ReportsPageHealthcheck.cpp + gui/reports/ReportsWidgetStatistics.cpp + gui/reports/ReportsPageStatistics.cpp gui/settings/SettingsWidget.cpp gui/widgets/ElidedLabel.cpp gui/widgets/PopupHelpWidget.cpp diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index 9cb4e0735c..b49af7005b 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -19,6 +19,7 @@ #include "BrowserSettings.h" #include "core/Config.h" +#include "core/PasswordHealth.h" BrowserSettings* BrowserSettings::m_instance(nullptr); @@ -541,7 +542,7 @@ QJsonObject BrowserSettings::generatePassword() m_passwordGenerator.setCharClasses(passwordCharClasses()); m_passwordGenerator.setFlags(passwordGeneratorFlags()); const QString pw = m_passwordGenerator.generatePassword(); - password["entropy"] = m_passwordGenerator.estimateEntropy(pw); + password["entropy"] = PasswordHealth(pw).entropy(); password["password"] = pw; } else { m_passPhraseGenerator.setWordCount(passPhraseWordCount()); diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp index a84e239630..3b75090571 100644 --- a/src/cli/Estimate.cpp +++ b/src/cli/Estimate.cpp @@ -19,6 +19,7 @@ #include "cli/Utils.h" #include "cli/TextStream.h" +#include "core/PasswordHealth.h" #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -49,10 +50,9 @@ static void estimate(const char* pwd, bool advanced) { TextStream out(Utils::STDOUT, QIODevice::WriteOnly); - double e = 0.0; int len = static_cast<int>(strlen(pwd)); if (!advanced) { - e = ZxcvbnMatch(pwd, nullptr, nullptr); + const auto e = PasswordHealth(pwd).entropy(); // clang-format off out << QObject::tr("Length %1").arg(len, 0) << '\t' << QObject::tr("Entropy %1").arg(e, 0, 'f', 3) << '\t' @@ -62,7 +62,7 @@ static void estimate(const char* pwd, bool advanced) int ChkLen = 0; ZxcMatch_t *info, *p; double m = 0.0; - e = ZxcvbnMatch(pwd, nullptr, &info); + const auto e = ZxcvbnMatch(pwd, nullptr, &info); for (p = info; p; p = p->Next) { m += p->Entrpy; } diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index e203af672b..ff271a4533 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -19,7 +19,6 @@ #include "PasswordGenerator.h" #include "crypto/Random.h" -#include <zxcvbn.h> const char* PasswordGenerator::DefaultExcludedChars = ""; @@ -31,11 +30,6 @@ PasswordGenerator::PasswordGenerator() { } -double PasswordGenerator::estimateEntropy(const QString& password) -{ - return ZxcvbnMatch(password.toLatin1(), nullptr, nullptr); -} - void PasswordGenerator::setLength(int length) { if (length <= 0) { diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h index 22627d25ba..55418b4ba2 100644 --- a/src/core/PasswordGenerator.h +++ b/src/core/PasswordGenerator.h @@ -57,7 +57,6 @@ class PasswordGenerator public: PasswordGenerator(); - double estimateEntropy(const QString& password); void setLength(int length); void setCharClasses(const CharClasses& classes); void setFlags(const GeneratorFlags& flags); diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp new file mode 100644 index 0000000000..58e4e42af5 --- /dev/null +++ b/src/core/PasswordHealth.cpp @@ -0,0 +1,188 @@ +/* + * 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 <QApplication> +#include <QString> + +#include "Database.h" +#include "Entry.h" +#include "Group.h" +#include "PasswordHealth.h" +#include "zxcvbn.h" + +PasswordHealth::PasswordHealth(double entropy) + : m_score(entropy) + , m_entropy(entropy) +{ + switch (quality()) { + case Quality::Bad: + case Quality::Poor: + m_scoreReasons << QApplication::tr("Very weak password"); + m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2)); + break; + + case Quality::Weak: + m_scoreReasons << QApplication::tr("Weak password"); + m_scoreDetails << QApplication::tr("Password entropy is %1 bits").arg(QString::number(m_entropy, 'f', 2)); + break; + + default: + // No reason or details for good and excellent passwords + break; + } +} + +PasswordHealth::PasswordHealth(QString pwd) + : PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr)) +{ +} + +void PasswordHealth::setScore(int score) +{ + m_score = score; +} + +void PasswordHealth::adjustScore(int amount) +{ + m_score += amount; +} + +QString PasswordHealth::scoreReason() const +{ + return m_scoreReasons.join("\n"); +} + +void PasswordHealth::addScoreReason(QString reason) +{ + m_scoreReasons << reason; +} + +QString PasswordHealth::scoreDetails() const +{ + return m_scoreDetails.join("\n"); +} + +void PasswordHealth::addScoreDetails(QString details) +{ + m_scoreDetails.append(details); +} + +PasswordHealth::Quality PasswordHealth::quality() const +{ + if (m_score <= 0) { + return Quality::Bad; + } else if (m_score < 40) { + return Quality::Poor; + } else if (m_score < 65) { + return Quality::Weak; + } else if (m_score < 100) { + return Quality::Good; + } + return Quality::Excellent; +} + +/** + * This class provides additional information about password health + * than can be derived from the password itself (re-use, expiry). + */ +HealthChecker::HealthChecker(QSharedPointer<Database> db) +{ + // Build the cache of re-used passwords + for (const auto* entry : db->rootGroup()->entriesRecursive()) { + if (!entry->isRecycled()) { + m_reuse[entry->password()] + << QApplication::tr("Used in %1/%2").arg(entry->group()->hierarchy().join('/'), entry->title()); + } + } +} + +/** + * Call operator of the Health Checker class. + * + * Returns the health of the password in `entry`, considering + * password entropy, re-use, expiration, etc. + */ +QSharedPointer<PasswordHealth> HealthChecker::evaluate(const Entry* entry) +{ + if (!entry) { + return {}; + } + + // Return from cache if we saw it before + if (m_cache.contains(entry->uuid())) { + return m_cache[entry->uuid()]; + } + + // First analyse the password itself + const auto pwd = entry->password(); + auto health = QSharedPointer<PasswordHealth>(new PasswordHealth(pwd)); + + // Second, if the password is in the database more than once, + // reduce the score accordingly + const auto& used = m_reuse[pwd]; + const auto count = used.size(); + if (count > 1) { + constexpr auto penalty = 15; + health->adjustScore(-penalty * (count - 1)); + health->addScoreReason(QApplication::tr("Password is used %1 times").arg(QString::number(count))); + // Add the first 20 uses of the password to prevent the details display from growing too large + for (int i = 0; i < used.size(); ++i) { + health->addScoreDetails(used[i]); + if (i == 19) { + health->addScoreDetails(QStringLiteral("...")); + break; + } + } + + // Don't allow re-used passwords to be considered "good" + // no matter how great their entropy is. + if (health->score() > 64) { + health->setScore(64); + } + } + + // Third, if the password has already expired, reduce score to 0; + // or, if the password is going to expire in the next 30 days, + // reduce score by 2 points per day. + if (entry->isExpired()) { + health->setScore(0); + health->addScoreReason(QApplication::tr("Password has expired")); + health->addScoreDetails(QApplication::tr("Password expiry was %1") + .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate))); + } else if (entry->timeInfo().expires()) { + const auto days = QDateTime::currentDateTime().daysTo(entry->timeInfo().expiryTime()); + if (days <= 30) { + // First bring the score down into the "weak" range + // so that the entry appears in Health Check. Then + // reduce the score by 2 points for every day that + // we get closer to expiry. days<=0 has already + // been handled above ("isExpired()"). + if (health->score() > 60) { + health->setScore(60); + } + health->adjustScore((30 - days) * -2); + health->addScoreReason(days <= 2 ? QApplication::tr("Password is about to expire") + : days <= 10 ? QApplication::tr("Password expires in %1 days").arg(days) + : QApplication::tr("Password will expire soon")); + health->addScoreDetails(QApplication::tr("Password expires on %1") + .arg(entry->timeInfo().expiryTime().toString(Qt::DefaultLocaleShortDate))); + } + } + + // Return the result + return m_cache.insert(entry->uuid(), health).value(); +} diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h new file mode 100644 index 0000000000..ca7f0236ec --- /dev/null +++ b/src/core/PasswordHealth.h @@ -0,0 +1,113 @@ +/* + * 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_PASSWORDHEALTH_H +#define KEEPASSX_PASSWORDHEALTH_H + +#include <QHash> +#include <QSharedPointer> +#include <QStringList> + +class Database; +class Entry; + +/** + * Health status of a single password. + * + * @see HealthChecker + */ +class PasswordHealth +{ +public: + explicit PasswordHealth(double entropy); + explicit PasswordHealth(QString pwd); + + /* + * The password score is defined to be the greater the better + * (more secure) the password is. It doesn't have a dimension, + * there are no defined maximum or minimum values, and score + * values may change with different versions of the software. + */ + int score() const + { + return m_score; + } + + void setScore(int score); + void adjustScore(int amount); + + /* + * A text description for the password's quality assessment + * (translated into the application language), and additional + * information. Empty if nothing is wrong with the password. + * May contain more than line, separated by '\n'. + */ + QString scoreReason() const; + void addScoreReason(QString reason); + + QString scoreDetails() const; + void addScoreDetails(QString details); + + /* + * The password quality assessment (based on the score). + */ + enum class Quality + { + Bad, + Poor, + Weak, + Good, + Excellent + }; + Quality quality() const; + + /* + * The password's raw entropy value, in bits. + */ + double entropy() const + { + return m_entropy; + } + +private: + int m_score = 0; + double m_entropy = 0.0; + QStringList m_scoreReasons; + QStringList m_scoreDetails; +}; + +/** + * Password health check for all entries of a database. + * + * @see PasswordHealth + */ +class HealthChecker +{ +public: + explicit HealthChecker(QSharedPointer<Database>); + + // Get the health status of an entry in the database + QSharedPointer<PasswordHealth> evaluate(const Entry* entry); + +private: + // Result cache (first=entry UUID) + QHash<QUuid, QSharedPointer<PasswordHealth>> m_cache; + // first = password, second = entries that use it + QHash<QString, QStringList> m_reuse; +}; + +#endif // KEEPASSX_PASSWORDHEALTH_H diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 4b9fe5f858..bd24cf165b 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -76,7 +76,7 @@ static const QString aboutContributors = R"( <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)</li> <li>mdaniel (OpVault Importer)</li> <li>keithbennett (KeePassHTTP)</li> <li>Typz (KeePassHTTP)</li> diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index c37e6c5ea6..7e158406b2 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -457,6 +457,11 @@ void DatabaseTabWidget::changeMasterKey() currentDatabaseWidget()->switchToMasterKeyChange(); } +void DatabaseTabWidget::changeReports() +{ + currentDatabaseWidget()->switchToReports(); +} + void DatabaseTabWidget::changeDatabaseSettings() { currentDatabaseWidget()->switchToDatabaseSettings(); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 5c55bc63c7..29019a2d29 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -78,6 +78,7 @@ public slots: void relockPendingDatabase(); void changeMasterKey(); + void changeReports(); void changeDatabaseSettings(); void performGlobalAutoType(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index eb33c09c0b..fd579b04a0 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -59,6 +59,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 +89,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 +167,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 +176,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); @@ -196,6 +200,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))); @@ -1105,6 +1110,12 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod } } +void DatabaseWidget::switchToReports() +{ + m_reportsDialog->load(m_db); + setCurrentWidget(m_reportsDialog); +} + void DatabaseWidget::switchToDatabaseSettings() { m_databaseSettingDialog->load(m_db); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 9f0c5c9765..6420a3b242 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; @@ -181,6 +182,7 @@ public slots: void sortGroupsAsc(); void sortGroupsDesc(); void switchToMasterKeyChange(); + void switchToReports(); void switchToDatabaseSettings(); void switchToOpenDatabase(); void switchToOpenDatabase(const QString& filePath); @@ -251,6 +253,7 @@ private slots: 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/MainWindow.cpp b/src/gui/MainWindow.cpp index e9c150dd5c..2d52331ff3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -332,6 +332,7 @@ MainWindow::MainWindow() 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->actionReports->setIcon(filePath()->icon("actions", "help-about")); 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")); @@ -403,6 +404,7 @@ MainWindow::MainWindow() 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->actionReports, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeReports())); connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); @@ -673,6 +675,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries && !recycleBinSelected); m_ui->actionChangeMasterKey->setEnabled(true); + m_ui->actionReports->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); m_ui->actionDatabaseSaveAs->setEnabled(true); @@ -719,6 +722,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionReports->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); @@ -746,6 +750,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionReports->setEnabled(false); m_ui->actionChangeDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index e09c91dd79..aec0efb37e 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -236,6 +236,7 @@ <addaction name="actionDatabaseClose"/> <addaction name="separator"/> <addaction name="actionChangeMasterKey"/> + <addaction name="actionReports"/> <addaction name="actionChangeDatabaseSettings"/> <addaction name="separator"/> <addaction name="actionDatabaseMerge"/> @@ -532,6 +533,20 @@ <string>Change master &key...</string> </property> </action> + <action name="actionReports"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>&Reports...</string> + </property> + <property name="toolTip"> + <string>Statistics, health check, etc.</string> + </property> + <property name="menuRole"> + <enum>QAction::NoRole</enum> + </property> + </action> <action name="actionChangeDatabaseSettings"> <property name="enabled"> <bool>false</bool> diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index e0f8fbe5fc..c04487c0e4 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -26,6 +26,7 @@ #include "core/Config.h" #include "core/FilePath.h" #include "core/PasswordGenerator.h" +#include "core/PasswordHealth.h" #include "gui/Clipboard.h" PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) @@ -261,21 +262,17 @@ 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() @@ -384,7 +381,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(); @@ -395,18 +392,27 @@ void PasswordGeneratorWidget::colorStrengthIndicator(double entropy) // 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) { + switch (health.quality()) { + case PasswordHealth::Quality::Bad: + case PasswordHealth::Quality::Poor: m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality"))); - } else if (entropy >= 40 && entropy < 65) { + break; + + case PasswordHealth::Quality::Weak: m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality"))); - } else if (entropy >= 65 && entropy < 100) { + break; + + case PasswordHealth::Quality::Good: m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality"))); - } else { + break; + + case PasswordHealth::Quality::Excellent: m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60")); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality"))); + break; } } diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index b39a2f10f9..eba7f815f6 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 @@ -77,7 +78,7 @@ private slots: void passwordSpinBoxChanged(); void dicewareSliderMoved(); void dicewareSpinBoxChanged(); - void colorStrengthIndicator(double entropy); + void colorStrengthIndicator(const PasswordHealth& health); void updateGenerator(); diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index 33c4df2c4d..e0e6765a46 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -19,7 +19,6 @@ #include "DatabaseSettingsDialog.h" #include "ui_DatabaseSettingsDialog.h" -#include "DatabaseSettingsPageStatistics.h" #include "DatabaseSettingsWidgetEncryption.h" #include "DatabaseSettingsWidgetGeneral.h" #include "DatabaseSettingsWidgetMasterKey.h" @@ -85,8 +84,6 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); - addSettingsPage(new DatabaseSettingsPageStatistics()); - #if defined(WITH_XC_KEESHARE) addSettingsPage(new DatabaseSettingsPageKeeShare()); #endif diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp new file mode 100644 index 0000000000..22ebab41a9 --- /dev/null +++ b/src/gui/reports/ReportsDialog.cpp @@ -0,0 +1,128 @@ +/* + * 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 "ReportsPageStatistics.h" +#include "ReportsWidgetHealthcheck.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_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_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(const Group*, Entry*)), + SLOT(entryActivationSignalReceived(const Group*, 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(const Group* group, Entry* entry) +{ + m_editEntryWidget->loadEntry(entry, false, false, group->hierarchy().join(" > "), m_db); + m_ui->stackedWidget->setCurrentWidget(m_editEntryWidget); +} + +void ReportsDialog::switchToMainView(bool previousDialogAccepted) +{ + m_ui->stackedWidget->setCurrentWidget(m_healthPage->m_healthWidget); + if (previousDialogAccepted) { + m_healthPage->m_healthWidget->calculateHealth(); + } +} diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h new file mode 100644 index 0000000000..7a53623c38 --- /dev/null +++ b/src/gui/reports/ReportsDialog.h @@ -0,0 +1,85 @@ +/* + * 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 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(const Group*, Entry* entry); + void switchToMainView(bool previousDialogAccepted); + +private: + QSharedPointer<Database> m_db; + const QScopedPointer<Ui::ReportsDialog> m_ui; + const QSharedPointer<ReportsPageHealthcheck> m_healthPage; + const QSharedPointer<ReportsPageStatistics> m_statPage; + QPointer<EditEntryWidget> m_editEntryWidget; + + 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 0000000000..773981a101 --- /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 0000000000..41fa406258 --- /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/FilePath.h" + +#include <QApplication> + +ReportsPageHealthcheck::ReportsPageHealthcheck() + : m_healthWidget(new ReportsWidgetHealthcheck()) +{ +} + +QString ReportsPageHealthcheck::name() +{ + return QApplication::tr("Health Check"); +} + +QIcon ReportsPageHealthcheck::icon() +{ + return FilePath::instance()->icon("actions", "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 0000000000..8a85b2d20d --- /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/dbsettings/DatabaseSettingsPageStatistics.cpp b/src/gui/reports/ReportsPageStatistics.cpp similarity index 57% rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.cpp rename to src/gui/reports/ReportsPageStatistics.cpp index 6fe24ff0f3..e4570e172d 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 "ReportsWidgetStatistics.h" #include "core/FilePath.h" -#include "core/Group.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"); } -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 similarity index 78% rename from src/gui/dbsettings/DatabaseSettingsPageStatistics.h rename to src/gui/reports/ReportsPageStatistics.h index c890f3b81c..00d611ee34 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 @@ class DatabaseSettingsPageStatistics : public IDatabaseSettingsPage 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 0000000000..1844341160 --- /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 0000000000..631490405d --- /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 0000000000..c668b3495d --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -0,0 +1,237 @@ +/* + * 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/FilePath.h" +#include "core/Group.h" +#include "core/PasswordHealth.h" + +#include <QSharedPointer> +#include <QStandardItemModel> +#include <QVector> + +namespace +{ + class Health + { + public: + struct Item + { + QPointer<const Group> group; + QPointer<const Entry> entry; + QSharedPointer<PasswordHealth> health; + + Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h) + : group(g) + , entry(e) + , health(h) + { + } + + 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; + } + + private: + QSharedPointer<Database> m_db; + HealthChecker m_checker; + QList<QSharedPointer<Item>> m_items; + }; +} // 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; + } + + // Add entry if its password isn't at least "good" + const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry))); + 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(FilePath::instance()->icon("status", "dialog-error")) +{ + m_ui->setupUi(this); + + m_referencesModel.reset(new QStandardItemModel()); + m_ui->healthcheckTableView->setModel(m_referencesModel.data()); + m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); + m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); +} + +ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() +{ +} + +void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health, + const Group* group, + const Entry* entry) +{ + QString descr, tip; + QColor qualityColor; + const auto quality = health->quality(); + switch (quality) { + case PasswordHealth::Quality::Bad: + descr = tr("Bad", "Password quality"); + tip = tr("Bad — password must be changed"); + qualityColor.setNamedColor("red"); + break; + + case PasswordHealth::Quality::Poor: + descr = tr("Poor", "Password quality"); + tip = tr("Poor — password should be changed"); + qualityColor.setNamedColor("orange"); + break; + + case PasswordHealth::Quality::Weak: + descr = tr("Weak", "Password quality"); + tip = tr("Weak — consider changing the password"); + qualityColor.setNamedColor("yellow"); + break; + + case PasswordHealth::Quality::Good: + case PasswordHealth::Quality::Excellent: + qualityColor.setNamedColor("green"); + break; + } + + auto row = QList<QStandardItem*>(); + row << new QStandardItem(descr); + row << new QStandardItem(entry->iconPixmap(), entry->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); + 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(); + + const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); })); + if (health->items().empty()) { + // No findings + m_referencesModel->clear(); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!")); + } else { + // Show our findings + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score") + << tr("Reason")); + for (const auto& item : health->items()) { + addHealthRow(item->health, item->group, item->entry); + } + } + + m_ui->healthcheckTableView->resizeRowsToContents(); +} + +void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + const auto row = m_rowToEntry[index.row()]; + const auto group = row.first; + const auto entry = row.second; + if (group && entry) { + emit entryActivated(group, const_cast<Entry*>(entry)); + } +} + +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 0000000000..bf0cf531e4 --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -0,0 +1,70 @@ +/* + * 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 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(const Group* group, Entry* entry); + +public slots: + void calculateHealth(); + void emitEntryActivated(const QModelIndex& index); + +private: + void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*); + + QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui; + + bool m_healthCalculated = false; + QIcon m_errorIcon; + QScopedPointer<QStandardItemModel> m_referencesModel; + QSharedPointer<Database> m_db; + QList<QPair<const Group*, const Entry*>> m_rowToEntry; +}; + +#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui new file mode 100644 index 0000000000..48d8df07fa --- /dev/null +++ b/src/gui/reports/ReportsWidgetHealthcheck.ui @@ -0,0 +1,79 @@ +<?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>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="healthcheckGroupBox"> + <property name="title"> + <string>Health Check</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTableView" name="healthcheckTableView"> + <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>true</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 reason to show additional details. Double-click entries to edit.</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp similarity index 86% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp rename to src/gui/reports/ReportsWidgetStatistics.cpp index b02741adbc..bc642af786 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -15,15 +15,15 @@ * 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/Group.h" #include "core/Metadata.h" -#include "zxcvbn.h" +#include "core/PasswordHealth.h" #include <QFileInfo> #include <QHash> @@ -48,6 +48,7 @@ namespace // 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 +93,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,7 +134,7 @@ 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; } @@ -138,9 +147,9 @@ namespace }; } // namespace -DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* parent) +ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent) : QWidget(parent) - , m_ui(new Ui::DatabaseSettingsWidgetStatistics()) + , m_ui(new Ui::ReportsWidgetStatistics()) , m_errIcon(FilePath::instance()->icon("status", "dialog-error")) { m_ui->setupUi(this); @@ -148,14 +157,15 @@ DatabaseSettingsWidgetStatistics::DatabaseSettingsWidgetStatistics(QWidget* pare 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 +180,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 +188,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 +199,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()); @@ -231,7 +241,7 @@ void DatabaseSettingsWidgetStatistics::calculateStats() 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 similarity index 74% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.h rename to src/gui/reports/ReportsWidgetStatistics.h index 2bd42f13d0..cc11a75f56 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 slots: void addStatsRow(QString name, QString value, bool bad = false, QString badMsg = ""); }; -#endif // KEEPASSXC_DATABASESETTINGSWIDGETSTATISTICS_H +#endif // KEEPASSXC_REPORTSWIDGETSTATISTICS_H diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui b/src/gui/reports/ReportsWidgetStatistics.ui similarity index 94% rename from src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui rename to src/gui/reports/ReportsWidgetStatistics.ui index ed9d6346e6..1f3bf5fea9 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetStatistics.ui +++ b/src/gui/reports/ReportsWidgetStatistics.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>DatabaseSettingsWidgetStatistics</class> - <widget class="QWidget" name="DatabaseSettingsWidgetStatistics"> + <class>ReportsWidgetStatistics</class> + <widget class="QWidget" name="ReportsWidgetStatistics"> <property name="geometry"> <rect> <x>0</x> diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fc27f48d33..c3f1c0e22b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -176,6 +176,9 @@ add_unit_test(NAME testmerge SOURCES TestMerge.cpp add_unit_test(NAME testpasswordgenerator SOURCES TestPasswordGenerator.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testpasswordhealth SOURCES TestPasswordHealth.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testpassphrasegenerator SOURCES TestPassphraseGenerator.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestPasswordHealth.cpp b/tests/TestPasswordHealth.cpp new file mode 100644 index 0000000000..238b78b924 --- /dev/null +++ b/tests/TestPasswordHealth.cpp @@ -0,0 +1,65 @@ +/* + * 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 "TestPasswordHealth.h" +#include "TestGlobal.h" + +#include "core/PasswordHealth.h" + +QTEST_GUILESS_MAIN(TestPasswordHealth) + +void TestPasswordHealth::initTestCase() +{ +} + +void TestPasswordHealth::testNoDb() +{ + const auto empty = PasswordHealth(""); + QCOMPARE(empty.score(), 0); + QCOMPARE(empty.entropy(), 0.0); + QCOMPARE(empty.quality(), PasswordHealth::Quality::Bad); + QVERIFY(!empty.scoreReason().isEmpty()); + QVERIFY(!empty.scoreDetails().isEmpty()); + + const auto poor = PasswordHealth("secret"); + QCOMPARE(poor.score(), 6); + QCOMPARE(int(poor.entropy()), 6); + QCOMPARE(poor.quality(), PasswordHealth::Quality::Poor); + QVERIFY(!poor.scoreReason().isEmpty()); + QVERIFY(!poor.scoreDetails().isEmpty()); + + const auto weak = PasswordHealth("Yohb2ChR4"); + QCOMPARE(weak.score(), 47); + QCOMPARE(int(weak.entropy()), 47); + QCOMPARE(weak.quality(), PasswordHealth::Quality::Weak); + QVERIFY(!weak.scoreReason().isEmpty()); + QVERIFY(!weak.scoreDetails().isEmpty()); + + const auto good = PasswordHealth("MIhIN9UKrgtPL2hp"); + QCOMPARE(good.score(), 78); + QCOMPARE(int(good.entropy()), 78); + QCOMPARE(good.quality(), PasswordHealth::Quality::Good); + QVERIFY(good.scoreReason().isEmpty()); + QVERIFY(good.scoreDetails().isEmpty()); + + const auto excellent = PasswordHealth("prompter-ream-oversleep-step-extortion-quarrel-reflected-prefix"); + QCOMPARE(excellent.score(), 164); + QCOMPARE(int(excellent.entropy()), 164); + QCOMPARE(excellent.quality(), PasswordHealth::Quality::Excellent); + QVERIFY(excellent.scoreReason().isEmpty()); + QVERIFY(excellent.scoreDetails().isEmpty()); +} diff --git a/tests/TestPasswordHealth.h b/tests/TestPasswordHealth.h new file mode 100644 index 0000000000..2d887a7de3 --- /dev/null +++ b/tests/TestPasswordHealth.h @@ -0,0 +1,32 @@ +/* + * 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_TESTPASSWORDHEALTH_H +#define KEEPASSX_TESTPASSWORDHEALTH_H + +#include <QObject> + +class TestPasswordHealth : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testNoDb(); +}; + +#endif // KEEPASSX_TESTPASSWORDHEALTH_H diff --git a/utils/makeicons.sh b/utils/makeicons.sh index 6efc608eed..887874161b 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -99,6 +99,7 @@ map() { group-edit) echo folder-edit-outline ;; group-empty-trash) echo trash-can-outline ;; group-new) echo folder-plus-outline ;; + health) echo heart-pulse ;; help-about) echo information-outline ;; internet-web-browser) echo web ;; key-enter) echo keyboard-variant ;; From c4270001842512084e670c07eb68dbde00ffb170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfram=20R=C3=B6sler?= <wolfram@roesler-ac.de> Date: Thu, 30 Jan 2020 21:09:29 +0100 Subject: [PATCH 053/215] Remove result cache from the HealthChecker class The way the class is currently being used, the cache never does anything (because evaluate is never invoked twice for the same entry), so according to YAGNI it has to go. Fixes #551 --- src/core/PasswordHealth.cpp | 10 +++------- src/core/PasswordHealth.h | 6 ++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/core/PasswordHealth.cpp b/src/core/PasswordHealth.cpp index 58e4e42af5..c179db77ca 100644 --- a/src/core/PasswordHealth.cpp +++ b/src/core/PasswordHealth.cpp @@ -116,17 +116,13 @@ HealthChecker::HealthChecker(QSharedPointer<Database> db) * Returns the health of the password in `entry`, considering * password entropy, re-use, expiration, etc. */ -QSharedPointer<PasswordHealth> HealthChecker::evaluate(const Entry* entry) +QSharedPointer<PasswordHealth> HealthChecker::evaluate(const Entry* entry) const { + // Pointer sanity check if (!entry) { return {}; } - // Return from cache if we saw it before - if (m_cache.contains(entry->uuid())) { - return m_cache[entry->uuid()]; - } - // First analyse the password itself const auto pwd = entry->password(); auto health = QSharedPointer<PasswordHealth>(new PasswordHealth(pwd)); @@ -184,5 +180,5 @@ QSharedPointer<PasswordHealth> HealthChecker::evaluate(const Entry* entry) } // Return the result - return m_cache.insert(entry->uuid(), health).value(); + return health; } diff --git a/src/core/PasswordHealth.h b/src/core/PasswordHealth.h index ca7f0236ec..70f83eee70 100644 --- a/src/core/PasswordHealth.h +++ b/src/core/PasswordHealth.h @@ -101,12 +101,10 @@ class HealthChecker explicit HealthChecker(QSharedPointer<Database>); // Get the health status of an entry in the database - QSharedPointer<PasswordHealth> evaluate(const Entry* entry); + QSharedPointer<PasswordHealth> evaluate(const Entry* entry) const; private: - // Result cache (first=entry UUID) - QHash<QUuid, QSharedPointer<PasswordHealth>> m_cache; - // first = password, second = entries that use it + // To determine password re-use: first = password, second = entries that use it QHash<QString, QStringList> m_reuse; }; From c663b5d5fcc3ed9f5af8d6e5e7fdf1e99f4a2c58 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Tue, 7 Jan 2020 22:06:31 -0500 Subject: [PATCH 054/215] Add braces around single line statements * Ran clang-tidy with "readability-braces-around-statements" to find missing braces around statements. --- src/core/CsvParser.cpp | 59 +++++++++++++++++---------- src/core/Entry.cpp | 6 ++- src/core/Tools.cpp | 3 +- src/format/HtmlExporter.cpp | 3 +- src/gui/FileDialog.cpp | 3 +- src/gui/csvImport/CsvImportWidget.cpp | 17 ++++---- src/gui/csvImport/CsvParserModel.cpp | 26 ++++++++---- src/gui/group/GroupView.cpp | 5 ++- src/gui/widgets/ElidedLabel.cpp | 9 ++-- tests/TestCsvParser.cpp | 3 +- tests/TestSymmetricCipher.cpp | 12 ++++-- tests/TestYkChallengeResponseKey.cpp | 6 ++- 12 files changed, 98 insertions(+), 54 deletions(-) diff --git a/src/core/CsvParser.cpp b/src/core/CsvParser.cpp index 7e49294814..adda56e495 100644 --- a/src/core/CsvParser.cpp +++ b/src/core/CsvParser.cpp @@ -67,15 +67,17 @@ bool CsvParser::parse(QFile* device) appendStatusMsg(QObject::tr("NULL device"), true); return false; } - if (!readFile(device)) + if (!readFile(device)) { return false; + } return parseFile(); } bool CsvParser::readFile(QFile* device) { - if (device->isOpen()) + if (device->isOpen()) { device->close(); + } device->open(QIODevice::ReadOnly); if (!Tools::readAllFromDevice(device, m_array)) { @@ -86,8 +88,9 @@ bool CsvParser::readFile(QFile* device) m_array.replace("\r\n", "\n"); m_array.replace("\r", "\n"); - if (0 == m_array.size()) + if (0 == m_array.size()) { appendStatusMsg(QObject::tr("file empty").append("\n")); + } m_isFileLoaded = true; } return m_isFileLoaded; @@ -124,8 +127,9 @@ bool CsvParser::parseFile() { parseRecord(); while (!m_isEof) { - if (!skipEndline()) + if (!skipEndline()) { appendStatusMsg(QObject::tr("malformed string"), true); + } m_currRow++; m_currCol = 1; parseRecord(); @@ -146,15 +150,17 @@ void CsvParser::parseRecord() getChar(m_ch); } while (isSeparator(m_ch) && !m_isEof); - if (!m_isEof) + if (!m_isEof) { ungetChar(); + } if (isEmptyRow(row)) { row.clear(); return; } m_table.push_back(row); - if (m_maxCols < row.size()) + if (m_maxCols < row.size()) { m_maxCols = row.size(); + } m_currCol++; } @@ -163,10 +169,11 @@ void CsvParser::parseField(CsvRow& row) QString field; peek(m_ch); if (!isTerminator(m_ch)) { - if (isQualifier(m_ch)) + if (isQualifier(m_ch)) { parseQuoted(field); - else + } else { parseSimple(field); + } } row.push_back(field); } @@ -179,8 +186,9 @@ void CsvParser::parseSimple(QString& s) s.append(c); getChar(c); } - if (!m_isEof) + if (!m_isEof) { ungetChar(); + } } void CsvParser::parseQuoted(QString& s) @@ -189,17 +197,20 @@ void CsvParser::parseQuoted(QString& s) getChar(m_ch); parseEscaped(s); // getChar(m_ch); - if (!isQualifier(m_ch)) + if (!isQualifier(m_ch)) { appendStatusMsg(QObject::tr("missing closing quote"), true); + } } void CsvParser::parseEscaped(QString& s) { parseEscapedText(s); - while (processEscapeMark(s, m_ch)) + while (processEscapeMark(s, m_ch)) { parseEscapedText(s); - if (!m_isEof) + } + if (!m_isEof) { ungetChar(); + } } void CsvParser::parseEscapedText(QString& s) @@ -233,8 +244,9 @@ bool CsvParser::processEscapeMark(QString& s, QChar c) } } else { // double quote syntax, e.g. "" - if (!isQualifier(c)) + if (!isQualifier(c)) { return false; + } peek(c2); if (!m_isEof) { // not EOF, can read one char if (isQualifier(c2)) { @@ -294,16 +306,18 @@ void CsvParser::ungetChar() void CsvParser::peek(QChar& c) { getChar(c); - if (!m_isEof) + if (!m_isEof) { ungetChar(); + } } bool CsvParser::isQualifier(const QChar& c) const { - if (true == m_isBackslashSyntax && (c != m_qualifier)) + if (true == m_isBackslashSyntax && (c != m_qualifier)) { return (c == '\\'); - else + } else { return (c == m_qualifier); + } } bool CsvParser::isComment() @@ -312,12 +326,13 @@ bool CsvParser::isComment() QChar c2; qint64 pos = m_ts.pos(); - do + do { getChar(c2); - while ((isSpace(c2) || isTab(c2)) && (!m_isEof)); + } while ((isSpace(c2) || isTab(c2)) && (!m_isEof)); - if (c2 == m_comment) + if (c2 == m_comment) { result = true; + } m_ts.seek(pos); return result; } @@ -330,9 +345,11 @@ bool CsvParser::isText(QChar c) const bool CsvParser::isEmptyRow(const CsvRow& row) const { CsvRow::const_iterator it = row.constBegin(); - for (; it != row.constEnd(); ++it) - if (((*it) != "\n") && ((*it) != "")) + for (; it != row.constEnd(); ++it) { + if (((*it) != "\n") && ((*it) != "")) { return false; + } + } return true; } diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 4e6911c37a..d53fa64689 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -779,8 +779,9 @@ Entry* Entry::clone(CloneFlags flags) const entry->m_data.timeInfo.setLocationChanged(now); } - if (flags & CloneRenameTitle) + if (flags & CloneRenameTitle) { entry->setTitle(tr("%1 - Clone").arg(entry->title())); + } entry->setUpdateTimeinfo(true); @@ -1075,8 +1076,9 @@ QString Entry::resolvePlaceholder(const QString& placeholder) const QString Entry::resolveUrlPlaceholder(const QString& str, Entry::PlaceholderType placeholderType) const { - if (str.isEmpty()) + if (str.isEmpty()) { return QString(); + } const QUrl qurl(str); switch (placeholderType) { diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 5d8889fae3..1b3eafcca8 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -113,8 +113,9 @@ namespace Tools extensions += "\n- " + QObject::tr("Secret Service Integration"); #endif - if (extensions.isEmpty()) + if (extensions.isEmpty()) { extensions = " " + QObject::tr("None"); + } debugInfo.append(QObject::tr("Enabled extensions:").append(extensions).append("\n")); return debugInfo; diff --git a/src/format/HtmlExporter.cpp b/src/format/HtmlExporter.cpp index 457623ec98..cd3654e36d 100644 --- a/src/format/HtmlExporter.cpp +++ b/src/format/HtmlExporter.cpp @@ -28,8 +28,9 @@ namespace { QString PixmapToHTML(const QPixmap& pixmap) { - if (pixmap.isNull()) + if (pixmap.isNull()) { return ""; + } // Based on https://stackoverflow.com/a/6621278 QByteArray a; diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp index 12f5827752..c2b774ceba 100644 --- a/src/gui/FileDialog.cpp +++ b/src/gui/FileDialog.cpp @@ -65,8 +65,9 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent, const auto& workingDir = dir.isEmpty() ? config()->get("LastDir").toString() : dir; auto results = QFileDialog::getOpenFileNames(parent, caption, workingDir, 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 diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index 6e6c282b9f..85c9e591ca 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -294,27 +294,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/CsvParserModel.cpp b/src/gui/csvImport/CsvParserModel.cpp index a6c24667d1..d18db87c5c 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/group/GroupView.cpp b/src/gui/group/GroupView.cpp index 33c5916961..48945085b3 100644 --- a/src/gui/group/GroupView.cpp +++ b/src/gui/group/GroupView.cpp @@ -155,10 +155,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/widgets/ElidedLabel.cpp b/src/gui/widgets/ElidedLabel.cpp index 749f075c8d..5e71fbcebf 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/tests/TestCsvParser.cpp b/tests/TestCsvParser.cpp index f31e30414e..758c31eccf 100644 --- a/tests/TestCsvParser.cpp +++ b/tests/TestCsvParser.cpp @@ -30,8 +30,9 @@ void TestCsvParser::initTestCase() void TestCsvParser::init() { file.reset(new QTemporaryFile()); - if (not file->open()) + if (not file->open()) { QFAIL("Cannot open file!"); + } parser->setBackslashSyntax(false); parser->setComment('#'); parser->setFieldSeparator(','); diff --git a/tests/TestSymmetricCipher.cpp b/tests/TestSymmetricCipher.cpp index 752fc09dfb..173f328ee0 100644 --- a/tests/TestSymmetricCipher.cpp +++ b/tests/TestSymmetricCipher.cpp @@ -289,14 +289,16 @@ void TestSymmetricCipher::testTwofish256CbcEncryption() QCOMPARE(cipher.blockSize(), 16); for (int j = 0; j < 5000; ++j) { ctCur = cipher.process(ptNext, &ok); - if (!ok) + if (!ok) { break; + } ptNext = ctPrev; ctPrev = ctCur; ctCur = cipher.process(ptNext, &ok); - if (!ok) + if (!ok) { break; + } ptNext = ctPrev; ctPrev = ctCur; } @@ -342,13 +344,15 @@ void TestSymmetricCipher::testTwofish256CbcDecryption() QCOMPARE(cipher.blockSize(), 16); for (int j = 0; j < 5000; ++j) { ptCur = cipher.process(ctNext, &ok); - if (!ok) + if (!ok) { break; + } ctNext = ptCur; ptCur = cipher.process(ctNext, &ok); - if (!ok) + if (!ok) { break; + } ctNext = ptCur; } diff --git a/tests/TestYkChallengeResponseKey.cpp b/tests/TestYkChallengeResponseKey.cpp index 0d6f9b5c38..a4dd762701 100644 --- a/tests/TestYkChallengeResponseKey.cpp +++ b/tests/TestYkChallengeResponseKey.cpp @@ -84,12 +84,14 @@ void TestYubiKeyChalResp::ykDetected(int slot, bool blocking) { Q_UNUSED(blocking); - if (slot > 0) + if (slot > 0) { m_detected++; + } /* Key used for later testing */ - if (!m_key) + if (!m_key) { m_key.reset(new YkChallengeResponseKey(slot, blocking)); + } } void TestYubiKeyChalResp::deinit() From f62e0534a23c2030c9c22e6f3db25148fcb486fd Mon Sep 17 00:00:00 2001 From: Carlo Teubner <carlo@cteubner.net> Date: Sun, 24 Nov 2019 14:40:16 +0000 Subject: [PATCH 055/215] Fixes for minor issues found by static analysis Mostly style issues. I used the following tools to find most of these: - lgtm.com - clang-tidy - cpplint - cppcheck --- src/autotype/xcb/AutoTypeXCB.cpp | 17 ++++++----------- src/core/ScreenLockListenerWin.h | 2 +- src/gui/EditWidgetIcons.cpp | 1 + src/gui/MainWindow.cpp | 14 ++++++-------- src/gui/MessageBox.cpp | 4 ++-- tests/TestSymmetricCipher.cpp | 4 ++-- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/autotype/xcb/AutoTypeXCB.cpp b/src/autotype/xcb/AutoTypeXCB.cpp index 94a132d40c..d2d757b4e6 100644 --- a/src/autotype/xcb/AutoTypeXCB.cpp +++ b/src/autotype/xcb/AutoTypeXCB.cpp @@ -485,10 +485,6 @@ KeySym AutoTypePlatformX11::keyToKeySym(Qt::Key key) */ void AutoTypePlatformX11::updateKeymap() { - int keycode, inx; - int mod_index, mod_key; - XModifierKeymap* modifiers; - if (m_xkb) { XkbFreeKeyboard(m_xkb, XkbAllComponentsMask, True); } @@ -500,10 +496,9 @@ void AutoTypePlatformX11::updateKeymap() m_keysymTable = XGetKeyboardMapping(m_dpy, m_minKeycode, m_maxKeycode - m_minKeycode + 1, &m_keysymPerKeycode); /* determine the keycode to use for remapped keys */ - inx = (m_remapKeycode - m_minKeycode) * m_keysymPerKeycode; if (m_remapKeycode == 0 || !isRemapKeycodeValid()) { - for (keycode = m_minKeycode; keycode <= m_maxKeycode; keycode++) { - inx = (keycode - m_minKeycode) * m_keysymPerKeycode; + for (int keycode = m_minKeycode; keycode <= m_maxKeycode; keycode++) { + int inx = (keycode - m_minKeycode) * m_keysymPerKeycode; if (m_keysymTable[inx] == NoSymbol) { m_remapKeycode = keycode; m_currentRemapKeysym = NoSymbol; @@ -513,11 +508,11 @@ void AutoTypePlatformX11::updateKeymap() } /* determine the keycode to use for modifiers */ - modifiers = XGetModifierMapping(m_dpy); - for (mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) { + XModifierKeymap* modifiers = XGetModifierMapping(m_dpy); + for (int mod_index = ShiftMapIndex; mod_index <= Mod5MapIndex; mod_index++) { m_modifier_keycode[mod_index] = 0; - for (mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) { - keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key]; + for (int mod_key = 0; mod_key < modifiers->max_keypermod; mod_key++) { + int keycode = modifiers->modifiermap[mod_index * modifiers->max_keypermod + mod_key]; if (keycode) { m_modifier_keycode[mod_index] = keycode; break; diff --git a/src/core/ScreenLockListenerWin.h b/src/core/ScreenLockListenerWin.h index 523ae5d0bc..ba7c98cd3a 100644 --- a/src/core/ScreenLockListenerWin.h +++ b/src/core/ScreenLockListenerWin.h @@ -29,7 +29,7 @@ class ScreenLockListenerWin : public ScreenLockListenerPrivate, public QAbstract public: explicit ScreenLockListenerWin(QWidget* parent = nullptr); ~ScreenLockListenerWin(); - virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override; + bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override; private: void* m_powerNotificationHandle; diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index 83bc0fc350..054156066d 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) { } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 2d52331ff3..2f54723633 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -214,9 +214,9 @@ MainWindow::MainWindow() 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())); + connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); + connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); + connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); // clang-format on m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); @@ -483,10 +483,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())); @@ -1322,7 +1320,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()) { diff --git a/src/gui/MessageBox.cpp b/src/gui/MessageBox.cpp index 7d2b2a5169..317754a62d 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/tests/TestSymmetricCipher.cpp b/tests/TestSymmetricCipher.cpp index 173f328ee0..bc872a5101 100644 --- a/tests/TestSymmetricCipher.cpp +++ b/tests/TestSymmetricCipher.cpp @@ -279,7 +279,6 @@ void TestSymmetricCipher::testTwofish256CbcEncryption() QByteArray::fromHex("6F725C5950133F82EF021A94CADC8508")}; SymmetricCipher cipher(SymmetricCipher::Twofish, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); - bool ok; for (int i = 0; i < keys.size(); ++i) { QVERIFY(cipher.init(keys[i], ivs[i])); @@ -287,6 +286,7 @@ void TestSymmetricCipher::testTwofish256CbcEncryption() QByteArray ctPrev = ivs[i]; QByteArray ctCur; QCOMPARE(cipher.blockSize(), 16); + bool ok = false; for (int j = 0; j < 5000; ++j) { ctCur = cipher.process(ptNext, &ok); if (!ok) { @@ -335,13 +335,13 @@ void TestSymmetricCipher::testTwofish256CbcDecryption() QByteArray::fromHex("4C81F5BDC1081170FF96F50B1F76A566")}; SymmetricCipher cipher(SymmetricCipher::Twofish, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); - bool ok; for (int i = 0; i < keys.size(); ++i) { cipher.init(keys[i], ivs[i]); QByteArray ctNext = cipherTexts[i]; QByteArray ptCur; QCOMPARE(cipher.blockSize(), 16); + bool ok = false; for (int j = 0; j < 5000; ++j) { ptCur = cipher.process(ctNext, &ok); if (!ok) { From f227a2d52946b3a7d4c232cfcee675e725aeaf04 Mon Sep 17 00:00:00 2001 From: humanoid <sami.vanttinen@protonmail.com> Date: Sun, 9 Feb 2020 10:06:19 +0200 Subject: [PATCH 056/215] Fix browser-like DbTab experience on macOS and Windows * macOS and Windows browsers do not use `Alt+#` to change tabs. Windows uses `Ctrl` and macOS uses `Command`. Linux uses `Alt`. * Remove shortcut for `Key+0` and assign `Key+9` as last tab selection * Streamline tab selection code in MainWindow --- src/gui/MainWindow.cpp | 76 ++++++++++++++++++++---------------------- src/gui/MainWindow.h | 3 +- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 2f54723633..b0dec4bf5a 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -302,26 +302,31 @@ MainWindow::MainWindow() 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())); - new QShortcut(Qt::ALT + Qt::Key_0, this, SLOT(selectLastDatabaseTab())); - auto shortcut = new QShortcut(Qt::ALT + Qt::Key_1, this); + // 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(Qt::ALT + Qt::Key_2, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_3, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(2); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_3, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_4, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(3); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_4, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_5, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(4); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_5, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_6, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(5); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_6, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_7, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(6); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_7, this); + shortcut = new QShortcut(dbTabModifier + Qt::Key_8, this); connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(7); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_8, this); - connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(8); }); - shortcut = new QShortcut(Qt::ALT + Qt::Key_9, this); - connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(9); }); + shortcut = new QShortcut(dbTabModifier + Qt::Key_9, this); + connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(m_ui->tabWidget->count() - 1); }); // Toggle password and username visibility in entry view new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); @@ -974,45 +979,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); } - } -} -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); - } + m_ui->tabWidget->setCurrentIndex(tabIndex); } } -void MainWindow::selectDatabaseTab(int tabIndex) +void MainWindow::selectNextDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - if (tabIndex <= m_ui->tabWidget->count()) { - m_ui->tabWidget->setCurrentIndex(--tabIndex); - } - } + selectDatabaseTab(m_ui->tabWidget->currentIndex() + 1, true); } -void MainWindow::selectLastDatabaseTab() +void MainWindow::selectPreviousDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->count() - 1; - m_ui->tabWidget->setCurrentIndex(index); - } + selectDatabaseTab(m_ui->tabWidget->currentIndex() - 1, true); } void MainWindow::databaseTabChanged(int tabIndex) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 888b5747c1..0e74edf60c 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -122,8 +122,7 @@ private slots: void showErrorMessage(const QString& message); void selectNextDatabaseTab(); void selectPreviousDatabaseTab(); - void selectDatabaseTab(int tabIndex); - void selectLastDatabaseTab(); + void selectDatabaseTab(int tabIndex, bool wrap = false); void togglePasswordsHidden(); void toggleUsernamesHidden(); void obtainContextFocusLock(); From 8dba308d54880a9fef5a774dd60005200b22a817 Mon Sep 17 00:00:00 2001 From: varjolintu <sami.vanttinen@protonmail.com> Date: Tue, 4 Feb 2020 12:28:26 +0200 Subject: [PATCH 057/215] Do not add duplicate entries --- src/browser/BrowserService.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index b182b535ea..35db02a057 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -610,7 +610,9 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& // Search for additional URL's starting with KP2A_URL for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), url, submitUrl)) { + if (key.startsWith(ADDITIONAL_URL) && + handleURL(entry->attributes()->value(key), url, submitUrl) && + !entries.contains(entry)) { entries.append(entry); continue; } From 8dbd5b11eb43c274b346d1ad69441082a5c3c029 Mon Sep 17 00:00:00 2001 From: varjolintu <sami.vanttinen@protonmail.com> Date: Tue, 4 Feb 2020 17:40:44 +0200 Subject: [PATCH 058/215] Code format --- src/browser/BrowserService.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 35db02a057..e0c896eca7 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -610,9 +610,8 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& // Search for additional URL's starting with KP2A_URL for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) && - handleURL(entry->attributes()->value(key), url, submitUrl) && - !entries.contains(entry)) { + if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), url, submitUrl) + && !entries.contains(entry)) { entries.append(entry); continue; } From c306fb55ae6c0734954ee0692216ead066be7b65 Mon Sep 17 00:00:00 2001 From: Jonathan <jonathanievans0@gmail.com> Date: Mon, 17 Feb 2020 12:28:08 +0000 Subject: [PATCH 059/215] Distinguish meaning of src directory and git root directory (#4337) * Remove 'cd git-project-directory' from INSTALL.md as it is unnecessary --- INSTALL.md | 1 - 1 file changed, 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 1b3d88fe6e..f452d84dd4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -68,7 +68,6 @@ git checkout master Navigate to the directory where you have downloaded KeePassXC and type these commands: ``` -cd directory-where-sources-live mkdir build cd build cmake -DWITH_XC_ALL=ON .. From 9f3516a4da8730cbb7db738c6357db738fa3aab7 Mon Sep 17 00:00:00 2001 From: varjolintu <sami.vanttinen@protonmail.com> Date: Thu, 20 Feb 2020 20:38:07 +0200 Subject: [PATCH 060/215] Icon downloader button list check --- src/core/IconDownloader.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/IconDownloader.cpp b/src/core/IconDownloader.cpp index fe346becd7..1d9bd01ad0 100644 --- a/src/core/IconDownloader.cpp +++ b/src/core/IconDownloader.cpp @@ -126,6 +126,10 @@ void IconDownloader::setUrl(const QString& entryUrl) void IconDownloader::download() { + if (m_urlsToTry.isEmpty()) { + return; + } + if (!m_timeout.isActive()) { int timeout = config()->get("FaviconDownloadTimeout", 10).toInt(); m_timeout.start(timeout * 1000); From cb6b0dde276577d2a66a80c1ee25884c7f44e676 Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Tue, 28 Jan 2020 20:42:36 +0200 Subject: [PATCH 061/215] Fix hiding entry edit pages Fixes regression caused by 9477437256c8b34d0bf124c07abf7e08690dd824 --- src/gui/EditWidget.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index f7030c9d72..cfae5d7e68 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -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 != nullptr && scrollArea->widget() == widget) { + index = i; + break; + } + } + if (index != -1) { m_ui->categoryList->setCategoryHidden(index, hidden); } From 40ad211f3e70b715f3960097563730926a07fe57 Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Tue, 28 Jan 2020 20:46:23 +0200 Subject: [PATCH 062/215] Allow toggling SSH Agent integration without restart - use Q_GLOBAL_STATIC for singleton - move all configuration to SSHAgent class - various cleanups to agent code Fixes #1196 --- src/gui/DatabaseWidget.cpp | 6 +- src/gui/EditWidget.cpp | 2 +- src/gui/MainWindow.cpp | 3 +- src/gui/entry/EditEntryWidget.cpp | 22 +++--- src/gui/entry/EditEntryWidget.h | 1 - src/sshagent/AgentSettingsWidget.cpp | 27 +++---- src/sshagent/AgentSettingsWidget.ui | 2 +- src/sshagent/SSHAgent.cpp | 107 +++++++++++++++++++-------- src/sshagent/SSHAgent.h | 26 ++++--- 9 files changed, 124 insertions(+), 72 deletions(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index fd579b04a0..d7521f3b2b 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -217,9 +217,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent) m_searchLimitGroup = config()->get("SearchLimitGroup", false).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(databaseLocked()), sshAgent(), SLOT(databaseModeChanged())); + connect(this, SIGNAL(databaseUnlocked()), sshAgent(), SLOT(databaseModeChanged())); } #endif diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index cfae5d7e68..f9bcbb5af6 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -74,7 +74,7 @@ void EditWidget::setPageHidden(QWidget* widget, bool hidden) for (int i = 0; i < m_ui->stackedWidget->count(); i++) { auto* scrollArea = qobject_cast<QScrollArea*>(m_ui->stackedWidget->widget(i)); - if (scrollArea != nullptr && scrollArea->widget() == widget) { + if (scrollArea && scrollArea->widget() == widget) { index = i; break; } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b0dec4bf5a..b225165a67 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -189,8 +189,7 @@ MainWindow::MainWindow() #endif #ifdef WITH_XC_SSHAGENT - SSHAgent::init(this); - connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); + connect(sshAgent(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 2880e8ba07..7b1e85472f 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -107,7 +107,6 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) #ifdef WITH_XC_SSHAGENT setupSSHAgent(); - m_sshAgentEnabled = config()->get("SSHAgent", false).toBool(); #endif #ifdef WITH_XC_BROWSER @@ -451,7 +450,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())); @@ -628,11 +627,11 @@ 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()); } } @@ -700,8 +699,8 @@ void EditEntryWidget::addKeyToAgent() KeeAgentSettings settings; toKeeAgentSettings(settings); - if (!SSHAgent::instance()->addIdentity(key, settings)) { - showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error); + if (!sshAgent()->addIdentity(key, settings)) { + showMessage(sshAgent()->errorString(), MessageWidget::Error); return; } } @@ -714,8 +713,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; } } @@ -792,6 +791,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); @@ -903,7 +905,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) updateAutoTypeEnabled(); #ifdef WITH_XC_SSHAGENT - if (m_sshAgentEnabled) { + if (sshAgent()->isEnabled()) { updateSSHAgent(); } #endif @@ -1090,7 +1092,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const entry->autoTypeAssociations()->copyDataFrom(m_autoTypeAssoc); #ifdef WITH_XC_SSHAGENT - if (m_sshAgentEnabled) { + if (sshAgent()->isEnabled()) { m_sshAgentSettings.toEntry(entry); } #endif diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 783bb3cb94..dafc4a2830 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -164,7 +164,6 @@ private slots: bool m_create; bool m_history; #ifdef WITH_XC_SSHAGENT - bool m_sshAgentEnabled; KeeAgentSettings m_sshAgentSettings; #endif const QScopedPointer<Ui::EditEntryWidgetMain> m_mainUi; diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp index f95a198455..e06929195e 100644 --- a/src/sshagent/AgentSettingsWidget.cpp +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -33,8 +33,7 @@ AgentSettingsWidget::AgentSettingsWidget(QWidget* parent) #else m_ui->sshAuthSockWidget->setVisible(false); #endif - auto sshAgentEnabled = config()->get("SSHAgent", false).toBool(); - m_ui->sshAuthSockMessageWidget->setVisible(sshAgentEnabled); + m_ui->sshAuthSockMessageWidget->setVisible(sshAgent()->isEnabled()); m_ui->sshAuthSockMessageWidget->setCloseButtonVisible(false); m_ui->sshAuthSockMessageWidget->setAutoHideTimeout(-1); } @@ -45,20 +44,21 @@ AgentSettingsWidget::~AgentSettingsWidget() void AgentSettingsWidget::loadSettings() { - auto sshAgentEnabled = config()->get("SSHAgent", false).toBool(); + auto sshAgentEnabled = sshAgent()->isEnabled(); + m_ui->enableSSHAgentCheckBox->setChecked(sshAgentEnabled); #ifdef Q_OS_WIN - m_ui->useOpenSSHCheckBox->setChecked(config()->get("SSHAgentOpenSSH", false).toBool()); + m_ui->useOpenSSHCheckBox->setChecked(sshAgent()->useOpenSSH()); #else - auto sshAuthSock = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); - auto sshAuthSockOverride = config()->get("SSHAuthSockOverride", "").toString(); + auto sshAuthSock = sshAgent()->socketPath(false); + auto sshAuthSockOverride = sshAgent()->authSockOverride(); m_ui->sshAuthSockLabel->setText(sshAuthSock.isEmpty() ? tr("(empty)") : sshAuthSock); m_ui->sshAuthSockOverrideEdit->setText(sshAuthSockOverride); #endif - if (sshAgentEnabled) { - m_ui->sshAuthSockMessageWidget->setVisible(true); + m_ui->sshAuthSockMessageWidget->setVisible(sshAgentEnabled); + if (sshAgentEnabled) { #ifndef Q_OS_WIN if (sshAuthSock.isEmpty() && sshAuthSockOverride.isEmpty()) { m_ui->sshAuthSockMessageWidget->showMessage( @@ -68,20 +68,21 @@ void AgentSettingsWidget::loadSettings() return; } #endif - if (SSHAgent::instance()->testConnection()) { + if (sshAgent()->testConnection()) { m_ui->sshAuthSockMessageWidget->showMessage(tr("SSH Agent connection is working!"), MessageWidget::Positive); } else { - m_ui->sshAuthSockMessageWidget->showMessage(SSHAgent::instance()->errorString(), MessageWidget::Error); + m_ui->sshAuthSockMessageWidget->showMessage(sshAgent()->errorString(), MessageWidget::Error); } } } void AgentSettingsWidget::saveSettings() { - config()->set("SSHAgent", m_ui->enableSSHAgentCheckBox->isChecked()); - config()->set("SSHAuthSockOverride", m_ui->sshAuthSockOverrideEdit->text()); + auto sshAuthSockOverride = m_ui->sshAuthSockOverrideEdit->text(); + sshAgent()->setAuthSockOverride(sshAuthSockOverride); #ifdef Q_OS_WIN - config()->set("SSHAgentOpenSSH", m_ui->useOpenSSHCheckBox->isChecked()); + sshAgent()->setUseOpenSSH(m_ui->useOpenSSHCheckBox->isChecked()); #endif + sshAgent()->setEnabled(m_ui->enableSSHAgentCheckBox->isChecked()); } diff --git a/src/sshagent/AgentSettingsWidget.ui b/src/sshagent/AgentSettingsWidget.ui index 20142f1c9d..3b8c70fad3 100644 --- a/src/sshagent/AgentSettingsWidget.ui +++ b/src/sshagent/AgentSettingsWidget.ui @@ -26,7 +26,7 @@ <item> <widget class="QCheckBox" name="enableSSHAgentCheckBox"> <property name="text"> - <string>Enable SSH Agent (requires restart)</string> + <string>Enable SSH Agent integration</string> </property> </widget> </item> diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 571f7b99f0..a6aedca370 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -29,46 +29,72 @@ #include <windows.h> #endif -SSHAgent* SSHAgent::m_instance; +Q_GLOBAL_STATIC(SSHAgent, s_sshAgent); -SSHAgent::SSHAgent(QObject* parent) - : QObject(parent) +SSHAgent::~SSHAgent() { -#ifndef Q_OS_WIN - m_socketPath = config()->get("SSHAuthSockOverride", "").toString(); - if (m_socketPath.isEmpty()) { - m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); - } -#else - m_socketPath = "\\\\.\\pipe\\openssh-ssh-agent"; -#endif + removeAllIdentities(); } -SSHAgent::~SSHAgent() +SSHAgent* SSHAgent::instance() { - auto it = m_addedKeys.begin(); - while (it != m_addedKeys.end()) { - // Remove key if requested to remove on lock - if (it.value()) { - OpenSSHKey key = it.key(); - removeIdentity(key); - } - it = m_addedKeys.erase(it); - } + return s_sshAgent; } -SSHAgent* SSHAgent::instance() +bool SSHAgent::isEnabled() const { - if (!m_instance) { - qFatal("Race condition: instance wanted before it was initialized, this is a bug."); + return config()->get("SSHAgent").toBool(); +} + +void SSHAgent::setEnabled(bool enabled) +{ + if (isEnabled() && !enabled) { + removeAllIdentities(); } - return m_instance; + config()->set("SSHAgent", enabled); +} + +QString SSHAgent::authSockOverride() const +{ + return config()->get("SSHAuthSockOverride").toString(); +} + +void SSHAgent::setAuthSockOverride(QString& authSockOverride) +{ + config()->set("SSHAuthSockOverride", authSockOverride); +} + +#ifdef Q_OS_WIN +bool SSHAgent::useOpenSSH() const +{ + return config()->get("SSHAgentOpenSSH").toBool(); } -void SSHAgent::init(QObject* parent) +void SSHAgent::setUseOpenSSH(bool useOpenSSH) { - m_instance = new SSHAgent(parent); + config()->set("SSHAgentOpenSSH", useOpenSSH); +} +#endif + +QString SSHAgent::socketPath(bool allowOverride = true) const +{ + QString socketPath; + +#ifndef Q_OS_WIN + if (allowOverride) { + socketPath = authSockOverride(); + } + + // if the overridden path is empty (no override set), default to environment + if (socketPath.isEmpty()) { + socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); + } +#else + socketPath = "\\\\.\\pipe\\openssh-ssh-agent"; +#endif + + return socketPath; } const QString SSHAgent::errorString() const @@ -79,12 +105,13 @@ const QString SSHAgent::errorString() const bool SSHAgent::isAgentRunning() const { #ifndef Q_OS_WIN - return !m_socketPath.isEmpty(); + QFileInfo socketFileInfo(socketPath()); + return !socketFileInfo.path().isEmpty() && socketFileInfo.exists(); #else - if (!config()->get("SSHAgentOpenSSH").toBool()) { + if (!useOpenSSH()) { return (FindWindowA("Pageant", "Pageant") != nullptr); } else { - return WaitNamedPipe(m_socketPath.toLatin1().data(), 100); + return WaitNamedPipe(socketPath().toLatin1().data(), 100); } #endif } @@ -92,7 +119,7 @@ bool SSHAgent::isAgentRunning() const bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out) { #ifdef Q_OS_WIN - if (!config()->get("SSHAgentOpenSSH").toBool()) { + if (!useOpenSSH()) { return sendMessagePageant(in, out); } #endif @@ -100,7 +127,7 @@ bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out) QLocalSocket socket; BinaryStream stream(&socket); - socket.connectToServer(m_socketPath); + socket.connectToServer(socketPath()); if (!socket.waitForConnected(500)) { m_error = tr("Agent connection failed."); return false; @@ -300,6 +327,22 @@ bool SSHAgent::removeIdentity(OpenSSHKey& key) return sendMessage(requestData, responseData); } +/** + * Remove all identities known to this instance + */ +void SSHAgent::removeAllIdentities() +{ + auto it = m_addedKeys.begin(); + while (it != m_addedKeys.end()) { + // Remove key if requested to remove on lock + if (it.value()) { + OpenSSHKey key = it.key(); + removeIdentity(key); + } + it = m_addedKeys.erase(it); + } +} + /** * Change "remove identity on lock" setting for a key already added to the agent. * Will to nothing if the key has not been added to the agent. diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index 92389112f6..9c76e8e6a9 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -32,14 +32,25 @@ class SSHAgent : public QObject Q_OBJECT public: + ~SSHAgent() override; static SSHAgent* instance(); - static void init(QObject* parent); + + bool isEnabled() const; + void setEnabled(bool enabled); + QString socketPath(bool allowOverride) const; + QString authSockOverride() const; + void setAuthSockOverride(QString& authSockOverride); +#ifdef Q_OS_WIN + bool useOpenSSH() const; + void setUseOpenSSH(bool useOpenSSH); +#endif const QString errorString() const; bool isAgentRunning() const; bool testConnection(); bool addIdentity(OpenSSHKey& key, KeeAgentSettings& settings); bool removeIdentity(OpenSSHKey& key); + void removeAllIdentities(); void setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove); signals: @@ -60,18 +71,10 @@ public slots: const quint8 SSH_AGENT_CONSTRAIN_LIFETIME = 1; const quint8 SSH_AGENT_CONSTRAIN_CONFIRM = 2; - explicit SSHAgent(QObject* parent = nullptr); - ~SSHAgent(); - bool sendMessage(const QByteArray& in, QByteArray& out); #ifdef Q_OS_WIN bool sendMessagePageant(const QByteArray& in, QByteArray& out); -#endif - static SSHAgent* m_instance; - - QString m_socketPath; -#ifdef Q_OS_WIN const quint32 AGENT_MAX_MSGLEN = 8192; const quint32 AGENT_COPYDATA_ID = 0x804e50ba; #endif @@ -80,4 +83,9 @@ public slots: QString m_error; }; +static inline SSHAgent* sshAgent() +{ + return SSHAgent::instance(); +} + #endif // KEEPASSXC_SSHAGENT_H From 409190c85afc90861f4374e039c0e6f6bf213afa Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 23 Feb 2020 22:51:18 -0500 Subject: [PATCH 063/215] Correct issues with building new SSH Agent fixes --- src/sshagent/SSHAgent.cpp | 3 ++- src/sshagent/SSHAgent.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index a6aedca370..3bd3c8df31 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -77,7 +77,7 @@ void SSHAgent::setUseOpenSSH(bool useOpenSSH) } #endif -QString SSHAgent::socketPath(bool allowOverride = true) const +QString SSHAgent::socketPath(bool allowOverride) const { QString socketPath; @@ -91,6 +91,7 @@ QString SSHAgent::socketPath(bool allowOverride = true) const socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); } #else + Q_UNUSED(allowOverride) socketPath = "\\\\.\\pipe\\openssh-ssh-agent"; #endif diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index 9c76e8e6a9..a70af44c88 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -37,7 +37,7 @@ class SSHAgent : public QObject bool isEnabled() const; void setEnabled(bool enabled); - QString socketPath(bool allowOverride) const; + QString socketPath(bool allowOverride = true) const; QString authSockOverride() const; void setAuthSockOverride(QString& authSockOverride); #ifdef Q_OS_WIN From b18838518492b4d3c9de490eac856ca057f21b48 Mon Sep 17 00:00:00 2001 From: Ojas Anand <sigmaupsilon@users.noreply.github.com> Date: Thu, 27 Feb 2020 21:21:27 -0500 Subject: [PATCH 064/215] Return keyboard focus after saving database edits (#4287) --- src/gui/DatabaseWidget.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index d7521f3b2b..3d6598fb2c 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1657,6 +1657,8 @@ bool DatabaseWidget::save() m_blockAutoSave = true; ++m_saveAttempts; + auto focusWidget = qApp->focusWidget(); + // TODO: Make this async // Lock out interactions m_entryView->setDisabled(true); @@ -1671,6 +1673,10 @@ bool DatabaseWidget::save() m_entryView->setDisabled(false); m_groupView->setDisabled(false); + if (focusWidget) { + focusWidget->setFocus(); + } + if (ok) { m_saveAttempts = 0; m_blockAutoSave = false; From eb88b8cc0c1440f958a7ec625e5733b3b33f8c7d Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 23 Feb 2020 19:59:17 -0500 Subject: [PATCH 065/215] Fix clearing search field when using application * Reset clear timer when manipulating the entry view and opening/closing entries * Only start the clear timer if there is an active search --- src/gui/SearchWidget.cpp | 13 ++++++++++++- src/gui/SearchWidget.h | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 822d40ba15..96c52e6393 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -115,7 +115,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) return true; } } - } else if (event->type() == QEvent::FocusOut) { + } else if (event->type() == QEvent::FocusOut && !m_ui->searchEdit->text().isEmpty()) { if (config()->get("security/clearsearch").toBool()) { int timeout = config()->get("security/clearsearchtimeout").toInt(); if (timeout > 0) { @@ -124,6 +124,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) } } } else if (event->type() == QEvent::FocusIn) { + // Never clear the search if we are using it m_clearSearchTimer->stop(); } @@ -139,6 +140,8 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx) mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword())); mx.connect(this, SIGNAL(downPressed()), SLOT(setFocus())); 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(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit())); } @@ -177,6 +180,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()); diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index f3646c362b..1a40aba954 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; From b9daed20558103e3c8e799f3e0575522bf823df6 Mon Sep 17 00:00:00 2001 From: Michal Suchanek <msuchanek@suse.de> Date: Mon, 24 Feb 2020 12:33:43 +0100 Subject: [PATCH 066/215] Correct issues with hiding and minimizing the MainWindow The GUI features depend on windowing system used, not just OS. There is an issue with the WM sometimes producing an event that keepassxc interprets as request to hide the main window just after it is shown. A workaround with immediately firing a timer was implemented. However, there is no guarantee on execution ordering of the timer callback and other application code. Remove the timer and override show() and hide() on main window to only hide if the window has not been shown recently. The user can set an option to hide window instead of minimizing when tray icon is enabled. This is not honored in most places where the main windows is minimized. Fix it. This also allows using the tray icon as a workaround for minimization not working under some circumstances in X11. Signed-off-by: Michal Suchanek <msuchanek@suse.de> --- src/core/Clock.cpp | 5 +++ src/core/Clock.h | 1 + src/gui/DatabaseWidget.cpp | 9 +++-- src/gui/MainWindow.cpp | 60 ++++++++++++++++++++++------ src/gui/MainWindow.h | 7 +++- src/gui/TotpDialog.cpp | 2 +- src/gui/TotpExportSettingsDialog.cpp | 2 +- 7 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/core/Clock.cpp b/src/core/Clock.cpp index be9e91dcf0..5704d4bff9 100644 --- a/src/core/Clock.cpp +++ b/src/core/Clock.cpp @@ -34,6 +34,11 @@ uint Clock::currentSecondsSinceEpoch() return instance().currentDateTimeImpl().toTime_t(); } +qint64 Clock::currentMilliSecondsSinceEpoch() +{ + return instance().currentDateTimeImpl().toMSecsSinceEpoch(); +} + QDateTime Clock::serialized(const QDateTime& dateTime) { auto time = dateTime.time(); diff --git a/src/core/Clock.h b/src/core/Clock.h index 8f81b0961a..4d1ee25371 100644 --- a/src/core/Clock.h +++ b/src/core/Clock.h @@ -28,6 +28,7 @@ class Clock static QDateTime currentDateTime(); static uint currentSecondsSinceEpoch(); + static qint64 currentMilliSecondsSinceEpoch(); static QDateTime serialized(const QDateTime& dateTime); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 3d6598fb2c..0eb713dad8 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -49,6 +49,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" @@ -677,7 +678,7 @@ void DatabaseWidget::setClipboardTextAndMinimize(const QString& text) clipboard()->setText(text); if (config()->get("HideWindowOnCopy").toBool()) { if (config()->get("MinimizeOnCopy").toBool()) { - window()->showMinimized(); + getMainWindow()->minimizeOrHide(); } else if (config()->get("DropToBackgroundOnCopy").toBool()) { window()->lower(); } @@ -782,7 +783,7 @@ void DatabaseWidget::openUrlForEntry(Entry* entry) QProcess::startDetached(cmdString.mid(6)); if (config()->get("MinimizeOnOpenUrl").toBool()) { - window()->showMinimized(); + getMainWindow()->minimizeOrHide(); } } } else { @@ -791,7 +792,7 @@ void DatabaseWidget::openUrlForEntry(Entry* entry) QDesktopServices::openUrl(url); if (config()->get("MinimizeOnOpenUrl").toBool()) { - window()->showMinimized(); + getMainWindow()->minimizeOrHide(); } } } @@ -972,7 +973,7 @@ void DatabaseWidget::loadDatabase(bool accepted) m_saveAttempts = 0; emit databaseUnlocked(); if (config()->get("MinimizeAfterUnlock").toBool()) { - window()->showMinimized(); + getMainWindow()->minimizeOrHide(); } } else { if (m_databaseOpenWidget->database()) { diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b225165a67..620a509bcc 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -294,7 +294,7 @@ 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())); @@ -1072,7 +1072,7 @@ void MainWindow::changeEvent(QEvent* event) if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() && config()->get("GUI/MinimizeToTray").toBool()) { event->ignore(); - QTimer::singleShot(0, this, SLOT(hide())); + hide(); } if (config()->get("security/lockdatabaseminimize").toBool()) { @@ -1245,7 +1245,7 @@ void MainWindow::applySettingsChanges() void MainWindow::focusWindowChanged(QWindow* focusWindow) { if (focusWindow != windowHandle()) { - m_lastFocusOutTime = Clock::currentSecondsSinceEpoch(); + m_lastFocusOutTime = Clock::currentMilliSecondsSinceEpoch(); } } @@ -1269,9 +1269,9 @@ 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 (hasFocus() || isHidden() || windowHandle()->isActive()) { @@ -1283,16 +1283,43 @@ void MainWindow::processTrayIconTrigger() } } +void MainWindow::show() +{ +#ifndef Q_OS_WIN + m_lastShowTime = Clock::currentMilliSecondsSinceEpoch(); +#endif + QMainWindow::show(); +} + +bool MainWindow::shouldHide() +{ +#ifndef Q_OS_WIN + qint64 current_time = Clock::currentMilliSecondsSinceEpoch(); + + if (current_time - m_lastShowTime < 50) { + return false; + } +#endif + return true; +} + +void MainWindow::hide() +{ + if (shouldHide()) { + QMainWindow::hide(); + } +} + 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 + if (QGuiApplication::platformName() != "xcb") { + // In X11 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. + setWindowState(windowState() | Qt::WindowMinimized); + } // Only hide if tray icon is active, otherwise window will be gone forever if (isTrayIconEnabled()) { hide(); @@ -1305,6 +1332,15 @@ void MainWindow::hideWindow() } } +void MainWindow::minimizeOrHide() +{ + if (config()->get("GUI/MinimizeToTray").toBool()) { + hideWindow(); + } else { + showMinimized(); + } +} + void MainWindow::toggleWindow() { if (isVisible() && !isMinimized()) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 0e74edf60c..83a504f82f 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -70,7 +70,10 @@ public slots: void hideGlobalMessage(); void showYubiKeyPopup(); void hideYubiKeyPopup(); + void hide(); + void show(); void hideWindow(); + void minimizeOrHide(); void toggleWindow(); void bringToFront(); void closeAllDatabases(); @@ -133,6 +136,7 @@ private slots: static const QString BaseWindowTitle; + bool shouldHide(); void saveWindowInformation(); bool saveLastDatabases(); void updateTrayIcon(); @@ -163,7 +167,8 @@ private slots: bool m_appExitCalled = false; bool m_appExiting = false; bool m_contextMenuFocusLock = false; - uint m_lastFocusOutTime = 0; + qint64 m_lastFocusOutTime = 0; + qint64 m_lastShowTime = 0; QTimer m_trayIconTriggerTimer; QSystemTrayIcon::ActivationReason m_trayIconTriggerReason; }; diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp index 639eb0ebde..7292cfcd39 100644 --- a/src/gui/TotpDialog.cpp +++ b/src/gui/TotpDialog.cpp @@ -67,7 +67,7 @@ void TotpDialog::copyToClipboard() clipboard()->setText(m_entry->totp()); if (config()->get("HideWindowOnCopy").toBool()) { if (config()->get("MinimizeOnCopy").toBool()) { - getMainWindow()->showMinimized(); + getMainWindow()->minimizeOrHide(); } else if (config()->get("DropToBackgroundOnCopy").toBool()) { getMainWindow()->lower(); window()->lower(); diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp index 178cd6d968..ea14eabdb8 100644 --- a/src/gui/TotpExportSettingsDialog.cpp +++ b/src/gui/TotpExportSettingsDialog.cpp @@ -105,7 +105,7 @@ void TotpExportSettingsDialog::copyToClipboard() clipboard()->setText(m_totpUri); if (config()->get("HideWindowOnCopy").toBool()) { if (config()->get("MinimizeOnCopy").toBool()) { - getMainWindow()->showMinimized(); + getMainWindow()->minimizeOrHide(); } else if (config()->get("DropToBackgroundOnCopy").toBool()) { getMainWindow()->lower(); window()->lower(); From 6d2ca748787c71eab3adaf2071d5738ba7c28a52 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff <janek@jbev.net> Date: Tue, 28 Jan 2020 21:42:57 +0100 Subject: [PATCH 067/215] Add OSUtils for platform-specific functionality. Moves MacUtils into a separate sub folder and adds WinUtils, NixUtils, and OSUtils for platform-native code on Windows, Unix-like, and generic/all systems. --- cmake/CLangFormat.cmake | 2 +- src/CMakeLists.txt | 13 +-- src/autotype/AutoType.cpp | 2 +- src/autotype/mac/AutoTypeMac.cpp | 2 +- src/browser/BrowserService.cpp | 2 +- src/gui/DatabaseTabWidget.cpp | 2 +- src/gui/IconDownloaderDialog.cpp | 2 +- src/gui/MainWindow.cpp | 2 +- src/gui/entry/EntryModel.cpp | 2 +- src/gui/osutils/OSUtils.h | 41 ++++++++++ src/gui/osutils/OSUtilsBase.cpp | 27 +++++++ src/gui/osutils/OSUtilsBase.h | 40 +++++++++ src/gui/{ => osutils}/macutils/AppKit.h | 0 src/gui/{ => osutils}/macutils/AppKitImpl.h | 0 src/gui/{ => osutils}/macutils/AppKitImpl.mm | 0 src/gui/{ => osutils}/macutils/MacUtils.cpp | 4 +- src/gui/{ => osutils}/macutils/MacUtils.h | 15 ++-- src/gui/osutils/nixutils/NixUtils.cpp | 50 ++++++++++++ src/gui/osutils/nixutils/NixUtils.h | 48 +++++++++++ src/gui/osutils/winutils/WinUtils.cpp | 85 ++++++++++++++++++++ src/gui/osutils/winutils/WinUtils.h | 59 ++++++++++++++ 21 files changed, 375 insertions(+), 23 deletions(-) create mode 100644 src/gui/osutils/OSUtils.h create mode 100644 src/gui/osutils/OSUtilsBase.cpp create mode 100644 src/gui/osutils/OSUtilsBase.h rename src/gui/{ => osutils}/macutils/AppKit.h (100%) rename src/gui/{ => osutils}/macutils/AppKitImpl.h (100%) rename src/gui/{ => osutils}/macutils/AppKitImpl.mm (100%) rename src/gui/{ => osutils}/macutils/MacUtils.cpp (96%) rename src/gui/{ => osutils}/macutils/MacUtils.h (87%) create mode 100644 src/gui/osutils/nixutils/NixUtils.cpp create mode 100644 src/gui/osutils/nixutils/NixUtils.h create mode 100644 src/gui/osutils/winutils/WinUtils.cpp create mode 100644 src/gui/osutils/winutils/WinUtils.h diff --git a/cmake/CLangFormat.cmake b/cmake/CLangFormat.cmake index 70169ed72c..c1e9572c28 100644 --- a/cmake/CLangFormat.cmake +++ b/cmake/CLangFormat.cmake @@ -19,7 +19,7 @@ set(EXCLUDED_DIRS # objective-c directories src/touchid/ src/autotype/mac/ - src/gui/macutils/) + src/gui/osutils/macutils/) set(EXCLUDED_FILES # third-party files diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b3d9abfab..bc63b5ee9e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -156,6 +156,7 @@ set(keepassx_SOURCES gui/reports/ReportsPageHealthcheck.cpp gui/reports/ReportsWidgetStatistics.cpp gui/reports/ReportsPageStatistics.cpp + gui/osutils/OSUtilsBase.cpp gui/settings/SettingsWidget.cpp gui/widgets/ElidedLabel.cpp gui/widgets/PopupHelpWidget.cpp @@ -181,20 +182,22 @@ if(APPLE) ${keepassx_SOURCES} core/ScreenLockListenerMac.cpp core/MacPasteboard.cpp - gui/macutils/MacUtils.cpp - gui/macutils/AppKitImpl.mm - gui/macutils/AppKit.h) + gui/osutils/macutils/MacUtils.cpp + gui/osutils/macutils/AppKitImpl.mm + gui/osutils/macutils/AppKit.h) endif() if(UNIX AND NOT APPLE) set(keepassx_SOURCES ${keepassx_SOURCES} core/ScreenLockListenerDBus.cpp - gui/MainWindowAdaptor.cpp) + gui/MainWindowAdaptor.cpp + gui/osutils/nixutils/NixUtils.cpp) endif() if(MINGW) set(keepassx_SOURCES ${keepassx_SOURCES} - core/ScreenLockListenerWin.cpp) + core/ScreenLockListenerWin.cpp + gui/osutils/winutils/WinUtils.cpp) endif() if(MINGW OR (UNIX AND NOT APPLE)) set(keepassx_SOURCES diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 80a2268ec3..fa7537373e 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -38,7 +38,7 @@ #include "gui/MessageBox.h" #ifdef Q_OS_MAC -#include "gui/macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" #endif AutoType* AutoType::m_instance = nullptr; diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp index fadc70e1c9..1e52f58fe9 100644 --- a/src/autotype/mac/AutoTypeMac.cpp +++ b/src/autotype/mac/AutoTypeMac.cpp @@ -17,7 +17,7 @@ */ #include "AutoTypeMac.h" -#include "gui/macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" #include "gui/MessageBox.h" #include <ApplicationServices/ApplicationServices.h> diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index e0c896eca7..a09a4d5649 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -39,7 +39,7 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" #ifdef Q_OS_MACOS -#include "gui/macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" #endif const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings"); diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 7e158406b2..c867526efc 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" diff --git a/src/gui/IconDownloaderDialog.cpp b/src/gui/IconDownloaderDialog.cpp index ebe6980a2d..b7c6567ffb 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> diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 620a509bcc..07649d244c 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -43,7 +43,7 @@ #include "keys/PasswordKey.h" #ifdef Q_OS_MACOS -#include "macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" #endif #ifdef WITH_XC_UPDATECHECK diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index b4c5840cfd..17347e3376 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -31,7 +31,7 @@ #include "core/Group.h" #include "core/Metadata.h" #ifdef Q_OS_MACOS -#include "gui/macutils/MacUtils.h" +#include "gui/osutils/macutils/MacUtils.h" #endif EntryModel::EntryModel(QObject* parent) diff --git a/src/gui/osutils/OSUtils.h b/src/gui/osutils/OSUtils.h new file mode 100644 index 0000000000..dd1bd8cd1c --- /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 0000000000..143cb72c19 --- /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 0000000000..9467fca09a --- /dev/null +++ b/src/gui/osutils/OSUtilsBase.h @@ -0,0 +1,40 @@ +/* + * 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: + virtual bool isDarkMode() = 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 similarity index 100% rename from src/gui/macutils/AppKit.h rename to src/gui/osutils/macutils/AppKit.h diff --git a/src/gui/macutils/AppKitImpl.h b/src/gui/osutils/macutils/AppKitImpl.h similarity index 100% rename from src/gui/macutils/AppKitImpl.h rename to src/gui/osutils/macutils/AppKitImpl.h diff --git a/src/gui/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm similarity index 100% rename from src/gui/macutils/AppKitImpl.mm rename to src/gui/osutils/macutils/AppKitImpl.mm diff --git a/src/gui/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp similarity index 96% rename from src/gui/macutils/MacUtils.cpp rename to src/gui/osutils/macutils/MacUtils.cpp index 211aaa7ebc..4983258686 100644 --- a/src/gui/macutils/MacUtils.cpp +++ b/src/gui/osutils/macutils/MacUtils.cpp @@ -19,10 +19,10 @@ #include "MacUtils.h" #include <QApplication> -MacUtils* MacUtils::m_instance = nullptr; +QPointer<MacUtils> MacUtils::m_instance = nullptr; MacUtils::MacUtils(QObject* parent) - : QObject(parent) + : OSUtils(parent) , m_appkit(new AppKit()) { connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases())); diff --git a/src/gui/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h similarity index 87% rename from src/gui/macutils/MacUtils.h rename to src/gui/osutils/macutils/MacUtils.h index 3e35994b14..427c7230a5 100644 --- a/src/gui/macutils/MacUtils.h +++ b/src/gui/osutils/macutils/MacUtils.h @@ -19,17 +19,16 @@ #ifndef KEEPASSXC_MACUTILS_H #define KEEPASSXC_MACUTILS_H +#include "gui/osutils/OSUtilsBase.h" #include "AppKit.h" -#include <QObject> -#include <QWidget> +#include <QPointer> -class MacUtils : public QObject +class MacUtils : public OSUtils { Q_OBJECT public: static MacUtils* instance(); - static void createTestInstance(); WId activeWindow(); bool raiseWindow(WId pid); @@ -37,20 +36,20 @@ class MacUtils : public QObject bool raiseOwnWindow(); bool hideOwnWindow(); bool isHidden(); - bool isDarkMode(); + bool isDarkMode() override; bool enableAccessibility(); bool enableScreenRecording(); signals: void lockDatabases(); -private: +protected: explicit MacUtils(QObject* parent = nullptr); - ~MacUtils(); + ~MacUtils() override; private: 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 0000000000..229d6b519e --- /dev/null +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -0,0 +1,50 @@ +/* + * 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 <QPalette> +#include <QStyle> + +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() +{ + if (!qApp || !qApp->style()) { + return false; + } + return qApp->style()->standardPalette().color(QPalette::Window).toHsl().lightness() < 110; +} diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h new file mode 100644 index 0000000000..bf236cdc37 --- /dev/null +++ b/src/gui/osutils/nixutils/NixUtils.h @@ -0,0 +1,48 @@ +/* + * 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() override; + +private: + explicit NixUtils(QObject* parent = nullptr); + ~NixUtils() override; + +private: + 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 0000000000..f92192f18a --- /dev/null +++ b/src/gui/osutils/winutils/WinUtils.cpp @@ -0,0 +1,85 @@ +/* + * 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 <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() +{ + QSettings settings(R"(HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)", + QSettings::NativeFormat); + return settings.value("AppsUseLightTheme", 1).toInt() == 0; +} diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h new file mode 100644 index 0000000000..1a95716e14 --- /dev/null +++ b/src/gui/osutils/winutils/WinUtils.h @@ -0,0 +1,59 @@ +/* + * 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() 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 From 557736ea5e4314162d7d716ac53fb6c74efce2a1 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff <janek@jbev.net> Date: Mon, 6 Jan 2020 03:00:25 +0100 Subject: [PATCH 068/215] Add custom light and dark UI themes --- src/CMakeLists.txt | 5 + src/core/Config.cpp | 1 + src/core/FilePath.cpp | 78 +- src/core/FilePath.h | 4 +- src/gui/ApplicationSettingsWidget.cpp | 19 +- src/gui/ApplicationSettingsWidgetGeneral.ui | 61 +- src/gui/CategoryListWidget.cpp | 17 +- src/gui/DatabaseOpenWidget.cpp | 6 - src/gui/DatabaseOpenWidget.ui | 255 +- src/gui/DatabaseTabWidget.cpp | 2 + src/gui/DatabaseTabWidget.h | 1 + src/gui/DatabaseWidgetStateSync.cpp | 6 +- src/gui/DialogyWidget.cpp | 1 + src/gui/KMessageWidget.cpp | 20 +- src/gui/MainWindow.cpp | 21 +- src/gui/MainWindow.h | 2 + src/gui/MainWindow.ui | 89 +- src/gui/PasswordGeneratorWidget.cpp | 19 +- src/gui/SearchHelpWidget.ui | 15 +- src/gui/SearchWidget.ui | 18 +- src/gui/entry/EntryModel.cpp | 62 +- src/gui/entry/EntryModel.h | 4 - src/gui/entry/EntryView.cpp | 146 +- src/gui/entry/EntryView.h | 6 +- src/gui/osutils/macutils/MacUtils.cpp | 2 +- src/gui/osutils/macutils/MacUtils.h | 5 +- src/gui/styles/base/BaseStyle.cpp | 4779 +++++++++++++++++++ src/gui/styles/base/BaseStyle.h | 101 + src/gui/styles/base/basestyle.qss | 48 + src/gui/styles/base/phantomcolor.cpp | 423 ++ src/gui/styles/base/phantomcolor.h | 165 + src/gui/styles/dark/DarkStyle.cpp | 125 + src/gui/styles/dark/DarkStyle.h | 37 + src/gui/styles/dark/darkstyle.qss | 18 + src/gui/styles/light/LightStyle.cpp | 124 + src/gui/styles/light/LightStyle.h | 37 + src/gui/styles/light/lightstyle.qss | 18 + src/gui/styles/styles.qrc | 8 + src/main.cpp | 17 + 39 files changed, 6408 insertions(+), 357 deletions(-) create mode 100644 src/gui/styles/base/BaseStyle.cpp create mode 100644 src/gui/styles/base/BaseStyle.h create mode 100644 src/gui/styles/base/basestyle.qss create mode 100644 src/gui/styles/base/phantomcolor.cpp create mode 100644 src/gui/styles/base/phantomcolor.h create mode 100644 src/gui/styles/dark/DarkStyle.cpp create mode 100644 src/gui/styles/dark/DarkStyle.h create mode 100644 src/gui/styles/dark/darkstyle.qss create mode 100644 src/gui/styles/light/LightStyle.cpp create mode 100644 src/gui/styles/light/LightStyle.h create mode 100644 src/gui/styles/light/lightstyle.qss create mode 100644 src/gui/styles/styles.qrc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bc63b5ee9e..5214242b27 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -87,6 +87,11 @@ set(keepassx_SOURCES format/OpVaultReaderAttachments.cpp format/OpVaultReaderBandEntry.cpp format/OpVaultReaderSections.cpp + gui/styles/styles.qrc + gui/styles/base/phantomcolor.cpp + gui/styles/base/BaseStyle.cpp + gui/styles/dark/DarkStyle.cpp + gui/styles/light/LightStyle.cpp gui/AboutDialog.cpp gui/Application.cpp gui/CategoryListWidget.cpp diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 445e587f80..41395214d7 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -232,6 +232,7 @@ void Config::init(const QString& fileName) m_defaults.insert("GUI/HidePasswords", true); m_defaults.insert("GUI/AdvancedSettings", false); m_defaults.insert("GUI/MonospaceNotes", false); + m_defaults.insert("GUI/ApplicationTheme", "auto"); } Config* Config::instance() diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 62db3929d6..95e7fc0c24 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -18,13 +18,15 @@ #include "FilePath.h" -#include <QCoreApplication> +#include <QBitmap> #include <QDir> #include <QLibrary> +#include <QStyle> #include "config-keepassx.h" #include "core/Config.h" #include "core/Global.h" +#include "gui/MainWindow.h" FilePath* FilePath::m_instance(nullptr); @@ -98,7 +100,7 @@ QIcon FilePath::applicationIcon() #ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc", false); #else - return icon("apps", "keepassxc"); + return icon("apps", "keepassxc", false); #endif } @@ -109,7 +111,7 @@ QIcon FilePath::trayIcon() #ifdef KEEPASSXC_DIST_SNAP return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); #else - return (darkIcon) ? icon("apps", "keepassxc-dark") : icon("apps", "keepassxc"); + return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); #endif } @@ -118,7 +120,7 @@ QIcon FilePath::trayIconLocked() #ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc-locked", false); #else - return icon("apps", "keepassxc-locked"); + return icon("apps", "keepassxc-locked", false); #endif } @@ -129,14 +131,13 @@ QIcon FilePath::trayIconUnlocked() #ifdef KEEPASSXC_DIST_SNAP return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); #else - return darkIcon ? icon("apps", "keepassxc-dark") : icon("apps", "keepassxc-unlocked"); + return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); #endif } -QIcon FilePath::icon(const QString& category, const QString& name) +QIcon FilePath::icon(const QString& category, const QString& name, bool recolor) { QString combinedName = category + "/" + name; - QIcon icon = m_iconCache.value(combinedName); if (!icon.isNull()) { @@ -154,7 +155,30 @@ QIcon FilePath::icon(const QString& category, const QString& name) } } filename = QString("%1/icons/application/scalable/%2.svg").arg(m_dataPath, combinedName); - if (QFile::exists(filename)) { + if (QFile::exists(filename) && getMainWindow() && recolor) { + QPalette palette = getMainWindow()->palette(); + + QFile f(filename); + QIcon scalable(filename); + QPixmap pixmap = scalable.pixmap({128, 128}); + + auto mask = QBitmap::fromImage(pixmap.toImage().createAlphaMask()); + pixmap.fill(palette.color(QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Normal); + + pixmap.fill(palette.color(QPalette::HighlightedText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Selected); + + pixmap.fill(palette.color(QPalette::Disabled, QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Disabled); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + icon.setIsMask(true); +#endif + } else if (QFile::exists(filename)) { icon.addFile(filename); } } @@ -164,7 +188,7 @@ QIcon FilePath::icon(const QString& category, const QString& name) return icon; } -QIcon FilePath::onOffIcon(const QString& category, const QString& name) +QIcon FilePath::onOffIcon(const QString& category, const QString& name, bool recolor) { QString combinedName = category + "/" + name; QString cacheName = "onoff/" + combinedName; @@ -175,31 +199,17 @@ QIcon FilePath::onOffIcon(const QString& category, const QString& name) return icon; } - for (int i = 0; i < 2; i++) { - QIcon::State state; - QString stateName; - - if (i == 0) { - state = QIcon::Off; - stateName = "off"; - } else { - state = QIcon::On; - stateName = "on"; - } - - const QList<int> pngSizes = {16, 22, 24, 32, 48, 64, 128}; - QString filename; - for (int size : pngSizes) { - filename = QString("%1/icons/application/%2x%2/%3-%4.png") - .arg(m_dataPath, QString::number(size), combinedName, stateName); - if (QFile::exists(filename)) { - icon.addFile(filename, QSize(size, size), QIcon::Normal, state); - } - } - filename = QString("%1/icons/application/scalable/%2-%3.svg").arg(m_dataPath, combinedName, stateName); - if (QFile::exists(filename)) { - icon.addFile(filename, QSize(), QIcon::Normal, state); - } + QIcon on = FilePath::icon(category, name + "-on", recolor); + for (const auto& size : on.availableSizes()) { + icon.addPixmap(on.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::On); + icon.addPixmap(on.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::On); + icon.addPixmap(on.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::On); + } + QIcon off = FilePath::icon(category, name + "-off", recolor); + for (const auto& size : off.availableSizes()) { + icon.addPixmap(off.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::Off); + icon.addPixmap(off.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::Off); + icon.addPixmap(off.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::Off); } m_iconCache.insert(cacheName, icon); diff --git a/src/core/FilePath.h b/src/core/FilePath.h index ceb9582378..008dfc33e9 100644 --- a/src/core/FilePath.h +++ b/src/core/FilePath.h @@ -32,8 +32,8 @@ class FilePath QIcon trayIcon(); QIcon trayIconLocked(); QIcon trayIconUnlocked(); - QIcon icon(const QString& category, const QString& name); - QIcon onOffIcon(const QString& category, const QString& name); + QIcon icon(const QString& category, const QString& name, bool recolor = true); + QIcon onOffIcon(const QString& category, const QString& name, bool recolor = true); static FilePath* instance(); diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index d3cc994f80..b17c44ecc1 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -213,6 +213,14 @@ void ApplicationSettingsWidget::loadSettings() m_generalUi->toolbarMovableCheckBox->setChecked(config()->get("GUI/MovableToolbar").toBool()); m_generalUi->monospaceNotesCheckBox->setChecked(config()->get("GUI/MonospaceNotes").toBool()); + m_generalUi->appThemeSelection->clear(); + m_generalUi->appThemeSelection->addItem(tr("Automatic"), QStringLiteral("auto")); + m_generalUi->appThemeSelection->addItem(tr("Light"), QStringLiteral("light")); + m_generalUi->appThemeSelection->addItem(tr("Dark"), QStringLiteral("dark")); + m_generalUi->appThemeSelection->addItem(tr("Classic (Platform-native)"), QStringLiteral("classic")); + m_generalUi->appThemeSelection->setCurrentIndex( + m_generalUi->appThemeSelection->findData(config()->get("GUI/ApplicationTheme").toString())); + m_generalUi->toolButtonStyleComboBox->clear(); m_generalUi->toolButtonStyleComboBox->addItem(tr("Icon only"), Qt::ToolButtonIconOnly); m_generalUi->toolButtonStyleComboBox->addItem(tr("Text only"), Qt::ToolButtonTextOnly); @@ -303,19 +311,18 @@ void ApplicationSettingsWidget::saveSettings() 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/Language", m_generalUi->languageComboBox->currentData().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()); + QString theme = m_generalUi->appThemeSelection->currentData().toString(); + config()->set("GUI/ApplicationTheme", theme); + + config()->set("GUI/ToolButtonStyle", m_generalUi->toolButtonStyleComboBox->currentData().toString()); config()->set("GUI/ShowTrayIcon", m_generalUi->systrayShowCheckBox->isChecked()); config()->set("GUI/DarkTrayIcon", m_generalUi->systrayDarkIconCheckBox->isChecked()); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index fa4da2acc9..d286a89736 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>684</width> - <height>951</height> + <width>499</width> + <height>1174</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_3"> @@ -337,7 +337,7 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0"> <item> <widget class="QLabel" name="faviconTimeoutLabel"> <property name="text"> @@ -350,12 +350,6 @@ <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> @@ -400,6 +394,50 @@ <string>General</string> </property> <layout class="QVBoxLayout" name="verticalLayout_7"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="appThemeLabel"> + <property name="text"> + <string>Application Theme:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="appThemeSelection"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="accessibleName"> + <string>Application Theme Selection</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>(restart program to activate)</string> + </property> + </widget> + </item> + <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> + </layout> + </item> <item> <widget class="QCheckBox" name="toolbarHideCheckBox"> <property name="text"> @@ -491,9 +529,6 @@ </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> @@ -726,7 +761,7 @@ <item> <widget class="QPushButton" name="resetSettingsButton"> <property name="text"> - <string>Reset Settings to Default</string> + <string>Reset settings to default…</string> </property> </widget> </item> diff --git a/src/gui/CategoryListWidget.cpp b/src/gui/CategoryListWidget.cpp index c57b19bc03..48ccb4783b 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, @@ -171,8 +170,8 @@ class WindowsCorrectedStyle : public QProxyStyle 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. + // 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. // 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 @@ -186,7 +185,6 @@ class WindowsCorrectedStyle : public QProxyStyle painter->restore(); } }; -#endif void CategoryListWidgetDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, @@ -203,12 +201,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( diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index c610a773d9..9559047a95 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -90,12 +90,6 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) 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); -#endif - #ifndef WITH_XC_TOUCHID m_ui->touchIDContainer->setVisible(false); #else diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 60b2feadc2..f2cd96b6aa 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -99,7 +99,7 @@ </spacer> </item> <item> - <widget class="QFrame" name="horizontalFrame"> + <widget class="QFrame" name="loginFrame"> <property name="minimumSize"> <size> <width>550</width> @@ -235,6 +235,54 @@ <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>comboKeyFile</cstring> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="keyFileLabelHelp"> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="toolTip"> + <string><p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave the field empty.</p><p>Click for more information...</p></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"> @@ -283,18 +331,77 @@ </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>comboChallengeResponse</cstring> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="hardwareKeyLabelHelp"> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="toolTip"> + <string><p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> +<p>Click for more information...</p></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"> @@ -322,119 +429,58 @@ </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><p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> -<p>Click for more information...</p></string> + <string>Refresh hardware tokens</string> </property> <property name="accessibleName"> - <string>Hardware key help</string> + <string>Refresh hardware tokens</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 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>comboKeyFile</cstring> + <string>Refresh</string> </property> </widget> </item> <item> - <widget class="QToolButton" name="keyFileLabelHelp"> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> </property> - <property name="toolTip"> - <string><p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave the field empty.</p><p>Click for more information...</p></string> - </property> - <property name="accessibleName"> - <string>Key file help</string> - </property> - <property name="styleSheet"> - <string notr="true">QToolButton { - border: none; - background: none; -}</string> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> </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> @@ -572,7 +618,6 @@ <tabstop>buttonBrowseFile</tabstop> <tabstop>hardwareKeyLabelHelp</tabstop> <tabstop>comboChallengeResponse</tabstop> - <tabstop>buttonRedetectYubikey</tabstop> <tabstop>checkTouchID</tabstop> </tabstops> <resources/> diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index c867526efc..5523f7c622 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -80,8 +80,10 @@ void DatabaseTabWidget::toggleTabbar() { if (count() > 1) { tabBar()->show(); + emit tabVisibilityChanged(true); } else { tabBar()->hide(); + emit tabVisibilityChanged(false); } } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 29019a2d29..5b2d3f0087 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -89,6 +89,7 @@ public slots: 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); diff --git a/src/gui/DatabaseWidgetStateSync.cpp b/src/gui/DatabaseWidgetStateSync.cpp index 5579b30cd0..df0e43ebcf 100644 --- a/src/gui/DatabaseWidgetStateSync.cpp +++ b/src/gui/DatabaseWidgetStateSync.cpp @@ -101,9 +101,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 +110,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 597bcc59de..0703939331 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/KMessageWidget.cpp b/src/gui/KMessageWidget.cpp index 8df7b63846..01925b7dd8 100644 --- a/src/gui/KMessageWidget.cpp +++ b/src/gui/KMessageWidget.cpp @@ -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/MainWindow.cpp b/src/gui/MainWindow.cpp index 07649d244c..3db3e98f6f 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -236,6 +236,9 @@ MainWindow::MainWindow() autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); } + m_ui->toolbarSeparator->setVisible(false); + m_showToolbarSeparator = config()->get("GUI/ApplicationTheme").toString() != "classic"; + m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); m_inactivityTimer = new InactivityTimer(this); @@ -389,6 +392,7 @@ MainWindow::MainWindow() connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), m_searchWidget, SLOT(databaseChanged())); connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(tabVisibilityChanged(bool)), SLOT(adjustToTabVisibilityChange(bool))); 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())); @@ -618,14 +622,18 @@ 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 (m_showToolbarSeparator) { + m_ui->toolbarSeparator->setVisible( + (!inWelcomeWidget && inDatabaseTabWidget && !m_ui->tabWidget->tabBar()->isVisible()) + || currentIndex == SettingsScreen); + } + if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); Q_ASSERT(dbWidget); @@ -776,6 +784,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) } } +void MainWindow::adjustToTabVisibilityChange(bool tabsVisible) +{ + if (m_showToolbarSeparator) { + m_ui->toolbarSeparator->setVisible(!tabsVisible && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen); + } +} + void MainWindow::updateWindowTitle() { QString customWindowTitlePart; @@ -1125,7 +1140,7 @@ void MainWindow::updateTrayIcon() QAction* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); - actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark")); + actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark", false)); menu->addAction(m_ui->actionLockDatabases); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 83a504f82f..5e2bfef800 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -86,6 +86,7 @@ public slots: private slots: void setMenuActionState(DatabaseWidget::Mode mode = DatabaseWidget::Mode::None); + void adjustToTabVisibilityChange(bool tabsVisible); void updateWindowTitle(); void showAboutDialog(); void showUpdateCheckStartup(); @@ -167,6 +168,7 @@ private slots: bool m_appExitCalled = false; bool m_appExiting = false; bool m_contextMenuFocusLock = false; + bool m_showToolbarSeparator = false; qint64 m_lastFocusOutTime = 0; qint64 m_lastShowTime = 0; QTimer m_trayIconTriggerTimer; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index aec0efb37e..41792986b5 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -61,6 +61,16 @@ </layout> </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> <widget class="QStackedWidget" name="stackedWidget"> <property name="sizePolicy"> @@ -195,7 +205,7 @@ <x>0</x> <y>0</y> <width>800</width> - <height>21</height> + <height>24</height> </rect> </property> <property name="focusPolicy"> @@ -210,7 +220,7 @@ </property> <widget class="QMenu" name="menuRecentDatabases"> <property name="title"> - <string>&Recent databases</string> + <string>&Recent Databases</string> </property> </widget> <widget class="QMenu" name="menuImport"> @@ -249,7 +259,6 @@ <property name="title"> <string>&Help</string> </property> - <addaction name="actionAbout"/> <addaction name="separator"/> <addaction name="actionGettingStarted"/> <addaction name="actionUserGuide"/> @@ -259,10 +268,11 @@ <addaction name="actionCheckForUpdates"/> <addaction name="actionDonate"/> <addaction name="actionBugReport"/> + <addaction name="actionAbout"/> </widget> <widget class="QMenu" name="menuEntries"> <property name="title"> - <string>E&ntries</string> + <string>&Entries</string> </property> <widget class="QMenu" name="menuEntryCopyAttribute"> <property name="enabled"> @@ -272,7 +282,7 @@ <string/> </property> <property name="title"> - <string>Copy att&ribute...</string> + <string>Copy Att&ribute</string> </property> <addaction name="actionEntryCopyTitle"/> <addaction name="actionEntryCopyURL"/> @@ -284,7 +294,7 @@ <bool>false</bool> </property> <property name="title"> - <string>TOTP...</string> + <string>TOTP</string> </property> <addaction name="actionEntryCopyTotp"/> <addaction name="actionEntryTotp"/> @@ -311,7 +321,6 @@ <string>&Groups</string> </property> <addaction name="actionGroupNew"/> - <addaction name="separator"/> <addaction name="actionGroupEdit"/> <addaction name="actionGroupDelete"/> <addaction name="actionGroupEmptyRecycleBin"/> @@ -347,8 +356,8 @@ </property> <property name="iconSize"> <size> - <width>32</width> - <height>32</height> + <width>26</width> + <height>26</height> </size> </property> <attribute name="toolBarArea"> @@ -393,7 +402,7 @@ </action> <action name="actionCheckForUpdates"> <property name="text"> - <string>&Check for Updates...</string> + <string>&Check for Updates</string> </property> <property name="menuRole"> <enum>QAction::ApplicationSpecificRole</enum> @@ -401,7 +410,7 @@ </action> <action name="actionDatabaseOpen"> <property name="text"> - <string>&Open database...</string> + <string>&Open Database…</string> </property> </action> <action name="actionDatabaseSave"> @@ -409,7 +418,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Save database</string> + <string>&Save Database</string> </property> </action> <action name="actionDatabaseClose"> @@ -417,12 +426,12 @@ <bool>false</bool> </property> <property name="text"> - <string>&Close database</string> + <string>&Close Database</string> </property> </action> <action name="actionDatabaseNew"> <property name="text"> - <string>&New database...</string> + <string>&New Database…</string> </property> <property name="toolTip"> <string>Create a new database</string> @@ -430,7 +439,7 @@ </action> <action name="actionDatabaseMerge"> <property name="text"> - <string>&Merge from database...</string> + <string>&Merge From Database…</string> </property> <property name="toolTip"> <string>Merge from another KDBX database</string> @@ -441,7 +450,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&New entry</string> + <string>&New Entry…</string> </property> <property name="toolTip"> <string>Add a new entry</string> @@ -452,7 +461,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Edit entry</string> + <string>&Edit Entry…</string> </property> <property name="toolTip"> <string>View or edit entry</string> @@ -463,7 +472,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Delete entry</string> + <string>&Delete Entry…</string> </property> </action> <action name="actionGroupNew"> @@ -471,7 +480,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&New group</string> + <string>&New Group…</string> </property> <property name="toolTip"> <string>Add a new group</string> @@ -482,7 +491,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Edit group</string> + <string>&Edit Group…</string> </property> </action> <action name="actionGroupDelete"> @@ -490,7 +499,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Delete group</string> + <string>&Delete Group…</string> </property> </action> <action name="actionGroupDownloadFavicons"> @@ -498,7 +507,7 @@ <bool>false</bool> </property> <property name="text"> - <string>Downlo&ad all favicons</string> + <string>Download All &Favicons…</string> </property> </action> <action name="actionGroupSortAsc"> @@ -522,7 +531,7 @@ <bool>false</bool> </property> <property name="text"> - <string>Sa&ve database as...</string> + <string>Sa&ve Database As…</string> </property> </action> <action name="actionChangeMasterKey"> @@ -530,7 +539,7 @@ <bool>false</bool> </property> <property name="text"> - <string>Change master &key...</string> + <string>Change Master &Key…</string> </property> </action> <action name="actionReports"> @@ -552,7 +561,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Database settings...</string> + <string>&Database Settings…</string> </property> <property name="toolTip"> <string>Database settings</string> @@ -566,7 +575,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Clone entry</string> + <string>&Clone Entry…</string> </property> </action> <action name="actionEntryCopyUsername"> @@ -574,7 +583,7 @@ <bool>false</bool> </property> <property name="text"> - <string>Copy &username</string> + <string>Copy &Username</string> </property> <property name="toolTip"> <string>Copy username to clipboard</string> @@ -585,7 +594,7 @@ <bool>false</bool> </property> <property name="text"> - <string>Copy &password</string> + <string>Copy &Password</string> </property> <property name="toolTip"> <string>Copy password to clipboard</string> @@ -620,7 +629,7 @@ </action> <action name="actionEntryDownloadIcon"> <property name="text"> - <string>Download favicon</string> + <string>Download &Favicon</string> </property> </action> <action name="actionEntryOpenUrl"> @@ -636,7 +645,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Lock databases</string> + <string>&Lock Databases</string> </property> </action> <action name="actionEntryCopyTitle"> @@ -677,7 +686,7 @@ <bool>false</bool> </property> <property name="text"> - <string>&Export to CSV file...</string> + <string>&Export to CSV File…</string> </property> </action> <action name="actionExportHtml"> @@ -685,12 +694,12 @@ <bool>false</bool> </property> <property name="text"> - <string>&Export to HTML file...</string> + <string>&Export to 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> @@ -698,7 +707,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> @@ -706,7 +715,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> @@ -714,17 +723,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"> @@ -747,7 +756,7 @@ </action> <action name="actionBugReport"> <property name="text"> - <string>Report a &bug</string> + <string>Report a &Bug</string> </property> </action> <action name="actionGettingStarted"> @@ -760,7 +769,7 @@ </action> <action name="actionOnlineHelp"> <property name="text"> - <string>&Online Help...</string> + <string>&Online Help</string> </property> <property name="toolTip"> <string>Go to online documentation (opens browser)</string> diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index c04487c0e4..7d6f05d417 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -28,6 +28,7 @@ #include "core/PasswordGenerator.h" #include "core/PasswordHealth.h" #include "gui/Clipboard.h" +#include "gui/osutils/OSUtils.h" PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) : QWidget(parent) @@ -390,27 +391,33 @@ void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& healt 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> + QList<QString> qualityColors; + if (osUtils->isDarkMode()) { + qualityColors << QStringLiteral("#C43F31") << QStringLiteral("#DB9837") << QStringLiteral("#608A22") + << QStringLiteral("#1F8023"); + } else { + qualityColors << QStringLiteral("#C43F31") << QStringLiteral("#E09932") << QStringLiteral("#5EA10E") + << QStringLiteral("#118f17"); + } switch (health.quality()) { case PasswordHealth::Quality::Bad: case PasswordHealth::Quality::Poor: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#c0392b")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[0])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Poor", "Password quality"))); break; case PasswordHealth::Quality::Weak: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#f39c1f")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[1])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Weak", "Password quality"))); break; case PasswordHealth::Quality::Good: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#11d116")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[2])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Good", "Password quality"))); break; case PasswordHealth::Quality::Excellent: - m_ui->entropyProgressBar->setStyleSheet(style.arg("#27ae60")); + m_ui->entropyProgressBar->setStyleSheet(style.arg(qualityColors[3])); m_ui->strengthLabel->setText(tr("Password Quality: %1").arg(tr("Excellent", "Password quality"))); break; } diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui index 45e0d0bc6e..4668e409ba 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>487</width> + <height>326</height> </rect> </property> <property name="windowTitle"> @@ -17,10 +17,7 @@ <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"> @@ -58,6 +55,9 @@ <property name="text"> <string>Search terms are as follows: [modifiers][field:]["]term["]</string> </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> </widget> </item> <item> @@ -77,6 +77,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> diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 93fbbdee51..b98f4aea78 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -16,7 +16,7 @@ <verstretch>0</verstretch> </sizepolicy> </property> - <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,4"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="4"> <property name="leftMargin"> <number>3</number> </property> @@ -29,22 +29,6 @@ <property name="bottomMargin"> <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"> diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 17347e3376..9bbf7d56d8 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> @@ -27,6 +26,7 @@ #include "core/Config.h" #include "core/DatabaseIcons.h" #include "core/Entry.h" +#include "core/FilePath.h" #include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" @@ -218,9 +218,6 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } return result; } - case Totp: - result = entry->hasTotp() ? tr("Yes") : ""; - return result; } } else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() switch (index.column()) { @@ -240,7 +237,9 @@ 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(); default: // For all other columns, simply use data provided by Qt::Display- // Role for sorting @@ -260,7 +259,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return entry->iconScaledPixmap(); case Paperclip: if (!entry->attachments()->isEmpty()) { - return m_paperClipPixmap; + return filePath()->icon("actions", "paperclip"); + } + break; + case Totp: + if (entry->hasTotp()) { + return filePath()->icon("actions", "chronometer"); } break; } @@ -327,16 +331,47 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro return tr("Accessed"); case Attachments: return tr("Attachments"); - case Totp: - return tr("TOTP"); } + } else if (role == Qt::DecorationRole) { - if (section == Paperclip) { - return m_paperClipPixmap; + switch (section) { + case Paperclip: + return filePath()->icon("actions", "paperclip"); + case Totp: + return filePath()->icon("actions", "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 Paperclip: + return tr("Has attachments"); + case Totp: + return tr("Has TOTP one-time password"); } } - return QVariant(); + return {}; } Qt::DropActions EntryModel::supportedDropActions() const @@ -502,8 +537,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 5f405bd41b..ea2c96d5aa 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -68,8 +68,6 @@ class EntryModel : public QAbstractTableModel bool isPasswordsHidden() const; void setPasswordsHidden(bool hide); - void setPaperClipPixmap(const QPixmap& paperclip); - signals: void usernamesHiddenChanged(); void passwordsHiddenChanged(); @@ -93,8 +91,6 @@ private slots: bool m_hideUsernames; bool m_hidePasswords; - QPixmap m_paperClipPixmap; - const QString HiddenContentDisplay; const Qt::DateFormat DateFormat; }; diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index bff11e1248..ea41b9b814 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -24,7 +24,6 @@ #include <QMenu> #include <QShortcut> -#include "core/FilePath.h" #include "gui/SortFilterHideProxyModel.h" EntryView::EntryView(QWidget* parent) @@ -70,22 +69,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 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())); @@ -114,22 +122,6 @@ EntryView::EntryView(QWidget* parent) // clang-format off connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SIGNAL(viewStateChanged())); // 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() @@ -325,6 +317,7 @@ bool EntryView::setViewState(const QByteArray& state) { bool status = header()->restoreState(state); resetFixedColumns(); + m_columnsNeedRelayout = state.isEmpty(); return status; } @@ -397,9 +390,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 +404,88 @@ 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); + + // 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 53de7aff52..f3786ed376 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -63,6 +63,7 @@ public slots: 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); @@ -75,15 +76,12 @@ private slots: void contextMenuShortcutPressed(); private: - void fillRemainingWidth(bool lastColumnOnly); void resetFixedColumns(); EntryModel* const m_model; SortFilterHideProxyModel* const m_sortModel; bool m_inSearchMode; - - QByteArray m_defaultListViewState; - QByteArray m_defaultSearchViewState; + bool m_columnsNeedRelayout = true; QMenu* m_headerMenu; QAction* m_hideUsernamesAction; diff --git a/src/gui/osutils/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp index 4983258686..44e5dbee49 100644 --- a/src/gui/osutils/macutils/MacUtils.cpp +++ b/src/gui/osutils/macutils/MacUtils.cpp @@ -22,7 +22,7 @@ QPointer<MacUtils> MacUtils::m_instance = nullptr; MacUtils::MacUtils(QObject* parent) - : OSUtils(parent) + : OSUtilsBase(parent) , m_appkit(new AppKit()) { connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases())); diff --git a/src/gui/osutils/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h index 427c7230a5..2146cdd3b3 100644 --- a/src/gui/osutils/macutils/MacUtils.h +++ b/src/gui/osutils/macutils/MacUtils.h @@ -21,9 +21,12 @@ #include "gui/osutils/OSUtilsBase.h" #include "AppKit.h" + #include <QPointer> +#include <QScopedPointer> +#include <qwindowdefs.h> -class MacUtils : public OSUtils +class MacUtils : public OSUtilsBase { Q_OBJECT diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp new file mode 100644 index 0000000000..b3e22efc9d --- /dev/null +++ b/src/gui/styles/base/BaseStyle.cpp @@ -0,0 +1,4779 @@ +/* + * 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 <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 / 8.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(); + } + 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) + { + return Grad(palette.color(group, QPalette::WindowText), palette.color(group, QPalette::Button)) + .sample(0.45); + } + 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); + } + 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 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_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_text] = pal.color(QPalette::WindowText); + 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_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] = Dc::dividerColor(colors[S_base]); + 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.02); + 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) + { + if (rect.isEmpty()) + return; + using namespace SwatchColors; + Phantom::drawArrow( + painter, rect, type, swatch.brush(allowEnabled ? S_indicator_current : S_indicator_disabled)); + } + + // 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, ©, 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); + if (header->sortIndicator & QStyleOptionHeader::SortUp) { + Ph::drawArrow(painter, r.translated(offset), Qt::DownArrow, swatch); + } else if (header->sortIndicator & QStyleOptionHeader::SortDown) { + Ph::drawArrow(painter, r.translated(offset), Qt::UpArrow, swatch); + } + 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_button; + 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_button); + } + + // 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 +{ + 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; + 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 option ? static_cast<int>(swatch.color(S_base_divider).rgb()) : 0; + } + 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 0000000000..d6269fad76 --- /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 0000000000..4219c4cfce --- /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.4em; + 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 0000000000..3689cfc3ff --- /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 0000000000..f9573ba658 --- /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 0000000000..db006dbfcf --- /dev/null +++ b/src/gui/styles/dark/DarkStyle.cpp @@ -0,0 +1,125 @@ +/* + * 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> + +void DarkStyle::polish(QPalette& palette) +{ + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#3B3B3D")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#404042")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#424242")); + + palette.setColor(QPalette::Active, QPalette::WindowText, QStringLiteral("#CACBCE")); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QStringLiteral("#C8C8C6")); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QStringLiteral("#707070")); + + palette.setColor(QPalette::Active, QPalette::Text, QStringLiteral("#CACBCE")); + palette.setColor(QPalette::Inactive, QPalette::Text, QStringLiteral("#C8C8C6")); + palette.setColor(QPalette::Disabled, QPalette::Text, QStringLiteral("#707070")); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QStringLiteral("#7D7D82")); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QStringLiteral("#87888C")); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QStringLiteral("#737373")); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QStringLiteral("#252627")); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QStringLiteral("#2D2D2F")); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QStringLiteral("#333333")); + + palette.setColor(QPalette::Active, QPalette::Base, QStringLiteral("#27272A")); + palette.setColor(QPalette::Inactive, QPalette::Base, QStringLiteral("#2A2A2D")); + palette.setColor(QPalette::Disabled, QPalette::Base, QStringLiteral("#343437")); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QStringLiteral("#303036")); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QStringLiteral("#333338")); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QStringLiteral("#36363A")); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QStringLiteral("#2D532D")); + palette.setColor(QPalette::All, QPalette::ToolTipText, QStringLiteral("#BFBFBF")); + + palette.setColor(QPalette::Active, QPalette::Button, QStringLiteral("#28282B")); + palette.setColor(QPalette::Inactive, QPalette::Button, QStringLiteral("#2B2B2E")); + palette.setColor(QPalette::Disabled, QPalette::Button, QStringLiteral("#2B2A2A")); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QStringLiteral("#B9B9BE")); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QStringLiteral("#9E9FA5")); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QStringLiteral("#73747E")); + + palette.setColor(QPalette::Active, QPalette::Highlight, QStringLiteral("#2D532D")); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QStringLiteral("#294C29")); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QStringLiteral("#293D29")); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QStringLiteral("#CCCCCC")); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QStringLiteral("#C7C7C7")); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QStringLiteral("#707070")); + + palette.setColor(QPalette::All, QPalette::Light, QStringLiteral("#414145")); + palette.setColor(QPalette::All, QPalette::Midlight, QStringLiteral("#39393C")); + palette.setColor(QPalette::All, QPalette::Mid, QStringLiteral("#2F2F32")); + palette.setColor(QPalette::All, QPalette::Dark, QStringLiteral("#202022")); + palette.setColor(QPalette::All, QPalette::Shadow, QStringLiteral("#19191A")); + + palette.setColor(QPalette::All, QPalette::Link, QStringLiteral("#6BAE6B")); + palette.setColor(QPalette::Disabled, QPalette::Link, QStringLiteral("#9DE9D")); + palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#70A970")); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QStringLiteral("#98A998")); +} + +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, QStringLiteral("#2A2A2A")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#2D2D2D")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#2A2A2A")); + } +#elif defined(Q_OS_WIN) + // Register event filter for better dark mode support + WinUtils::registerEventFilters(); + palette.setColor(QPalette::All, QPalette::Window, QStringLiteral("#2F2F30")); +#else + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#2F2F30")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#313133")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#3A3A3B")); +#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 0000000000..aab949c3a2 --- /dev/null +++ b/src/gui/styles/dark/DarkStyle.h @@ -0,0 +1,37 @@ +/* + * 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: + using BaseStyle::polish; + void polish(QPalette& palette) override; + 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 0000000000..39ec32a2bb --- /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 0000000000..2f73c53b9f --- /dev/null +++ b/src/gui/styles/light/LightStyle.cpp @@ -0,0 +1,124 @@ +/* + * 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> + +void LightStyle::polish(QPalette& palette) +{ + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#F7F7F7")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#FCFCFC")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#EDEDED")); + + palette.setColor(QPalette::Active, QPalette::WindowText, QStringLiteral("#1D1D20")); + palette.setColor(QPalette::Inactive, QPalette::WindowText, QStringLiteral("#252528")); + palette.setColor(QPalette::Disabled, QPalette::WindowText, QStringLiteral("#8C8C92")); + + palette.setColor(QPalette::Active, QPalette::Text, QStringLiteral("#1D1D20")); + palette.setColor(QPalette::Inactive, QPalette::Text, QStringLiteral("#252528")); + palette.setColor(QPalette::Disabled, QPalette::Text, QStringLiteral("#8C8C92")); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + palette.setColor(QPalette::Active, QPalette::PlaceholderText, QStringLiteral("#71727D")); + palette.setColor(QPalette::Inactive, QPalette::PlaceholderText, QStringLiteral("#878893")); + palette.setColor(QPalette::Disabled, QPalette::PlaceholderText, QStringLiteral("#A3A4AC")); +#endif + + palette.setColor(QPalette::Active, QPalette::BrightText, QStringLiteral("#F3F3F4")); + palette.setColor(QPalette::Inactive, QPalette::BrightText, QStringLiteral("#EAEAEB")); + palette.setColor(QPalette::Disabled, QPalette::BrightText, QStringLiteral("#E4E5E7")); + + palette.setColor(QPalette::Active, QPalette::Base, QStringLiteral("#F9F9F9")); + palette.setColor(QPalette::Inactive, QPalette::Base, QStringLiteral("#F5F5F4")); + palette.setColor(QPalette::Disabled, QPalette::Base, QStringLiteral("#EFEFF2")); + + palette.setColor(QPalette::Active, QPalette::AlternateBase, QStringLiteral("#ECF3E8")); + palette.setColor(QPalette::Inactive, QPalette::AlternateBase, QStringLiteral("#EAF2E6")); + palette.setColor(QPalette::Disabled, QPalette::AlternateBase, QStringLiteral("#E1E9DD")); + + palette.setColor(QPalette::All, QPalette::ToolTipBase, QStringLiteral("#548C1D")); + palette.setColor(QPalette::All, QPalette::ToolTipText, QStringLiteral("#F7F7F7")); + + palette.setColor(QPalette::Active, QPalette::Button, QStringLiteral("#D4D5DD")); + palette.setColor(QPalette::Inactive, QPalette::Button, QStringLiteral("#DCDCE0")); + palette.setColor(QPalette::Disabled, QPalette::Button, QStringLiteral("#E5E5E6")); + + palette.setColor(QPalette::Active, QPalette::ButtonText, QStringLiteral("#181A18")); + palette.setColor(QPalette::Inactive, QPalette::ButtonText, QStringLiteral("#5F6671")); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, QStringLiteral("#97979B")); + + palette.setColor(QPalette::Active, QPalette::Highlight, QStringLiteral("#549712")); + palette.setColor(QPalette::Inactive, QPalette::Highlight, QStringLiteral("#528D16")); + palette.setColor(QPalette::Disabled, QPalette::Highlight, QStringLiteral("#6F9847")); + + palette.setColor(QPalette::Active, QPalette::HighlightedText, QStringLiteral("#FCFCFC")); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, QStringLiteral("#F2F2F2")); + palette.setColor(QPalette::Disabled, QPalette::HighlightedText, QStringLiteral("#D9D9D9")); + + palette.setColor(QPalette::All, QPalette::Light, QStringLiteral("#F9F9F9")); + palette.setColor(QPalette::All, QPalette::Midlight, QStringLiteral("#E9E9EB")); + palette.setColor(QPalette::All, QPalette::Mid, QStringLiteral("#C9C9CF")); + palette.setColor(QPalette::All, QPalette::Dark, QStringLiteral("#BBBBC2")); + palette.setColor(QPalette::All, QPalette::Shadow, QStringLiteral("#6C6D79")); + + palette.setColor(QPalette::All, QPalette::Link, QStringLiteral("#429F14")); + palette.setColor(QPalette::Disabled, QPalette::Link, QStringLiteral("#949F8F")); + palette.setColor(QPalette::All, QPalette::LinkVisited, QStringLiteral("#3F8C17")); + palette.setColor(QPalette::Disabled, QPalette::LinkVisited, QStringLiteral("#838C7E")); +} + +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, QStringLiteral("#D6D6D6")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#F6F6F6")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#D4D4D4")); + } +#elif defined(Q_OS_WIN) + palette.setColor(QPalette::All, QPalette::Window, QStringLiteral("#FFFFFF")); +#else + palette.setColor(QPalette::Active, QPalette::Window, QStringLiteral("#EFF0F1")); + palette.setColor(QPalette::Inactive, QPalette::Window, QStringLiteral("#EFF0F1")); + palette.setColor(QPalette::Disabled, QPalette::Window, QStringLiteral("#E1E2E4")); +#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 0000000000..72153bd15f --- /dev/null +++ b/src/gui/styles/light/LightStyle.h @@ -0,0 +1,37 @@ +/* + * 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: + using BaseStyle::polish; + void polish(QPalette& palette) override; + 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 0000000000..e6ea4d1389 --- /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: palette(highlight); +} + +QToolTip { + color: #F7F7F7; + background-color: #548C1D; +} diff --git a/src/gui/styles/styles.qrc b/src/gui/styles/styles.qrc new file mode 100644 index 0000000000..c8e9057dc9 --- /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/main.cpp b/src/main.cpp index 846baa67db..524f112b3d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,9 @@ #include "gui/Application.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" +#include "gui/osutils/OSUtils.h" +#include "gui/styles/dark/DarkStyle.h" +#include "gui/styles/light/LightStyle.h" #if defined(WITH_ASAN) && defined(WITH_LSAN) #include <sanitizer/lsan_interface.h> @@ -60,6 +63,20 @@ int main(int argc, char** argv) Application app(argc, argv); Application::setApplicationName("KeePassXC"); Application::setApplicationVersion(KEEPASSXC_VERSION); + + QString appTheme = config()->get("GUI/ApplicationTheme").toString(); + if (appTheme == "auto") { + if (osUtils->isDarkMode()) { + QApplication::setStyle(new DarkStyle); + } else { + QApplication::setStyle(new LightStyle); + } + } else if (appTheme == "light") { + QApplication::setStyle(new LightStyle); + } else if (appTheme == "dark") { + QApplication::setStyle(new DarkStyle); + } + // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) Bootstrap::bootstrapApplication(); From e26063a872fc800d7dc9fc30ff10e3233420da38 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sat, 1 Feb 2020 08:51:11 -0500 Subject: [PATCH 069/215] Fix compile errors when building snap package * System icons are no longer used eliminating the need to differentiate behavior for the snap package on Linux. --- src/core/Bootstrap.cpp | 6 ------ src/core/FilePath.cpp | 24 ++---------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index 46aff92d58..c983253f0b 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -85,12 +85,6 @@ namespace Bootstrap bootstrap(); MessageBox::initializeButtonDefs(); -#ifdef KEEPASSXC_DIST_SNAP - // snap: force fallback theme to avoid using system theme (gtk integration) - // with missing actions just like on Windows and macOS - QIcon::setThemeSearchPaths(QStringList() << ":/icons"); -#endif - #ifdef Q_OS_MACOS // Don't show menu icons on OSX QApplication::setAttribute(Qt::AA_DontShowIconsInMenus); diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 95e7fc0c24..6ae52bd8f3 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -97,42 +97,22 @@ QString FilePath::wordlistPath(const QString& name) QIcon FilePath::applicationIcon() { -#ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc", false); -#else - return icon("apps", "keepassxc", false); -#endif } QIcon FilePath::trayIcon() { - bool darkIcon = useDarkIcon(); - -#ifdef KEEPASSXC_DIST_SNAP - return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); -#else - return (darkIcon) ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); -#endif + return useDarkIcon() ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); } QIcon FilePath::trayIconLocked() { -#ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc-locked", false); -#else - return icon("apps", "keepassxc-locked", false); -#endif } QIcon FilePath::trayIconUnlocked() { - bool darkIcon = useDarkIcon(); - -#ifdef KEEPASSXC_DIST_SNAP - return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); -#else - return darkIcon ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); -#endif + return useDarkIcon() ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); } QIcon FilePath::icon(const QString& category, const QString& name, bool recolor) From 50e52df04b27bc3f248be10bd30340d5c527eb6c Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sat, 25 Jan 2020 09:22:13 -0500 Subject: [PATCH 070/215] Fix issues with PopupHelpWidget on Linux and macOS * Clean up parent alignment code * Don't hide widget if it currently has focus * Use Qt::Tool window type on macOS as well. This prevents the popup help from hiding to the background if the main window has focus. * Fixes #2814 --- src/gui/SearchHelpWidget.ui | 68 ++--------------------------- src/gui/SearchWidget.cpp | 1 - src/gui/widgets/PopupHelpWidget.cpp | 32 ++++++-------- src/gui/widgets/PopupHelpWidget.h | 1 - 4 files changed, 18 insertions(+), 84 deletions(-) diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui index 4668e409ba..d2d8a23394 100644 --- a/src/gui/SearchHelpWidget.ui +++ b/src/gui/SearchHelpWidget.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>487</width> - <height>326</height> + <width>397</width> + <height>264</height> </rect> </property> <property name="windowTitle"> @@ -20,28 +20,10 @@ <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> @@ -63,7 +45,7 @@ <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> @@ -96,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"> @@ -222,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"> @@ -299,12 +248,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"> @@ -404,9 +347,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 96c52e6393..001c3d861e 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -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); diff --git a/src/gui/widgets/PopupHelpWidget.cpp b/src/gui/widgets/PopupHelpWidget.cpp index 269c31c5b2..2a604dce9f 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 3c02ccc1a3..353121c6a9 100644 --- a/src/gui/widgets/PopupHelpWidget.h +++ b/src/gui/widgets/PopupHelpWidget.h @@ -37,7 +37,6 @@ class PopupHelpWidget : public QFrame private: void alignWithParent(); - QPointer<QWidget> m_parentWindow; QPointer<QWidget> m_appWindow; QPoint m_offset; From ca471e99861e84c4d4a19d6ac73748e538546c0d Mon Sep 17 00:00:00 2001 From: Andrew Meyer <apmeyer@bgsu.edu> Date: Sun, 16 Feb 2020 01:57:36 -0500 Subject: [PATCH 071/215] Display database path in root group tooltip When mousing over the root group entry, show the file path for the current database. Fixes #4038 --- src/gui/group/GroupModel.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index d3f2f40f65..a9b61315dc 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -142,6 +142,13 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const 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(); } From a6c3c118a78a8ea40376948228c6713360bac4c8 Mon Sep 17 00:00:00 2001 From: Timo Ulich <timo@ulich.net> Date: Fri, 3 Jan 2020 22:58:17 +0100 Subject: [PATCH 072/215] Add the name of the group to the results for browser extensions Fixes #466 So it can be displayed in the autocomplete list when more than one login matches. For users that use groups and have similar names for multiple logins but organized in different groups --- src/browser/BrowserService.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index a09a4d5649..3131719067 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -859,6 +859,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry) res["password"] = entry->resolveMultiplePlaceholders(entry->password()); res["name"] = entry->resolveMultiplePlaceholders(entry->title()); res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex()); + res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name()); if (entry->hasTotp()) { res["totp"] = entry->totp(); From e6186b07e1dbd61df3d4227ea89419b2c7a6f12f Mon Sep 17 00:00:00 2001 From: varjolintu <sami.vanttinen@protonmail.com> Date: Mon, 2 Mar 2020 10:18:33 +0200 Subject: [PATCH 073/215] Add Created column to Browser Integration at Database settings --- src/browser/BrowserService.cpp | 2 ++ src/core/CustomData.cpp | 3 ++- src/core/CustomData.h | 1 + src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp | 8 +++++--- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 3131719067..46add89d25 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -338,6 +338,8 @@ QString BrowserService::storeKey(const QString& key) hideWindow(); db->metadata()->customData()->set(ASSOCIATE_KEY_PREFIX + id, key); + db->metadata()->customData()->set(QString("%1_%2").arg(CustomData::Created, id), + Clock::currentDateTime().toString(Qt::SystemLocaleShortDate)); return id; } diff --git a/src/core/CustomData.cpp b/src/core/CustomData.cpp index f009176a0d..95aee805d1 100644 --- a/src/core/CustomData.cpp +++ b/src/core/CustomData.cpp @@ -20,7 +20,8 @@ #include "core/Global.h" -const QString CustomData::LastModified = "_LAST_MODIFIED"; +const QString CustomData::LastModified = QStringLiteral("_LAST_MODIFIED"); +const QString CustomData::Created = QStringLiteral("_CREATED"); CustomData::CustomData(QObject* parent) : QObject(parent) diff --git a/src/core/CustomData.h b/src/core/CustomData.h index 126d4d84e6..212765f769 100644 --- a/src/core/CustomData.h +++ b/src/core/CustomData.h @@ -47,6 +47,7 @@ class CustomData : public QObject bool operator!=(const CustomData& other) const; static const QString LastModified; + static const QString Created; signals: void customDataModified(); diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp index 4ea30c1f6f..906278c92b 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp @@ -120,14 +120,16 @@ 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)) { QString strippedKey = key; strippedKey.remove(BrowserService::ASSOCIATE_KEY_PREFIX); - m_customDataModel->appendRow(QList<QStandardItem*>() << new QStandardItem(strippedKey) - << new QStandardItem(customData()->value(key))); + auto created = customData()->value(QString("%1_%2").arg(CustomData::Created, strippedKey)); + m_customDataModel->appendRow(QList<QStandardItem*>() + << new QStandardItem(strippedKey) + << new QStandardItem(customData()->value(key)) << new QStandardItem(created)); } } From 6bce5836f99819410876d3125a7e3aa12028ce1c Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Mon, 20 Jan 2020 09:47:02 -0500 Subject: [PATCH 074/215] Fix crash when switching tabs while unlocking --- src/gui/DatabaseOpenWidget.cpp | 74 +++++++++++++++++----------------- src/gui/DatabaseOpenWidget.h | 1 + 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 9559047a95..f3bb502d04 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -164,12 +164,14 @@ void DatabaseOpenWidget::load(const QString& filename) void DatabaseOpenWidget::clearForms() { - m_ui->editPassword->setText(""); - m_ui->comboKeyFile->clear(); - m_ui->comboKeyFile->setEditText(""); - m_ui->checkTouchID->setChecked(false); - m_ui->buttonTogglePassword->setChecked(false); - m_db.reset(); + if (!m_isOpeningDatabase) { + m_ui->editPassword->setText(""); + m_ui->comboKeyFile->clear(); + m_ui->comboKeyFile->setEditText(""); + m_ui->checkTouchID->setChecked(false); + m_ui->buttonTogglePassword->setChecked(false); + m_db.reset(); + } } QSharedPointer<Database> DatabaseOpenWidget::database() @@ -196,6 +198,7 @@ void DatabaseOpenWidget::openDatabase() m_ui->buttonTogglePassword->setChecked(false); QCoreApplication::processEvents(); + m_isOpeningDatabase = true; m_db.reset(new Database()); QString error; @@ -206,7 +209,34 @@ void DatabaseOpenWidget::openDatabase() QApplication::restoreOverrideCursor(); m_ui->passwordFormFrame->setEnabled(true); - if (!ok) { + if (ok) { +#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); + m_isOpeningDatabase = false; + clearForms(); + } else { + m_isOpeningDatabase = false; if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) { QScopedPointer<QMessageBox> msgBox(new QMessageBox(this)); msgBox->setIcon(QMessageBox::Critical); @@ -226,40 +256,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 diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index aa0a4315bc..61a220f438 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -79,6 +79,7 @@ private slots: private: bool m_yubiKeyBeingPolled = false; bool m_keyFileComboEdited = false; + bool m_isOpeningDatabase = false; Q_DISABLE_COPY(DatabaseOpenWidget) }; From 7ac292e09b31ecf56969755c2ddc70d0a64e9d69 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Thu, 5 Mar 2020 22:59:07 -0500 Subject: [PATCH 075/215] Fix crashes on database save * Add saving mutex to database class to prevent re-entrant saving * Prevent saving multiple times to the same file if the database is not marked as modified * Prevent locking the database while saving. This also prevents closing the application and database tab while saving. * FileWatcher: only perform async checksum calculations when triggered by timer (prevents random GUI freezes) * Re-attempt database lock when requested during save operation * Prevent database tabs from closing before all databases are locked on quit --- src/cli/Create.cpp | 1 + src/cli/Import.cpp | 1 + src/core/Database.cpp | 44 +++++++++++++++++++++++++++++------ src/core/Database.h | 3 +++ src/core/FileWatcher.cpp | 34 ++++++++++++++------------- src/core/FileWatcher.h | 1 + src/core/Metadata.cpp | 19 +++++++++++++-- src/core/Metadata.h | 3 +++ src/gui/DatabaseTabWidget.cpp | 31 +++++++++++++++++------- src/gui/DatabaseTabWidget.h | 2 +- src/gui/DatabaseWidget.cpp | 22 +++++++++++++----- src/gui/DatabaseWidget.h | 1 + 12 files changed, 122 insertions(+), 40 deletions(-) diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index c8e3b771fc..68289bf250 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -121,6 +121,7 @@ int Create::execute(const QStringList& arguments) QSharedPointer<Database> db(new Database); db->setKey(key); + db->setInitialized(true); if (decryptionTime != 0) { auto kdf = db->kdf(); diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index dd7b12c641..e34a4cebff 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -84,6 +84,7 @@ int Import::execute(const QStringList& arguments) Database db; db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); db.setKey(key); + db.setInitialized(true); if (!db.import(xmlExportPath, &errorMessage)) { errorTextStream << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl; diff --git a/src/core/Database.cpp b/src/core/Database.cpp index fcd48f1e20..a3637bb52e 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -18,6 +18,7 @@ #include "Database.h" +#include "core/AsyncTask.h" #include "core/Clock.h" #include "core/FileWatcher.h" #include "core/Group.h" @@ -159,6 +160,15 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> return true; } +bool Database::isSaving() +{ + bool locked = m_saveMutex.tryLock(); + if (locked) { + m_saveMutex.unlock(); + } + return !locked; +} + /** * Save the database to the current file path. It is an error to call this function * if no file path has been defined. @@ -201,6 +211,25 @@ bool Database::save(QString* error, bool atomic, bool backup) */ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup) { + // Disallow overlapping save operations + if (isSaving()) { + if (error) { + *error = tr("Database save is already in progress."); + } + return false; + } + + // Never save an uninitialized database + if (!m_initialized) { + if (error) { + *error = tr("Could not save, database has not been initialized!"); + } + return false; + } + + // Prevent destructive operations while saving + QMutexLocker locker(&m_saveMutex); + if (filePath == m_data.filePath) { // Disallow saving to the same file if read-only if (m_data.isReadOnly) { @@ -226,7 +255,7 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool m_fileWatcher->stop(); auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath; - bool ok = performSave(canonicalFilePath, error, atomic, backup); + bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(canonicalFilePath, error, atomic, backup); }); if (ok) { markAsClean(); setFilePath(filePath); @@ -389,6 +418,9 @@ bool Database::import(const QString& xmlExportPath, QString* error) void Database::releaseData() { + // Prevent data release while saving + QMutexLocker locker(&m_saveMutex); + s_uuidMap.remove(m_uuid); m_uuid = QUuid(); @@ -397,22 +429,20 @@ void Database::releaseData() } m_data.clear(); + m_metadata->clear(); if (m_rootGroup && m_rootGroup->parent() == this) { delete m_rootGroup; } - if (m_metadata) { - delete m_metadata; - } - if (m_fileWatcher) { - delete m_fileWatcher; - } + + m_fileWatcher->stop(); m_deletedObjects.clear(); m_commonUsernames.clear(); m_initialized = false; m_modified = false; + m_modifiedTimer.stop(); } /** diff --git a/src/core/Database.h b/src/core/Database.h index d3d88e7d2e..74024424db 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -21,6 +21,7 @@ #include <QDateTime> #include <QHash> +#include <QMutex> #include <QPointer> #include <QScopedPointer> #include <QTimer> @@ -85,6 +86,7 @@ class Database : public QObject void setEmitModified(bool value); bool isReadOnly() const; void setReadOnly(bool readOnly); + bool isSaving(); QUuid uuid() const; QString filePath() const; @@ -208,6 +210,7 @@ public slots: QPointer<Group> m_rootGroup; QList<DeletedObject> m_deletedObjects; QTimer m_modifiedTimer; + QMutex m_saveMutex; QPointer<FileWatcher> m_fileWatcher; bool m_initialized = false; bool m_modified = false; diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp index 0bc5e34440..0619a3cbf3 100644 --- a/src/core/FileWatcher.cpp +++ b/src/core/FileWatcher.cpp @@ -43,6 +43,11 @@ FileWatcher::FileWatcher(QObject* parent) m_fileIgnoreDelayTimer.setSingleShot(true); } +FileWatcher::~FileWatcher() +{ + stop(); +} + void FileWatcher::start(const QString& filePath, int checksumIntervalSeconds, int checksumSizeKibibytes) { stop(); @@ -120,8 +125,7 @@ void FileWatcher::checkFileChanged() // Prevent reentrance m_ignoreFileChange = true; - // Only trigger the change notice if there is a checksum mismatch - auto checksum = calculateChecksum(); + auto checksum = AsyncTask::runAndWaitForFuture([this]() -> QByteArray { return calculateChecksum(); }); if (checksum != m_fileChecksum) { m_fileChecksum = checksum; m_fileChangeDelayTimer.start(0); @@ -132,21 +136,19 @@ void FileWatcher::checkFileChanged() QByteArray FileWatcher::calculateChecksum() { - return AsyncTask::runAndWaitForFuture([this]() -> QByteArray { - QFile file(m_filePath); - if (file.open(QFile::ReadOnly)) { - QCryptographicHash hash(QCryptographicHash::Sha256); - if (m_fileChecksumSizeBytes > 0) { - hash.addData(file.read(m_fileChecksumSizeBytes)); - } else { - hash.addData(&file); - } - return hash.result(); + QFile file(m_filePath); + if (file.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); + if (m_fileChecksumSizeBytes > 0) { + hash.addData(file.read(m_fileChecksumSizeBytes)); + } else { + hash.addData(&file); } - // If we fail to open the file return the last known checksum, this - // prevents unnecessary merge requests on intermittent network shares - return m_fileChecksum; - }); + return hash.result(); + } + // If we fail to open the file return the last known checksum, this + // prevents unnecessary merge requests on intermittent network shares + return m_fileChecksum; } BulkFileWatcher::BulkFileWatcher(QObject* parent) diff --git a/src/core/FileWatcher.h b/src/core/FileWatcher.h index 9b55badc16..7a90853606 100644 --- a/src/core/FileWatcher.h +++ b/src/core/FileWatcher.h @@ -29,6 +29,7 @@ class FileWatcher : public QObject public: explicit FileWatcher(QObject* parent = nullptr); + ~FileWatcher() override; void start(const QString& path, int checksumIntervalSeconds = 0, int checksumSizeKibibytes = -1); void stop(); diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index fb18f711b9..200fef89f7 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -31,7 +31,13 @@ Metadata::Metadata(QObject* parent) , m_customData(new CustomData(this)) , m_updateDatetime(true) { - m_data.generator = "KeePassXC"; + init(); + connect(m_customData, SIGNAL(customDataModified()), SIGNAL(metadataModified())); +} + +void Metadata::init() +{ + m_data.generator = QStringLiteral("KeePassXC"); m_data.maintenanceHistoryDays = 365; m_data.masterKeyChangeRec = -1; m_data.masterKeyChangeForce = -1; @@ -52,8 +58,17 @@ Metadata::Metadata(QObject* parent) m_entryTemplatesGroupChanged = now; m_masterKeyChanged = now; m_settingsChanged = now; +} - connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(metadataModified())); +void Metadata::clear() +{ + init(); + m_customIcons.clear(); + m_customIconCacheKeys.clear(); + m_customIconScaledCacheKeys.clear(); + m_customIconsOrder.clear(); + m_customIconsHashes.clear(); + m_customData->clear(); } template <class P, class V> bool Metadata::set(P& property, const V& value) diff --git a/src/core/Metadata.h b/src/core/Metadata.h index b39dafaf02..85fb2fdd92 100644 --- a/src/core/Metadata.h +++ b/src/core/Metadata.h @@ -62,6 +62,9 @@ class Metadata : public QObject bool protectNotes; }; + void init(); + void clear(); + QString generator() const; QString name() const; QDateTime nameChanged() const; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 5523f7c622..b0ced46df4 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -353,13 +353,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) @@ -597,15 +601,26 @@ 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() { + int numLocked = 0; for (int i = 0, c = count(); 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 == count(); } /** diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 5b2d3f0087..b8cbdbb6f5 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -71,7 +71,7 @@ public slots: 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); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 0eb713dad8..5bda87be15 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -255,15 +255,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; } } @@ -272,6 +272,11 @@ bool DatabaseWidget::isLocked() const return currentMode() == Mode::LockedMode; } +bool DatabaseWidget::isSaving() const +{ + return m_db->isSaving(); +} + bool DatabaseWidget::isSearchActive() const { return m_entryView->inSearchMode(); @@ -1380,6 +1385,12 @@ bool DatabaseWidget::lock() 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(); @@ -1660,7 +1671,6 @@ bool DatabaseWidget::save() auto focusWidget = qApp->focusWidget(); - // TODO: Make this async // Lock out interactions m_entryView->setDisabled(true); m_groupView->setDisabled(true); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 6420a3b242..a96a34a9f3 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -80,6 +80,7 @@ class DatabaseWidget : public QStackedWidget DatabaseWidget::Mode currentMode() const; bool isLocked() const; + bool isSaving() const; bool isSearchActive() const; bool isEntryEditActive() const; bool isGroupEditActive() const; From 91c6e436b3622015d5b7c27b6b5230bb1f285df7 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Wed, 4 Mar 2020 09:37:13 -0500 Subject: [PATCH 076/215] Dynamically determine database validity * Check that the database composite key exists, has sub-keys associated with it, and the root group exists. --- src/browser/BrowserService.cpp | 4 +-- src/cli/Create.cpp | 1 - src/cli/Import.cpp | 1 - src/core/Database.cpp | 42 +++++++--------------------- src/core/Database.h | 5 ---- src/gui/DatabaseTabWidget.cpp | 2 +- src/gui/wizard/NewDatabaseWizard.cpp | 6 +--- tests/TestKeePass1Reader.cpp | 2 +- 8 files changed, 14 insertions(+), 49 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 46add89d25..b76d0c31eb 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -1122,9 +1122,7 @@ QSharedPointer<Database> BrowserService::selectedDatabase() for (int i = 0;; ++i) { auto* dbWidget = m_dbTabWidget->databaseWidgetFromIndex(i); // Add only open databases - if (dbWidget && dbWidget->database()->hasKey() - && (dbWidget->currentMode() == DatabaseWidget::Mode::ViewMode - || dbWidget->currentMode() == DatabaseWidget::Mode::EditMode)) { + if (dbWidget && !dbWidget->isLocked()) { databaseWidgets.push_back(dbWidget); continue; } diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index 68289bf250..c8e3b771fc 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -121,7 +121,6 @@ int Create::execute(const QStringList& arguments) QSharedPointer<Database> db(new Database); db->setKey(key); - db->setInitialized(true); if (decryptionTime != 0) { auto kdf = db->kdf(); diff --git a/src/cli/Import.cpp b/src/cli/Import.cpp index e34a4cebff..dd7b12c641 100644 --- a/src/cli/Import.cpp +++ b/src/cli/Import.cpp @@ -84,7 +84,6 @@ int Import::execute(const QStringList& arguments) Database db; db.setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); db.setKey(key); - db.setInitialized(true); if (!db.import(xmlExportPath, &errorMessage)) { errorTextStream << QObject::tr("Unable to import XML database: %1").arg(errorMessage) << endl; diff --git a/src/core/Database.cpp b/src/core/Database.cpp index a3637bb52e..9c59c70bd2 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -112,13 +112,6 @@ bool Database::open(QSharedPointer<const CompositeKey> key, QString* error, bool */ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error, bool readOnly) { - if (isInitialized() && m_modified) { - emit databaseDiscarded(); - } - - m_initialized = false; - setEmitModified(false); - QFile dbFile(filePath); if (!dbFile.exists()) { if (error) { @@ -138,6 +131,8 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> return false; } + setEmitModified(false); + KeePass2Reader reader; if (!reader.readDatabase(&dbFile, std::move(key), this)) { if (error) { @@ -152,7 +147,6 @@ bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> markAsClean(); - m_initialized = true; emit databaseOpened(); m_fileWatcher->start(canonicalFilePath(), 30, 1); setEmitModified(true); @@ -220,7 +214,7 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool } // Never save an uninitialized database - if (!m_initialized) { + if (!isInitialized()) { if (error) { *error = tr("Could not save, database has not been initialized!"); } @@ -346,7 +340,7 @@ bool Database::writeDatabase(QIODevice* device, QString* error) } PasswordKey oldTransformedKey; - if (m_data.hasKey) { + if (m_data.key->isEmpty()) { oldTransformedKey.setHash(m_data.transformedMasterKey->rawKey()); } @@ -440,7 +434,6 @@ void Database::releaseData() m_deletedObjects.clear(); m_commonUsernames.clear(); - m_initialized = false; m_modified = false; m_modifiedTimer.stop(); } @@ -496,22 +489,14 @@ void Database::setReadOnly(bool readOnly) } /** - * Returns true if database has been fully decrypted and populated, i.e. if - * it's not just an empty default instance. + * Returns true if the database key exists, has subkeys, and the + * root group exists * * @return true if database has been fully initialized */ bool Database::isInitialized() const { - return m_initialized; -} - -/** - * @param initialized true to mark database as initialized - */ -void Database::setInitialized(bool initialized) -{ - m_initialized = initialized; + return m_data.key && !m_data.key->isEmpty() && m_rootGroup; } Group* Database::rootGroup() @@ -535,7 +520,7 @@ void Database::setRootGroup(Group* group) { Q_ASSERT(group); - if (isInitialized() && m_modified) { + if (isInitialized() && isModified()) { emit databaseDiscarded(); } @@ -723,7 +708,6 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, if (!key) { m_data.key.reset(); m_data.transformedMasterKey.reset(new PasswordKey()); - m_data.hasKey = false; return true; } @@ -733,7 +717,7 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, } PasswordKey oldTransformedMasterKey; - if (m_data.hasKey) { + if (m_data.key && !m_data.key->isEmpty()) { oldTransformedMasterKey.setHash(m_data.transformedMasterKey->rawKey()); } @@ -749,7 +733,6 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, if (!transformedMasterKey.isEmpty()) { m_data.transformedMasterKey->setHash(transformedMasterKey); } - m_data.hasKey = true; if (updateChangedTime) { m_metadata->setMasterKeyChanged(Clock::currentDateTimeUtc()); } @@ -761,14 +744,9 @@ bool Database::setKey(const QSharedPointer<const CompositeKey>& key, return true; } -bool Database::hasKey() const -{ - return m_data.hasKey; -} - bool Database::verifyKey(const QSharedPointer<CompositeKey>& key) const { - Q_ASSERT(hasKey()); + Q_ASSERT(!m_data.key->isEmpty()); if (!m_data.challengeResponseKey->rawKey().isEmpty()) { QByteArray result; diff --git a/src/core/Database.h b/src/core/Database.h index 74024424db..63a1f8cb6c 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -81,7 +81,6 @@ class Database : public QObject void releaseData(); bool isInitialized() const; - void setInitialized(bool initialized); bool isModified() const; void setEmitModified(bool value); bool isReadOnly() const; @@ -115,7 +114,6 @@ class Database : public QObject QList<QString> commonUsernames(); - bool hasKey() const; QSharedPointer<const CompositeKey> key() const; bool setKey(const QSharedPointer<const CompositeKey>& key, bool updateChangedTime = true, @@ -168,7 +166,6 @@ public slots: QScopedPointer<PasswordKey> transformedMasterKey; QScopedPointer<PasswordKey> challengeResponseKey; - bool hasKey = false; QSharedPointer<const CompositeKey> key; QSharedPointer<Kdf> kdf = QSharedPointer<AesKdf>::create(true); @@ -190,7 +187,6 @@ public slots: transformedMasterKey.reset(); challengeResponseKey.reset(); - hasKey = false; key.reset(); kdf.reset(); @@ -212,7 +208,6 @@ public slots: QTimer m_modifiedTimer; QMutex m_saveMutex; QPointer<FileWatcher> m_fileWatcher; - bool m_initialized = false; bool m_modified = false; bool m_emitModified; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index b0ced46df4..6ea8789de7 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -671,7 +671,7 @@ void DatabaseTabWidget::relockPendingDatabase() return; } - if (m_dbWidgetPendingLock->isLocked() || !m_dbWidgetPendingLock->database()->hasKey()) { + if (m_dbWidgetPendingLock->isLocked() || !m_dbWidgetPendingLock->database()->isInitialized()) { m_dbWidgetPendingLock = nullptr; return; } diff --git a/src/gui/wizard/NewDatabaseWizard.cpp b/src/gui/wizard/NewDatabaseWizard.cpp index eaadc53ff2..34c594046f 100644 --- a/src/gui/wizard/NewDatabaseWizard.cpp +++ b/src/gui/wizard/NewDatabaseWizard.cpp @@ -57,11 +57,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/tests/TestKeePass1Reader.cpp b/tests/TestKeePass1Reader.cpp index 078447acb9..30f744e287 100644 --- a/tests/TestKeePass1Reader.cpp +++ b/tests/TestKeePass1Reader.cpp @@ -106,7 +106,7 @@ void TestKeePass1Reader::testBasic() void TestKeePass1Reader::testMasterKey() { - QVERIFY(m_db->hasKey()); + QVERIFY(m_db->isInitialized()); QCOMPARE(m_db->kdf()->rounds(), 713); } From a8c02fdc3cb39e8cabfd7012a6104e60938d3bb8 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Wed, 4 Mar 2020 10:05:33 -0500 Subject: [PATCH 077/215] Move database open to async task * Wrap key transformation in AsyncTask when reading a database. Significantly reduces user interface lockup. * Replace root group with new group instead of deleting the pointer (fulfills member validity promise). --- src/core/Database.cpp | 17 ++++++++--------- src/core/Group.cpp | 9 +-------- src/format/Kdbx3Reader.cpp | 4 +++- src/format/Kdbx4Reader.cpp | 4 +++- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 9c59c70bd2..66af624ecb 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -415,27 +415,26 @@ void Database::releaseData() // Prevent data release while saving QMutexLocker locker(&m_saveMutex); - s_uuidMap.remove(m_uuid); - m_uuid = QUuid(); - if (m_modified) { emit databaseDiscarded(); } + setEmitModified(false); + m_modified = false; + m_modifiedTimer.stop(); + + s_uuidMap.remove(m_uuid); + m_uuid = QUuid(); + m_data.clear(); m_metadata->clear(); - if (m_rootGroup && m_rootGroup->parent() == this) { - delete m_rootGroup; - } + setRootGroup(new Group()); m_fileWatcher->stop(); m_deletedObjects.clear(); m_commonUsernames.clear(); - - m_modified = false; - m_modifiedTimer.stop(); } /** diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 2c0d67091d..eb795f9504 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -967,14 +967,7 @@ void Group::removeEntry(Entry* entry) void Group::connectDatabaseSignalsRecursive(Database* db) { if (m_db) { - disconnect(SIGNAL(groupDataChanged(Group*)), m_db); - disconnect(SIGNAL(groupAboutToRemove(Group*)), m_db); - disconnect(SIGNAL(groupRemoved()), m_db); - disconnect(SIGNAL(groupAboutToAdd(Group*, int)), m_db); - disconnect(SIGNAL(groupAdded()), m_db); - disconnect(SIGNAL(aboutToMove(Group*, Group*, int)), m_db); - disconnect(SIGNAL(groupMoved()), m_db); - disconnect(SIGNAL(groupModified()), m_db); + disconnect(m_db); } for (Entry* entry : asConst(m_entries)) { diff --git a/src/format/Kdbx3Reader.cpp b/src/format/Kdbx3Reader.cpp index 9196bc6161..cce46deb40 100644 --- a/src/format/Kdbx3Reader.cpp +++ b/src/format/Kdbx3Reader.cpp @@ -18,6 +18,7 @@ #include "Kdbx3Reader.h" +#include "core/AsyncTask.h" #include "core/Endian.h" #include "core/Group.h" #include "crypto/CryptoHash.h" @@ -47,7 +48,8 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device, return false; } - if (!db->setKey(key, false)) { + bool ok = AsyncTask::runAndWaitForFuture([&] { return db->setKey(key, false); }); + if (!ok) { raiseError(tr("Unable to calculate master key")); return false; } diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp index d0914a04ed..ebdf634a1d 100644 --- a/src/format/Kdbx4Reader.cpp +++ b/src/format/Kdbx4Reader.cpp @@ -19,6 +19,7 @@ #include <QBuffer> +#include "core/AsyncTask.h" #include "core/Endian.h" #include "core/Group.h" #include "crypto/CryptoHash.h" @@ -47,7 +48,8 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device, return false; } - if (!db->setKey(key, false, false)) { + bool ok = AsyncTask::runAndWaitForFuture([&] { return db->setKey(key, false, false); }); + if (!ok) { raiseError(tr("Unable to calculate master key")); return false; } From 1d7ef5d4eb67f935741d95f638770e38ed04f059 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 8 Mar 2020 22:45:51 -0400 Subject: [PATCH 078/215] Move theme detection into Application * Add function to Application to quickly determine if in light or dark theme * Add kpxcApp symbol * Explicitly define main function for GUI tests to improve performance and use custom Application. --- src/core/Config.cpp | 10 +++++++--- src/core/Config.h | 3 ++- src/gui/Application.cpp | 36 ++++++++++++++++++++++++++++++++---- src/gui/Application.h | 4 ++++ src/main.cpp | 16 ---------------- tests/gui/TestGui.cpp | 21 ++++++++++++++++----- tests/gui/TestGuiBrowser.cpp | 17 ++++++++++++++++- 7 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 41395214d7..5965367b17 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -41,7 +41,7 @@ static const QMap<QString, QString> deprecationMap = { {QStringLiteral("security/IconDownloadFallbackToGoogle"), QStringLiteral("security/IconDownloadFallback")}, }; -Config* Config::m_instance(nullptr); +QPointer<Config> Config::m_instance(nullptr); QVariant Config::get(const QString& key) { @@ -246,13 +246,17 @@ Config* Config::instance() void Config::createConfigFromFile(const QString& file) { - Q_ASSERT(!m_instance); + if (m_instance) { + delete m_instance; + } m_instance = new Config(file, qApp); } void Config::createTempFileInstance() { - Q_ASSERT(!m_instance); + if (m_instance) { + delete m_instance; + } auto* tmpFile = new QTemporaryFile(); bool openResult = tmpFile->open(); Q_ASSERT(openResult); diff --git a/src/core/Config.h b/src/core/Config.h index d65b3256b6..ef6dd6af1d 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -19,6 +19,7 @@ #ifndef KEEPASSX_CONFIG_H #define KEEPASSX_CONFIG_H +#include <QPointer> #include <QScopedPointer> #include <QVariant> @@ -53,7 +54,7 @@ class Config : public QObject void init(const QString& fileName); void upgrade(); - static Config* m_instance; + static QPointer<Config> m_instance; QScopedPointer<QSettings> m_settings; QHash<QString, QVariant> m_defaults; diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index b79f2c30a9..7a2e956fca 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 @@ -65,6 +68,26 @@ Application::Application(int& argc, char** argv) registerUnixSignals(); #endif + QString appTheme = config()->get("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, only check for dark theme when not on Windows +#ifndef Q_OS_WIN + m_darkTheme = osUtils->isDarkMode(); +#endif + } + QString userName = qgetenv("USER"); if (userName.isEmpty()) { userName = qgetenv("USERNAME"); @@ -281,3 +304,8 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) const bool disconnected = client.waitForDisconnected(WaitTimeoutMSec); return writeOk && disconnected; } + +bool Application::isDarkTheme() const +{ + return m_darkTheme; +} diff --git a/src/gui/Application.h b/src/gui/Application.h index 9a3ef756b7..b39fbe0e97 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -41,6 +41,7 @@ class Application : public QApplication bool event(QEvent* event) override; bool isAlreadyRunning() const; + bool isDarkTheme() const; bool sendFileNamesToRunningInstance(const QStringList& fileNames); @@ -68,6 +69,7 @@ private slots: static int unixSignalSocket[2]; #endif bool m_alreadyRunning; + bool m_darkTheme = false; QLockFile* m_lockFile; QLocalServer m_lockServer; QString m_socketName; @@ -76,4 +78,6 @@ private slots: #endif }; +#define kpxcApp qobject_cast<Application*>(Application::instance()) + #endif // KEEPASSX_APPLICATION_H diff --git a/src/main.cpp b/src/main.cpp index 524f112b3d..c3494f02fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,9 +29,6 @@ #include "gui/Application.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" -#include "gui/osutils/OSUtils.h" -#include "gui/styles/dark/DarkStyle.h" -#include "gui/styles/light/LightStyle.h" #if defined(WITH_ASAN) && defined(WITH_LSAN) #include <sanitizer/lsan_interface.h> @@ -64,19 +61,6 @@ int main(int argc, char** argv) Application::setApplicationName("KeePassXC"); Application::setApplicationVersion(KEEPASSXC_VERSION); - QString appTheme = config()->get("GUI/ApplicationTheme").toString(); - if (appTheme == "auto") { - if (osUtils->isDarkMode()) { - QApplication::setStyle(new DarkStyle); - } else { - QApplication::setStyle(new LightStyle); - } - } else if (appTheme == "light") { - QApplication::setStyle(new LightStyle); - } else if (appTheme == "dark") { - QApplication::setStyle(new DarkStyle); - } - // don't set organizationName as that changes the return value of // QStandardPaths::writableLocation(QDesktopServices::DataLocation) Bootstrap::bootstrapApplication(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 8ce9b05879..76766c8dc2 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -74,16 +74,27 @@ #include "keys/FileKey.h" #include "keys/PasswordKey.h" -QTEST_MAIN(TestGui) +int main(int argc, char* argv[]) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + Application app(argc, argv); + app.setApplicationName("KeePassXC"); + app.setApplicationVersion(KEEPASSXC_VERSION); + app.setQuitOnLastWindowClosed(false); + app.setAttribute(Qt::AA_Use96Dpi, true); + QTEST_DISABLE_KEYPAD_NAVIGATION + TestGui tc; + QTEST_SET_MAIN_SOURCE_PATH + return QTest::qExec(&tc, argc, argv); +} static QString dbFileName = QStringLiteral(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"); void TestGui::initTestCase() { - Application::setApplicationName("KeePassXC"); - Application::setApplicationVersion(KEEPASSXC_VERSION); - QApplication::setQuitOnLastWindowClosed(false); - QVERIFY(Crypto::init()); Config::createTempFileInstance(); // Disable autosave so we can test the modified file indicator diff --git a/tests/gui/TestGuiBrowser.cpp b/tests/gui/TestGuiBrowser.cpp index 834aea581c..1750caa804 100644 --- a/tests/gui/TestGuiBrowser.cpp +++ b/tests/gui/TestGuiBrowser.cpp @@ -47,7 +47,22 @@ #include "gui/entry/EditEntryWidget.h" #include "gui/entry/EntryView.h" -QTEST_MAIN(TestGuiBrowser) +int main(int argc, char* argv[]) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + Application app(argc, argv); + app.setApplicationName("KeePassXC"); + app.setApplicationVersion(KEEPASSXC_VERSION); + app.setQuitOnLastWindowClosed(false); + app.setAttribute(Qt::AA_Use96Dpi, true); + QTEST_DISABLE_KEYPAD_NAVIGATION + TestGuiBrowser tc; + QTEST_SET_MAIN_SOURCE_PATH + return QTest::qExec(&tc, argc, argv); +} void TestGuiBrowser::initTestCase() { From fe1189ea7973466e929810a42b247c284cd26f42 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Sun, 8 Mar 2020 22:22:01 -0400 Subject: [PATCH 079/215] Enhance Password Editing Fields * Remove repeat password on entry edit * Move show/hide password and password generator buttons into the field as actions. * Register keyboard shortcut Ctrl+H to toggle password visibility * Register keyboard shortcut Ctrl+G to open the password generator * Cleanup code and improve interactions between elements * Simplify Password Generator button layout; convert advanced mode button to toggle button * Update GUI tests * Fixes #4120 --- COPYING | 2 + .../scalable/actions/clipboard-text.svg | 1 + .../application/scalable/actions/refresh.svg | 1 + src/gui/DatabaseOpenWidget.cpp | 6 +- src/gui/DatabaseOpenWidget.ui | 43 +- src/gui/MainWindow.cpp | 2 +- src/gui/MainWindow.ui | 22 + src/gui/PasswordEdit.cpp | 132 +- src/gui/PasswordEdit.h | 21 +- src/gui/PasswordGeneratorWidget.cpp | 236 +- src/gui/PasswordGeneratorWidget.h | 15 +- src/gui/PasswordGeneratorWidget.ui | 1986 ++++++++--------- src/gui/entry/EditEntryWidget.cpp | 46 +- src/gui/entry/EditEntryWidget.h | 2 +- src/gui/entry/EditEntryWidgetMain.ui | 212 +- src/gui/masterkey/PasswordEditWidget.cpp | 36 +- src/gui/masterkey/PasswordEditWidget.h | 1 - src/gui/masterkey/PasswordEditWidget.ui | 87 +- .../wizard/NewDatabaseWizardPageMasterKey.cpp | 2 +- tests/gui/TestGui.cpp | 28 +- 20 files changed, 1358 insertions(+), 1523 deletions(-) create mode 100644 share/icons/application/scalable/actions/clipboard-text.svg create mode 100644 share/icons/application/scalable/actions/refresh.svg diff --git a/COPYING b/COPYING index 9202a00235..f06c6b225e 100644 --- a/COPYING +++ b/COPYING @@ -180,6 +180,8 @@ Files: share/icons/application/scalable/categories/preferences-other.svg share/icons/application/scalable/actions/favicon-download.svg share/icons/application/scalable/actions/document-open.svg share/icons/application/scalable/actions/document-save-as.svg + share/icons/application/scalable/actions/refresh.svg + share/icons/application/scalable/actions/clipboard-text.svg Copyright: 2019 Austin Andrews <http://templarian.com/> License: SIL OPEN FONT LICENSE Version 1.1 Comment: Taken from Material Design icon set (https://github.com/templarian/MaterialDesign/) diff --git a/share/icons/application/scalable/actions/clipboard-text.svg b/share/icons/application/scalable/actions/clipboard-text.svg new file mode 100644 index 0000000000..88e025e02f --- /dev/null +++ b/share/icons/application/scalable/actions/clipboard-text.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,3H14.82C14.25,1.44 12.53,0.64 11,1.2C10.14,1.5 9.5,2.16 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M7,7H17V5H19V19H5V5H7V7M17,11H7V9H17V11M15,15H7V13H15V15Z" /></svg> \ No newline at end of file diff --git a/share/icons/application/scalable/actions/refresh.svg b/share/icons/application/scalable/actions/refresh.svg new file mode 100644 index 0000000000..ebe3f16e79 --- /dev/null +++ b/share/icons/application/scalable/actions/refresh.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" /></svg> \ No newline at end of file diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index f3bb502d04..c58b2df40d 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -56,9 +56,6 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) 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())); @@ -166,10 +163,10 @@ void DatabaseOpenWidget::clearForms() { if (!m_isOpeningDatabase) { m_ui->editPassword->setText(""); + m_ui->editPassword->setShowPassword(false); m_ui->comboKeyFile->clear(); m_ui->comboKeyFile->setEditText(""); m_ui->checkTouchID->setChecked(false); - m_ui->buttonTogglePassword->setChecked(false); m_db.reset(); } } @@ -195,7 +192,6 @@ void DatabaseOpenWidget::openDatabase() } m_ui->editPassword->setShowPassword(false); - m_ui->buttonTogglePassword->setChecked(false); QCoreApplication::processEvents(); m_isOpeningDatabase = true; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index f2cd96b6aa..f89e30ffde 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>580</width> + <height>410</height> + </rect> + </property> <property name="accessibleName"> <string>Unlock KeePassXC Database</string> </property> @@ -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"> @@ -612,8 +603,6 @@ </customwidget> </customwidgets> <tabstops> - <tabstop>editPassword</tabstop> - <tabstop>buttonTogglePassword</tabstop> <tabstop>comboKeyFile</tabstop> <tabstop>buttonBrowseFile</tabstop> <tabstop>hardwareKeyLabelHelp</tabstop> diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 3db3e98f6f..a25213980c 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -451,7 +451,7 @@ MainWindow::MainWindow() 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->passwordGeneratorWidget, SIGNAL(closePasswordGenerator()), SLOT(closePasswordGen())); connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 41792986b5..c68d18b836 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -186,6 +186,15 @@ </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"> @@ -193,6 +202,19 @@ </property> </widget> </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> </widget> diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index 37b82ad8b6..0cc72a9957 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -20,14 +20,24 @@ #include "core/Config.h" #include "core/FilePath.h" +#include "gui/Application.h" #include "gui/Font.h" +#include "gui/PasswordGeneratorWidget.h" -const QColor PasswordEdit::CorrectSoFarColor = QColor(255, 205, 15); -const QColor PasswordEdit::ErrorColor = QColor(255, 125, 125); +#include <QDialog> +#include <QVBoxLayout> + +namespace +{ + const QColor CorrectSoFarColor(255, 205, 15); + const QColor CorrectSoFarColorDark(115, 104, 46); + const QColor ErrorColor(255, 125, 125); + const QColor ErrorColorDark(128, 45, 45); + +} // namespace PasswordEdit::PasswordEdit(QWidget* parent) : QLineEdit(parent) - , m_basePasswordEdit(nullptr) { const QIcon errorIcon = filePath()->icon("status", "dialog-error"); m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition); @@ -40,70 +50,122 @@ PasswordEdit::PasswordEdit(QWidget* parent) 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( + filePath()->icon("actions", "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( + filePath()->icon("actions", "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); } -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())); +} + +void PasswordEdit::setParentPasswordEdit(PasswordEdit* parent) +{ + m_parentPasswordEdit = parent; + // Hide actions + m_toggleVisibleAction->setVisible(false); + m_passwordGeneratorAction->setVisible(false); +} - 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::enablePasswordGenerator(bool signalOnly) +{ + disconnect(m_passwordGeneratorAction); + m_passwordGeneratorAction->setVisible(true); - connect(m_basePasswordEdit, SIGNAL(showPasswordChanged(bool)), SLOT(setShowPassword(bool))); + if (signalOnly) { + connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::togglePasswordGenerator); + } else { + 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) { + m_toggleVisibleAction->setIcon(filePath()->icon("actions", 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("security/passwordsrepeat").toBool()) { - setEnabled(!show); - setReadOnly(show); - setText(m_basePasswordEdit->text()); + 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 pwGenerator = new PasswordGeneratorWidget(); + QDialog pwDialog(this); + pwDialog.setWindowTitle(tr("Generate Password")); + auto layout = new QVBoxLayout(); + pwDialog.setLayout(layout); + layout->addWidget(pwGenerator); + + pwGenerator->setStandaloneMode(false); + pwGenerator->setPasswordVisible(isPasswordVisible()); + + connect(pwGenerator, SIGNAL(closePasswordGenerator()), &pwDialog, SLOT(close())); + connect(pwGenerator, SIGNAL(appliedPassword(QString)), SLOT(setText(QString))); + if (m_repeatPasswordEdit) { + connect(pwGenerator, SIGNAL(appliedPassword(QString)), m_repeatPasswordEdit, SLOT(setText(QString))); + } + + pwDialog.exec(); } -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; + QColor color = kpxcApp->isDarkTheme() ? ErrorColorDark : ErrorColor; + if (!password.isEmpty() && otherPassword.startsWith(password)) { + color = kpxcApp->isDarkTheme() ? CorrectSoFarColorDark : CorrectSoFarColor; + isCorrect = true; } + setStyleSheet(stylesheetTemplate.arg(color.name())); m_correctAction->setVisible(isCorrect); m_errorAction->setVisible(!isCorrect); } else { diff --git a/src/gui/PasswordEdit.h b/src/gui/PasswordEdit.h index b6e74ed00d..365f4cdb10 100644 --- a/src/gui/PasswordEdit.h +++ b/src/gui/PasswordEdit.h @@ -28,29 +28,32 @@ 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(bool signalOnly = false); + void setRepeatPartner(PasswordEdit* repeatEdit); bool isPasswordVisible() const; public slots: void setShowPassword(bool show); + void updateRepeatStatus(); signals: - void showPasswordChanged(bool show); + void togglePasswordGenerator(); private slots: - void updateStylesheet(); void autocompletePassword(const QString& password); + void popupPasswordGenerator(); + void setParentPasswordEdit(PasswordEdit* parent); 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<PasswordEdit> m_repeatPasswordEdit; + QPointer<PasswordEdit> m_parentPasswordEdit; + bool m_sendGeneratorSignal = false; + bool m_isRepeatPartner = false; }; #endif // KEEPASSX_PASSWORDEDIT_H diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 7d6f05d417..bfdbfe304b 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -27,36 +27,38 @@ #include "core/FilePath.h" #include "core/PasswordGenerator.h" #include "core/PasswordHealth.h" +#include "gui/Application.h" #include "gui/Clipboard.h" -#include "gui/osutils/OSUtils.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(filePath()->icon("actions", "refresh")); + m_ui->buttonGenerate->setToolTip( + tr("Regenerate password (%1)").arg(m_ui->buttonGenerate->shortcut().toString(QKeySequence::NativeText))); + m_ui->buttonCopy->setIcon(filePath()->icon("actions", "clipboard-text")); + m_ui->buttonClose->setShortcut(Qt::Key_Escape); 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->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(closePasswordGenerator())); - 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())); @@ -100,12 +102,6 @@ PasswordGeneratorWidget::~PasswordGeneratorWidget() { } -void PasswordGeneratorWidget::showEvent(QShowEvent* event) -{ - QWidget::showEvent(event); - reset(); -} - void PasswordGeneratorWidget::loadSettings() { // Password config @@ -118,19 +114,13 @@ void PasswordGeneratorWidget::loadSettings() 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->buttonAdvancedMode->setChecked( + config()->get("generator/AdvancedMode", PasswordGenerator::DefaultAdvancedMode).toBool()); + setAdvancedMode(m_ui->buttonAdvancedMode->isChecked()); + 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( @@ -174,7 +164,7 @@ void PasswordGeneratorWidget::saveSettings() config()->set("generator/Numbers", m_ui->checkBoxNumbersAdv->isChecked()); config()->set("generator/EASCII", m_ui->checkBoxExtASCIIAdv->isChecked()); } - config()->set("generator/AdvancedMode", m_ui->advancedBar->isVisible()); + config()->set("generator/AdvancedMode", m_ui->buttonAdvancedMode->isChecked()); config()->set("generator/SpecialChars", m_ui->checkBoxSpecialChars->isChecked()); config()->set("generator/Braces", m_ui->checkBoxBraces->isChecked()); config()->set("generator/Punctuation", m_ui->checkBoxPunctuation->isChecked()); @@ -215,10 +205,10 @@ 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); } } @@ -230,7 +220,7 @@ QString PasswordGeneratorWidget::getGeneratedPassword() void PasswordGeneratorWidget::keyPressEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Escape && m_standalone) { - emit dialogTerminated(); + emit closePasswordGenerator(); } else { e->ignore(); } @@ -280,7 +270,7 @@ void PasswordGeneratorWidget::applyPassword() { saveSettings(); emit appliedPassword(m_ui->editNewPassword->text()); - emit dialogTerminated(); + emit closePasswordGenerator(); } void PasswordGeneratorWidget::copyPassword() @@ -288,43 +278,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(); } @@ -332,49 +309,46 @@ 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(); -} - -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(); + if (state) { + 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(); + } else { + 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(); + } } void PasswordGeneratorWidget::excludeHexChars() @@ -392,7 +366,7 @@ void PasswordGeneratorWidget::colorStrengthIndicator(const PasswordHealth& healt // Set the color and background based on entropy QList<QString> qualityColors; - if (osUtils->isDarkMode()) { + if (kpxcApp->isDarkTheme()) { qualityColors << QStringLiteral("#C43F31") << QStringLiteral("#DB9837") << QStringLiteral("#608A22") << QStringLiteral("#1F8023"); } else { @@ -496,12 +470,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; @@ -510,62 +486,52 @@ 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->setExcludedChars(m_ui->editExcludedChars->text()); + } else { + m_passwordGenerator->setExcludedChars(""); } - m_passwordGenerator->setFlags(flags); if (m_passwordGenerator->isValid()) { m_ui->buttonGenerate->setEnabled(true); @@ -573,21 +539,9 @@ void PasswordGeneratorWidget::updateGenerator() 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_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()); diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index eba7f815f6..7e9390a9f5 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -54,9 +54,6 @@ class PasswordGeneratorWidget : public QWidget QString getGeneratedPassword(); bool isPasswordVisible() const; -protected: - void showEvent(QShowEvent* event) override; - public slots: void regeneratePassword(); void applyPassword(); @@ -65,25 +62,21 @@ public slots: signals: void appliedPassword(const QString& password); - void dialogTerminated(); + void closePasswordGenerator(); 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 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(); diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index 38af84b75c..62d96a2a1d 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>404</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,1037 @@ 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>350</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="2"> - <widget class="QToolButton" name="togglePasswordButton"> - <property name="accessibleName"> - <string>Toggle password visibility</string> + <widget class="QPushButton" name="buttonCopy"> + <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> + <item row="0" column="1"> + <widget class="QPushButton" name="buttonGenerate"> + <property name="accessibleDescription"> + <string>Regenerate password</string> + </property> + <property name="shortcut"> + <string notr="true">Ctrl+R</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> - <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">/*_& ...</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>" '</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><*+!?=</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>#$%&&@^`~</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 "do not include" 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: "0", "1", "l", "I", "O", "|", "﹒"</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> - </item> - <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>6</number> - </property> - <item> - <widget class="QLabel" name="labelLength"> - <property name="text"> - <string>&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="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="1" column="0"> + <widget class="QGroupBox" name="groupBox"> + <property name="minimumSize"> + <size> + <width>580</width> + <height>0</height> + </size> + </property> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </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="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="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> + <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 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="2" column="0" alignment="Qt::AlignRight"> - <widget class="QLabel" name="labelWordSeparator"> + <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>Word Separator:</string> + <string notr="true">/*_& ...</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="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>Wordlist:</string> + <string>ExtendedASCII</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> + <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"> + </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="QSlider" name="sliderWordCount"> - <property name="minimum"> - <number>1</number> - </property> - <property name="maximum"> - <number>40</number> - </property> - <property name="value"> - <number>6</number> + <widget class="QToolButton" name="checkBoxUpperAdv"> + <property name="minimumSize"> + <size> + <width>40</width> + <height>25</height> + </size> </property> - <property name="sliderPosition"> - <number>6</number> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> </property> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <property name="toolTip"> + <string>Upper-case letters</string> </property> - <property name="tickPosition"> - <enum>QSlider::TicksBelow</enum> + <property name="accessibleName"> + <string>Upper-case letters</string> </property> - <property name="tickInterval"> - <number>8</number> + <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="QSpinBox" name="spinBoxWordCount"> - <property name="minimum"> - <number>1</number> + <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="maximum"> - <number>100</number> + <property name="text"> + <string>a-z</string> </property> - <property name="value"> - <number>6</number> + <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" alignment="Qt::AlignRight"> - <widget class="QLabel" name="labelWordCount"> - <property name="text"> - <string>Word Count:</string> - </property> - <property name="buddy"> - <cstring>spinBoxLength</cstring> + <item> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="sizeConstraint"> + <enum>QLayout::SetMinimumSize</enum> </property> - </widget> + <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 row="2" column="1"> - <widget class="QLineEdit" name="editWordSeparator"> - <property name="text"> - <string/> + <item> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <property name="sizeConstraint"> + <enum>QLayout::SetMinimumSize</enum> </property> - </widget> + <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>" '</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">optionButtons</string> + </attribute> + </widget> + </item> + </layout> </item> - <item row="3" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_6"> + <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><*+!?=</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">optionButtons</string> + </attribute> + </widget> + </item> <item> - <widget class="QComboBox" name="wordCaseComboBox"/> + <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> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <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="sizeHint" stdset="0"> + <property name="toolTip"> + <string>Logograms</string> + </property> + <property name="accessibleName"> + <string>Logograms</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="checkBoxExtASCIIAdv"> + <property name="minimumSize"> <size> - <width>40</width> - <height>20</height> + <width>105</width> + <height>25</height> </size> </property> - </spacer> + <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> - </item> - </layout> - </item> - </layout> - </widget> - </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="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> + <item> + <widget class="QLabel" name="labelExcludedChars"> + <property name="text"> + <string>Do not include:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="editExcludedChars"> + <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="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> + <widget class="QPushButton" name="buttonAddHex"> + <property name="focusPolicy"> + <enum>Qt::TabFocus</enum> + </property> + <property name="toolTip"> + <string>Add non-hex letters to "do not include" list</string> + </property> + <property name="accessibleName"> + <string>Hex Passwords</string> + </property> + <property name="text"> + <string>Hex</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QCheckBox" name="checkBoxExcludeAlike"> + <property name="toolTip"> + <string>Excluded characters: "0", "1", "l", "I", "O", "|", "﹒"</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> - <item> - <widget class="QPushButton" name="buttonCopy"> - <property name="toolTip"> - <string>Copy password</string> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="passwordLengthSliderLayout"> + <property name="spacing"> + <number>15</number> </property> - <property name="accessibleDescription"> - <string/> + <property name="sizeConstraint"> + <enum>QLayout::SetMinimumSize</enum> </property> - <property name="text"> - <string>Copy</string> + <property name="topMargin"> + <number>0</number> </property> - </widget> + <item> + <widget class="QLabel" name="labelLength"> + <property name="text"> + <string>&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> + <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> - <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> + </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> </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> + </widget> + </item> + <item> + <widget class="QPushButton" name="buttonApply"> + <property name="text"> + <string>Apply Password</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> @@ -1219,16 +1137,17 @@ 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>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> @@ -1243,13 +1162,12 @@ QProgressBar::chunk { <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/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 7b1e85472f..948e8456e4 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -148,8 +148,6 @@ 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->setDisabled(true); @@ -157,8 +155,6 @@ void EditEntryWidget::setupMain() 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))); @@ -166,8 +162,9 @@ void EditEntryWidget::setupMain() #endif connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); 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))); + connect(m_mainUi->passwordGenerator, SIGNAL(closePasswordGenerator()), SLOT(togglePasswordGenerator())); m_mainUi->expirePresets->setMenu(createPresetsMenu()); connect(m_mainUi->expirePresets->menu(), SIGNAL(triggered(QAction*)), this, SLOT(useExpiryPreset(QAction*))); @@ -417,7 +414,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))); @@ -809,7 +805,6 @@ 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()); @@ -821,8 +816,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) } else { m_mainUi->notesEdit->setFont(Font::defaultFont()); } - m_mainUi->togglePasswordGeneratorButton->setChecked(false); - m_mainUi->togglePasswordGeneratorButton->setDisabled(m_history); + m_mainUi->passwordGenerator->setVisible(false); m_mainUi->passwordGenerator->reset(entry->password().length()); m_advancedUi->attachmentsWidget->setReadOnly(m_history); @@ -849,11 +843,14 @@ 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("security/passwordscleartext").toBool()); + if (!m_history) { + m_mainUi->passwordEdit->enablePasswordGenerator(true); + } 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); @@ -973,13 +970,8 @@ 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() + if (m_mainUi->passwordGenerator->isVisible() && m_mainUi->passwordGenerator->getGeneratedPassword() != m_mainUi->passwordEdit->text()) { auto answer = MessageBox::question(this, tr("Apply generated password?"), @@ -992,7 +984,7 @@ bool EditEntryWidget::commitEntry() } // Hide the password generator - m_mainUi->togglePasswordGeneratorButton->setChecked(false); + m_mainUi->passwordGenerator->setVisible(false); if (m_advancedUi->attributesView->currentIndex().isValid() && m_advancedUi->attributesEdit->isEnabled()) { QString key = m_attributesModel->keyByIndex(m_advancedUi->attributesView->currentIndex()); @@ -1139,7 +1131,6 @@ void EditEntryWidget::clear() m_mainUi->titleEdit->setText(""); m_mainUi->passwordEdit->setText(""); - m_mainUi->passwordRepeatEdit->setText(""); m_mainUi->urlEdit->setText(""); m_mainUi->notesEdit->clear(); @@ -1151,25 +1142,20 @@ void EditEntryWidget::clear() hideMessage(); } -void EditEntryWidget::togglePasswordGeneratorButton(bool checked) +void EditEntryWidget::togglePasswordGenerator() { - if (checked) { + bool visible = m_mainUi->passwordGenerator->isVisible(); + if (!visible) { m_mainUi->passwordGenerator->regeneratePassword(); + m_mainUi->passwordGenerator->setPasswordVisible(m_mainUi->passwordEdit->isPasswordVisible()); } - m_mainUi->passwordGenerator->setVisible(checked); -} - -bool EditEntryWidget::passwordsEqual() -{ - return m_mainUi->passwordEdit->text() == m_mainUi->passwordRepeatEdit->text(); + m_mainUi->passwordGenerator->setVisible(!visible); } void EditEntryWidget::setGeneratedPassword(const QString& password) { m_mainUi->passwordEdit->setText(password); - m_mainUi->passwordRepeatEdit->setText(password); - - m_mainUi->togglePasswordGeneratorButton->setChecked(false); + m_mainUi->passwordGenerator->setVisible(false); } #ifdef WITH_XC_NETWORKING diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index dafc4a2830..be994b88c5 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -83,7 +83,7 @@ private slots: void acceptEntry(); bool commitEntry(); void cancel(); - void togglePasswordGeneratorButton(bool checked); + void togglePasswordGenerator(); void setGeneratedPassword(const QString& password); #ifdef WITH_XC_NETWORKING void updateFaviconButtonEnable(const QString& url); diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index 54140fcd9f..2be52a28e7 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -20,122 +20,16 @@ <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"> - <item> - <widget class="URLEdit" name="urlEdit"> - <property name="accessibleName"> - <string>Url field</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="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> - </property> - </widget> - </item> - <item row="3" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_4"> - <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"> - <property name="toolTip"> - <string>Toggle password generator</string> - </property> - <property name="accessibleName"> - <string>Toggle password generator</string> - </property> - <property name="checkable"> - <bool>true</bool> - </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> - </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="togglePasswordButton"> - <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> - </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"> + <item row="7" column="0" alignment="Qt::AlignRight"> + <widget class="QCheckBox" name="expireCheck"> <property name="toolTip"> - <string>Toggle notes visible</string> + <string>Toggle expiration</string> </property> <property name="accessibleName"> - <string>Toggle notes visible</string> + <string>Toggle expiration</string> </property> <property name="text"> - <string>Notes</string> + <string>Expires</string> </property> </widget> </item> @@ -194,10 +88,48 @@ </property> </widget> </item> + <item row="1" column="1"> + <widget class="QComboBox" name="usernameComboBox"> + <property name="accessibleName"> + <string>Username field</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="titleEdit"> + <property name="accessibleName"> + <string>Title field</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item> + <widget class="URLEdit" name="urlEdit"> + <property name="accessibleName"> + <string>Url field</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="9" column="1"> <widget class="QLabel" name="notesHint"> <property name="visible"> - <bool>false</bool> + <bool>true</bool> </property> <property name="text"> <string>Toggle the checkbox to reveal the notes section.</string> @@ -207,6 +139,13 @@ </property> </widget> </item> + <item row="5" column="0" alignment="Qt::AlignRight"> + <widget class="QLabel" name="urlLabel"> + <property name="text"> + <string>URL:</string> + </property> + </widget> + </item> <item row="1" column="0" alignment="Qt::AlignRight"> <widget class="QLabel" name="usernameLabel"> <property name="text"> @@ -214,34 +153,50 @@ </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="titleEdit"> + <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>Title field</string> + <string>Toggle notes visible</string> + </property> + <property name="text"> + <string>Notes</string> </property> </widget> </item> - <item row="1" column="1"> - <widget class="QComboBox" name="usernameComboBox"> - <property name="accessibleName"> - <string>Username field</string> + <item row="2" column="0" alignment="Qt::AlignRight"> + <widget class="QLabel" name="passwordLabel"> + <property name="text"> + <string>Password:</string> </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> </layout> + <widget class="QLabel" name="titleLabel"> + <property name="geometry"> + <rect> + <x>35</x> + <y>0</y> + <width>23</width> + <height>19</height> + </rect> + </property> + <property name="text"> + <string>Title:</string> + </property> + </widget> </widget> <customwidgets> <customwidget> @@ -267,9 +222,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/masterkey/PasswordEditWidget.cpp b/src/gui/masterkey/PasswordEditWidget.cpp index 96b5fd3056..60689e9204 100644 --- a/src/gui/masterkey/PasswordEditWidget.cpp +++ b/src/gui/masterkey/PasswordEditWidget.cpp @@ -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")); - 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/masterkey/PasswordEditWidget.h index 57c225c1fb..8022194511 100644 --- a/src/gui/masterkey/PasswordEditWidget.h +++ b/src/gui/masterkey/PasswordEditWidget.h @@ -47,7 +47,6 @@ class PasswordEditWidget : public KeyComponentWidget 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/masterkey/PasswordEditWidget.ui index d0a85eb59c..d8382ed94b 100644 --- a/src/gui/masterkey/PasswordEditWidget.ui +++ b/src/gui/masterkey/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/wizard/NewDatabaseWizardPageMasterKey.cpp b/src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp index 130560e27f..71bd5e60f3 100644 --- a/src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp +++ b/src/gui/wizard/NewDatabaseWizardPageMasterKey.cpp @@ -37,5 +37,5 @@ NewDatabaseWizardPageMasterKey::~NewDatabaseWizardPageMasterKey() void NewDatabaseWizardPageMasterKey::updateWindowSize() { // ugly workaround for QWizard not managing to react to size changes automatically - QApplication::activeWindow()->adjustSize(); + window()->adjustSize(); } diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 76766c8dc2..dd69e30427 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -471,17 +471,6 @@ void TestGui::testEditEntry() QCOMPARE(attrTextEdit->toPlainText(), attrText); editEntryWidget->setCurrentPage(0); - // Test mismatch passwords - auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit"); - QString originalPassword = passwordEdit->text(); - passwordEdit->setText("newpass"); - QTest::mouseClick(okButton, Qt::LeftButton); - auto* messageWiget = editEntryWidget->findChild<MessageWidget*>("messageWidget"); - QTRY_VERIFY(messageWiget->isVisible()); - QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode); - QCOMPARE(passwordEdit->text(), QString("newpass")); - passwordEdit->setText(originalPassword); - // Save the edit (press OK) QTest::mouseClick(okButton, Qt::LeftButton); QApplication::processEvents(); @@ -506,7 +495,6 @@ void TestGui::testEditEntry() titleEdit->setText("multiline\ntitle"); editEntryWidget->findChild<QComboBox*>("usernameComboBox")->lineEdit()->setText("multiline\nusername"); editEntryWidget->findChild<QLineEdit*>("passwordEdit")->setText("multiline\npassword"); - editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit")->setText("multiline\npassword"); editEntryWidget->findChild<QLineEdit*>("urlEdit")->setText("multiline\nurl"); QTest::mouseClick(okButton, Qt::LeftButton); @@ -615,9 +603,7 @@ void TestGui::testAddEntry() QTest::keyClicks(usernameComboBox, "Auto"); QTest::keyPress(usernameComboBox, Qt::Key_Right); auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit"); - auto* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit"); QTest::keyClicks(passwordEdit, "something 2"); - QTest::keyClicks(passwordRepeatEdit, "something 2"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); @@ -663,8 +649,10 @@ void TestGui::testPasswordEntryEntropy() QTest::keyClicks(titleEdit, "test"); // Open the password generator - auto* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton"); - QTest::mouseClick(generatorButton, Qt::LeftButton); + auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>(); + QVERIFY(passwordEdit); + QTest::mouseClick(passwordEdit, Qt::LeftButton); + QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier); // Type in some password auto* editNewPassword = editEntryWidget->findChild<QLineEdit*>("editNewPassword"); @@ -735,8 +723,10 @@ void TestGui::testDicewareEntryEntropy() QTest::keyClicks(titleEdit, "test"); // Open the password generator - auto* generatorButton = editEntryWidget->findChild<QToolButton*>("togglePasswordGeneratorButton"); - QTest::mouseClick(generatorButton, Qt::LeftButton); + auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>(); + QVERIFY(passwordEdit); + QTest::mouseClick(passwordEdit, Qt::LeftButton); + QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier); // Select Diceware auto* tabWidget = editEntryWidget->findChild<QTabWidget*>("tabWidget"); @@ -1440,7 +1430,6 @@ int TestGui::addCannedEntries() auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget"); auto* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit"); auto* passwordEdit = editEntryWidget->findChild<QLineEdit*>("passwordEdit"); - auto* passwordRepeatEdit = editEntryWidget->findChild<QLineEdit*>("passwordRepeatEdit"); // Add entry "test" and confirm added QTest::mouseClick(entryNewWidget, Qt::LeftButton); @@ -1453,7 +1442,6 @@ int TestGui::addCannedEntries() QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 2"); QTest::keyClicks(passwordEdit, "something 2"); - QTest::keyClicks(passwordRepeatEdit, "something 2"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); ++entries_added; From fb5173cebd98b4417966e8eeaa30f32b061923d5 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Tue, 25 Feb 2020 18:02:46 -0500 Subject: [PATCH 080/215] Remove inline password generator when editing entries * Always use a pop-up generator to avoid cluttering the user interface and making it clear that a password is being created --- src/gui/PasswordEdit.cpp | 11 +- src/gui/PasswordEdit.h | 7 +- src/gui/PasswordGeneratorWidget.cpp | 7 +- src/gui/PasswordGeneratorWidget.h | 2 +- src/gui/entry/EditEntryWidget.cpp | 45 +---- src/gui/entry/EditEntryWidget.h | 2 - src/gui/entry/EditEntryWidgetMain.ui | 191 +++++++++--------- .../group/EditGroupWidgetKeeShare.cpp | 34 +--- src/keeshare/group/EditGroupWidgetKeeShare.h | 2 - src/keeshare/group/EditGroupWidgetKeeShare.ui | 128 ++++++------ tests/gui/TestGui.cpp | 72 ++++--- tests/gui/TestGui.h | 2 + 12 files changed, 221 insertions(+), 282 deletions(-) diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index 0cc72a9957..9474adb16e 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -94,14 +94,10 @@ void PasswordEdit::setParentPasswordEdit(PasswordEdit* parent) m_passwordGeneratorAction->setVisible(false); } -void PasswordEdit::enablePasswordGenerator(bool signalOnly) +void PasswordEdit::enablePasswordGenerator() { - disconnect(m_passwordGeneratorAction); - m_passwordGeneratorAction->setVisible(true); - - if (signalOnly) { - connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::togglePasswordGenerator); - } else { + if (!m_passwordGeneratorAction->isVisible()) { + m_passwordGeneratorAction->setVisible(true); connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::popupPasswordGenerator); } } @@ -139,6 +135,7 @@ void PasswordEdit::popupPasswordGenerator() pwGenerator->setStandaloneMode(false); pwGenerator->setPasswordVisible(isPasswordVisible()); + pwGenerator->setPasswordLength(text().length()); connect(pwGenerator, SIGNAL(closePasswordGenerator()), &pwDialog, SLOT(close())); connect(pwGenerator, SIGNAL(appliedPassword(QString)), SLOT(setText(QString))); diff --git a/src/gui/PasswordEdit.h b/src/gui/PasswordEdit.h index 365f4cdb10..3ebc5d9753 100644 --- a/src/gui/PasswordEdit.h +++ b/src/gui/PasswordEdit.h @@ -23,13 +23,15 @@ #include <QLineEdit> #include <QPointer> +class QDialog; + class PasswordEdit : public QLineEdit { Q_OBJECT public: explicit PasswordEdit(QWidget* parent = nullptr); - void enablePasswordGenerator(bool signalOnly = false); + void enablePasswordGenerator(); void setRepeatPartner(PasswordEdit* repeatEdit); bool isPasswordVisible() const; @@ -37,9 +39,6 @@ public slots: void setShowPassword(bool show); void updateRepeatStatus(); -signals: - void togglePasswordGenerator(); - private slots: void autocompletePassword(const QString& password); void popupPasswordGenerator(); diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index bfdbfe304b..4361248131 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -187,18 +187,13 @@ void PasswordGeneratorWidget::saveSettings() config()->set("generator/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()); } - - setStandaloneMode(false); - setPasswordVisible(config()->get("security/passwordscleartext").toBool()); - updateGenerator(); } void PasswordGeneratorWidget::setStandaloneMode(bool standalone) diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index 7e9390a9f5..fe3f9f67fa 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -49,7 +49,7 @@ class PasswordGeneratorWidget : public QWidget ~PasswordGeneratorWidget(); void loadSettings(); void saveSettings(); - void reset(int length = 0); + void setPasswordLength(int length); void setStandaloneMode(bool standalone); QString getGeneratedPassword(); bool isPasswordVisible() const; diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 948e8456e4..201c9628a5 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -128,8 +128,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()); } @@ -163,14 +161,8 @@ void EditEntryWidget::setupMain() connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool))); - connect(m_mainUi->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); - connect(m_mainUi->passwordGenerator, SIGNAL(closePasswordGenerator()), SLOT(togglePasswordGenerator())); - 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() @@ -816,8 +808,6 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) } else { m_mainUi->notesEdit->setFont(Font::defaultFont()); } - m_mainUi->passwordGenerator->setVisible(false); - m_mainUi->passwordGenerator->reset(entry->password().length()); m_advancedUi->attachmentsWidget->setReadOnly(m_history); m_advancedUi->addAttributeButton->setEnabled(!m_history); @@ -845,13 +835,12 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) m_mainUi->passwordEdit->setText(entry->password()); m_mainUi->passwordEdit->setShowPassword(config()->get("security/passwordscleartext").toBool()); if (!m_history) { - m_mainUi->passwordEdit->enablePasswordGenerator(true); + 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); - QList<QString> commonUsernames = m_db->commonUsernames(); m_usernameCompleterModel->setStringList(commonUsernames); QString usernameToRestore = m_mainUi->usernameComboBox->lineEdit()->text(); @@ -970,22 +959,6 @@ bool EditEntryWidget::commitEntry() return true; } - // Ask the user to apply the generator password, if open - if (m_mainUi->passwordGenerator->isVisible() - && 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->passwordGenerator->setVisible(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)); @@ -1142,22 +1115,6 @@ void EditEntryWidget::clear() hideMessage(); } -void EditEntryWidget::togglePasswordGenerator() -{ - bool visible = m_mainUi->passwordGenerator->isVisible(); - if (!visible) { - m_mainUi->passwordGenerator->regeneratePassword(); - m_mainUi->passwordGenerator->setPasswordVisible(m_mainUi->passwordEdit->isPasswordVisible()); - } - m_mainUi->passwordGenerator->setVisible(!visible); -} - -void EditEntryWidget::setGeneratedPassword(const QString& password) -{ - m_mainUi->passwordEdit->setText(password); - m_mainUi->passwordGenerator->setVisible(false); -} - #ifdef WITH_XC_NETWORKING void EditEntryWidget::updateFaviconButtonEnable(const QString& url) { diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index be994b88c5..e0a67b5eac 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -83,8 +83,6 @@ private slots: void acceptEntry(); bool commitEntry(); void cancel(); - void togglePasswordGenerator(); - void setGeneratedPassword(const QString& password); #ifdef WITH_XC_NETWORKING void updateFaviconButtonEnable(const QString& url); #endif diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index 2be52a28e7..491f35ce5a 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -20,56 +20,78 @@ <property name="bottomMargin"> <number>0</number> </property> - <item row="7" column="0" alignment="Qt::AlignRight"> - <widget class="QCheckBox" name="expireCheck"> - <property name="toolTip"> - <string>Toggle expiration</string> - </property> - <property name="accessibleName"> - <string>Toggle expiration</string> + <item row="8" column="1"> + <widget class="QLabel" name="notesHint"> + <property name="visible"> + <bool>true</bool> </property> <property name="text"> - <string>Expires</string> + <string>Toggle the checkbox to reveal the notes section.</string> + </property> + <property name="alignment"> + <set>Qt::AlignTop</set> </property> </widget> </item> - <item row="7" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item row="1" column="1"> + <widget class="QComboBox" name="usernameComboBox"> + <property name="accessibleName"> + <string>Username field</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_6"> <item> - <widget class="QDateTimeEdit" name="expireDatePicker"> - <property name="enabled"> - <bool>false</bool> - </property> + <widget class="URLEdit" name="urlEdit"> <property name="accessibleName"> - <string>Expiration field</string> - </property> - <property name="calendarPopup"> - <bool>true</bool> + <string>Url field</string> </property> </widget> </item> <item> - <widget class="QPushButton" name="expirePresets"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> + <widget class="QToolButton" name="fetchFaviconButton"> <property name="toolTip"> - <string>Expiration Presets</string> + <string>Download favicon for URL</string> </property> <property name="accessibleName"> - <string>Expiration presets</string> - </property> - <property name="text"> - <string>Presets</string> + <string>Download favicon for URL</string> </property> </widget> </item> </layout> </item> - <item row="9" column="1"> + <item row="4" column="0" alignment="Qt::AlignRight"> + <widget class="QLabel" name="urlLabel"> + <property name="text"> + <string>URL:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="PasswordEdit" name="passwordEdit"> + <property name="accessibleName"> + <string>Password field</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="6" column="0" alignment="Qt::AlignRight"> + <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> + <item row="8" column="1"> <widget class="QPlainTextEdit" name="notesEdit"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> @@ -88,16 +110,19 @@ </property> </widget> </item> - <item row="1" column="1"> - <widget class="QComboBox" name="usernameComboBox"> + <item row="8" 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>Username field</string> + <string>Toggle notes visible</string> + </property> + <property name="text"> + <string>Notes</string> </property> </widget> </item> - <item row="4" column="1"> - <widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/> - </item> <item row="0" column="1"> <widget class="QLineEdit" name="titleEdit"> <property name="accessibleName"> @@ -105,47 +130,49 @@ </property> </widget> </item> - <item row="5" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item row="2" column="0" alignment="Qt::AlignRight"> + <widget class="QLabel" name="passwordLabel"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> <item> - <widget class="URLEdit" name="urlEdit"> + <widget class="QDateTimeEdit" name="expireDatePicker"> + <property name="enabled"> + <bool>false</bool> + </property> <property name="accessibleName"> - <string>Url field</string> + <string>Expiration field</string> + </property> + <property name="calendarPopup"> + <bool>true</bool> </property> </widget> </item> <item> - <widget class="QToolButton" name="fetchFaviconButton"> + <widget class="QPushButton" name="expirePresets"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="toolTip"> - <string>Download favicon for URL</string> + <string>Expiration Presets</string> </property> <property name="accessibleName"> - <string>Download favicon for URL</string> + <string>Expiration presets</string> + </property> + <property name="text"> + <string>Presets</string> </property> </widget> </item> </layout> </item> - <item row="9" column="1"> - <widget class="QLabel" name="notesHint"> - <property name="visible"> - <bool>true</bool> - </property> - <property name="text"> - <string>Toggle the checkbox to reveal the notes section.</string> - </property> - <property name="alignment"> - <set>Qt::AlignTop</set> - </property> - </widget> - </item> - <item row="5" column="0" alignment="Qt::AlignRight"> - <widget class="QLabel" name="urlLabel"> - <property name="text"> - <string>URL:</string> - </property> - </widget> - </item> <item row="1" column="0" alignment="Qt::AlignRight"> <widget class="QLabel" name="usernameLabel"> <property name="text"> @@ -153,36 +180,6 @@ </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="2" column="0" alignment="Qt::AlignRight"> - <widget class="QLabel" name="passwordLabel"> - <property name="text"> - <string>Password:</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="PasswordEdit" name="passwordEdit"> - <property name="accessibleName"> - <string>Password field</string> - </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> </layout> <widget class="QLabel" name="titleLabel"> <property name="geometry"> @@ -199,12 +196,6 @@ </widget> </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> diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 43f32b3436..30098ef5d4 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -36,21 +36,13 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) { m_ui->setupUi(this); - m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); - m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator")); - - m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); - m_ui->passwordGenerator->hide(); - m_ui->passwordGenerator->reset(); - m_ui->messageWidget->hide(); m_ui->messageWidget->setCloseButtonVisible(false); m_ui->messageWidget->setAutoHideTimeout(-1); - connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); - connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); + m_ui->passwordEdit->enablePasswordGenerator(); + connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); - connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); @@ -201,10 +193,6 @@ void EditGroupWidgetKeeShare::update() showSharingState(); } - - m_ui->passwordGenerator->hide(); - m_ui->togglePasswordGeneratorButton->setChecked(false); - m_ui->togglePasswordButton->setChecked(false); } void EditGroupWidgetKeeShare::clearInputs() @@ -215,24 +203,6 @@ void EditGroupWidgetKeeShare::clearInputs() m_ui->passwordEdit->clear(); m_ui->pathEdit->clear(); m_ui->typeComboBox->setCurrentIndex(KeeShareSettings::Inactive); - m_ui->passwordGenerator->setVisible(false); -} - -void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked) -{ - m_ui->passwordGenerator->regeneratePassword(); - m_ui->passwordGenerator->setVisible(checked); -} - -void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password) -{ - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.password = password; - KeeShare::setReferenceTo(m_temporaryGroup, reference); - m_ui->togglePasswordGeneratorButton->setChecked(false); } void EditGroupWidgetKeeShare::selectPath() diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.h b/src/keeshare/group/EditGroupWidgetKeeShare.h index b4e169b5a0..54eef2bb09 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.h +++ b/src/keeshare/group/EditGroupWidgetKeeShare.h @@ -49,8 +49,6 @@ private slots: void selectPassword(); void launchPathSelectionDialog(); void selectPath(); - void setGeneratedPassword(const QString& password); - void togglePasswordGeneratorButton(bool checked); private: QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui; diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.ui b/src/keeshare/group/EditGroupWidgetKeeShare.ui index ad3f6dbe47..58d8dccb3d 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.ui +++ b/src/keeshare/group/EditGroupWidgetKeeShare.ui @@ -6,56 +6,62 @@ <rect> <x>0</x> <y>0</y> - <width>342</width> - <height>378</height> + <width>344</width> + <height>143</height> </rect> </property> - <property name="windowTitle"> - <string>Form</string> - </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> + <number>10</number> </property> <item> <widget class="MessageWidget" name="messageWidget" native="true"/> </item> <item> <layout class="QFormLayout" name="formLayout"> - <item row="2" column="0"> + <item row="0" column="0"> <widget class="QLabel" name="typeLabel"> <property name="text"> <string>Type:</string> </property> </widget> </item> - <item row="2" column="1"> + <item row="0" column="1"> <widget class="QComboBox" name="typeComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="accessibleName"> <string>Sharing mode field</string> </property> </widget> </item> - <item row="3" column="0"> + <item row="1" column="0"> <widget class="QLabel" name="pathLabel"> <property name="text"> <string>Path:</string> </property> </widget> </item> - <item row="3" column="1"> + <item row="1" column="1"> <layout class="QHBoxLayout" name="pathLayout"> <item> <widget class="QLineEdit" name="pathEdit"> + <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="accessibleName"> <string>Path to share file field</string> </property> @@ -73,60 +79,68 @@ </item> </layout> </item> - <item row="4" column="0"> + <item row="2" column="0"> <widget class="QLabel" name="passwordLabel"> <property name="text"> <string>Password:</string> </property> </widget> </item> - <item row="4" column="1"> - <layout class="QHBoxLayout" name="passwordLayout"> + <item row="2" column="1"> + <widget class="PasswordEdit" name="passwordEdit"> + <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="accessibleName"> + <string>Password field</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> <item> - <widget class="PasswordEdit" name="passwordEdit"> - <property name="accessibleName"> - <string>Password field</string> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> </property> - </widget> + </spacer> </item> <item> - <widget class="QToolButton" name="togglePasswordButton"> - <property name="accessibleName"> - <string>Toggle password visibility</string> + <widget class="QPushButton" name="clearButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="checkable"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="togglePasswordGeneratorButton"> <property name="accessibleName"> - <string>Toggle password generator</string> + <string>Clear fields</string> </property> - <property name="checkable"> - <bool>true</bool> + <property name="text"> + <string>Clear</string> </property> </widget> </item> </layout> </item> - <item row="5" column="1"> - <widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/> - </item> - <item row="6" column="1"> - <widget class="QPushButton" name="clearButton"> - <property name="accessibleName"> - <string>Clear fields</string> - </property> - <property name="text"> - <string>Clear</string> - </property> - </widget> - </item> </layout> </item> <item> @@ -145,12 +159,6 @@ </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> diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index dd69e30427..f8fbfa7f1f 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -58,6 +58,7 @@ #include "gui/FileDialog.h" #include "gui/MessageBox.h" #include "gui/PasswordEdit.h" +#include "gui/PasswordGeneratorWidget.h" #include "gui/SearchWidget.h" #include "gui/TotpDialog.h" #include "gui/TotpSetupDialog.h" @@ -652,52 +653,63 @@ void TestGui::testPasswordEntryEntropy() auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>(); QVERIFY(passwordEdit); QTest::mouseClick(passwordEdit, Qt::LeftButton); + + QTimer::singleShot(50, this, SLOT(passwordGeneratorCallback())); QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier); +} + +void TestGui::passwordGeneratorCallback() +{ + auto* pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>(); + QVERIFY(pwGeneratorWidget); // Type in some password - auto* editNewPassword = editEntryWidget->findChild<QLineEdit*>("editNewPassword"); - auto* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel"); - auto* strengthLabel = editEntryWidget->findChild<QLabel*>("strengthLabel"); + auto* generatedPassword = pwGeneratorWidget->findChild<QLineEdit*>("editNewPassword"); + auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel"); + auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel"); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "hello"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "hello"); QCOMPARE(entropyLabel->text(), QString("Entropy: 6.38 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "helloworld"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "helloworld"); QCOMPARE(entropyLabel->text(), QString("Entropy: 13.10 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "password1"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "password1"); QCOMPARE(entropyLabel->text(), QString("Entropy: 4.00 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "D0g.................."); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "D0g.................."); QCOMPARE(entropyLabel->text(), QString("Entropy: 19.02 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "Tr0ub4dour&3"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "Tr0ub4dour&3"); QCOMPARE(entropyLabel->text(), QString("Entropy: 30.87 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Poor")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "correcthorsebatterystaple"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "correcthorsebatterystaple"); QCOMPARE(entropyLabel->text(), QString("Entropy: 47.98 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Weak")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "YQC3kbXbjC652dTDH"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "YQC3kbXbjC652dTDH"); QCOMPARE(entropyLabel->text(), QString("Entropy: 95.83 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Good")); - editNewPassword->setText(""); - QTest::keyClicks(editNewPassword, "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km"); + generatedPassword->setText(""); + QTest::keyClicks(generatedPassword, "Bs5ZFfthWzR8DGFEjaCM6bGqhmCT4km"); QCOMPARE(entropyLabel->text(), QString("Entropy: 174.59 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Excellent")); + + QTest::mouseClick(generatedPassword, Qt::LeftButton); + QTest::keyClick(generatedPassword, Qt::Key_Escape); } void TestGui::testDicewareEntryEntropy() @@ -726,11 +738,20 @@ void TestGui::testDicewareEntryEntropy() auto* passwordEdit = editEntryWidget->findChild<PasswordEdit*>(); QVERIFY(passwordEdit); QTest::mouseClick(passwordEdit, Qt::LeftButton); + + QTimer::singleShot(50, this, SLOT(passwordGeneratorCallback())); QTest::keyClick(passwordEdit, Qt::Key_G, Qt::ControlModifier); +} + +void TestGui::passphraseGeneratorCallback() +{ + auto* pwGeneratorWidget = m_dbWidget->findChild<PasswordGeneratorWidget*>(); + QVERIFY(pwGeneratorWidget); // Select Diceware - auto* tabWidget = editEntryWidget->findChild<QTabWidget*>("tabWidget"); - auto* dicewareWidget = editEntryWidget->findChild<QWidget*>("dicewareWidget"); + auto* generatedPassword = pwGeneratorWidget->findChild<QLineEdit*>("editNewPassword"); + auto* tabWidget = pwGeneratorWidget->findChild<QTabWidget*>("tabWidget"); + auto* dicewareWidget = pwGeneratorWidget->findChild<QWidget*>("dicewareWidget"); tabWidget->setCurrentWidget(dicewareWidget); auto* comboBoxWordList = dicewareWidget->findChild<QComboBox*>("comboBoxWordList"); @@ -738,12 +759,15 @@ void TestGui::testDicewareEntryEntropy() auto* spinBoxWordCount = dicewareWidget->findChild<QSpinBox*>("spinBoxWordCount"); spinBoxWordCount->setValue(6); - // Type in some password - auto* entropyLabel = editEntryWidget->findChild<QLabel*>("entropyLabel"); - auto* strengthLabel = editEntryWidget->findChild<QLabel*>("strengthLabel"); + // Verify entropy and strength + auto* entropyLabel = pwGeneratorWidget->findChild<QLabel*>("entropyLabel"); + auto* strengthLabel = pwGeneratorWidget->findChild<QLabel*>("strengthLabel"); QCOMPARE(entropyLabel->text(), QString("Entropy: 77.55 bit")); QCOMPARE(strengthLabel->text(), QString("Password Quality: Good")); + + QTest::mouseClick(generatedPassword, Qt::LeftButton); + QTest::keyClick(generatedPassword, Qt::Key_Escape); } void TestGui::testTotp() diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index b7798d0b26..df84f53187 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -39,6 +39,8 @@ class TestGui : public QObject protected slots: void createDatabaseCallback(); + void passwordGeneratorCallback(); + void passphraseGeneratorCallback(); private slots: void initTestCase(); From 8ae718b747b99c6917fb55814304d6e263fb3414 Mon Sep 17 00:00:00 2001 From: Balazs Gyurak <ba32107@gmail.com> Date: Sun, 17 Nov 2019 20:39:16 +0000 Subject: [PATCH 081/215] Ignore focus when checking toolbar state * Support copy shortcut when in QTextEdit to prevent inadvertently copying password when interacting with those elements. --- src/gui/DatabaseWidget.cpp | 14 +++++++++----- src/gui/DatabaseWidget.h | 1 - src/gui/MainWindow.cpp | 8 +++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 5bda87be15..6434ba9236 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -30,6 +30,7 @@ #include <QLineEdit> #include <QProcess> #include <QSplitter> +#include <QTextEdit> #include "autotype/AutoType.h" #include "core/Config.h" @@ -638,6 +639,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())); @@ -1566,11 +1575,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(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a96a34a9f3..2b9388f37c 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -104,7 +104,6 @@ class DatabaseWidget : public QStackedWidget bool isPasswordsHidden() const; void setPasswordsHidden(bool hide); void clearAllWidgets(); - bool currentEntryHasFocus(); bool currentEntryHasTitle(); bool currentEntryHasUsername(); bool currentEntryHasPassword(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index a25213980c..7623a13f8d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -644,10 +644,8 @@ 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(); @@ -1192,7 +1190,7 @@ 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) { From 2359742de1f96ba0a4807754cc209994a35ca83d Mon Sep 17 00:00:00 2001 From: Aetf <aetf@unlimitedcodeworks.xyz> Date: Sat, 15 Feb 2020 20:39:32 -0500 Subject: [PATCH 082/215] FdoSecrets: only enable the settings page when there is actually a service instance * Fix #4311 --- .../DatabaseSettingsWidgetFdoSecrets.ui | 12 ++++++ .../widgets/SettingsWidgetFdoSecrets.cpp | 41 +++++++++++-------- .../widgets/SettingsWidgetFdoSecrets.h | 2 +- .../widgets/SettingsWidgetFdoSecrets.ui | 12 ++++++ 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui index 7eb21705a1..24c820e08b 100644 --- a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui @@ -11,6 +11,18 @@ </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_2"> + <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="MessageWidget" name="warningWidget" native="true"/> </item> diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 29b01c67b3..838f6d20fc 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -264,8 +264,11 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi dbViewHeader->setSectionResizeMode(1, QHeaderView::Stretch); // group dbViewHeader->setSectionResizeMode(2, QHeaderView::ResizeToContents); // manage button - m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked()); - connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled); + // prompt the user to save settings before the sections are enabled + connect(m_plugin, &FdoSecretsPlugin::secretServiceStarted, this, &SettingsWidgetFdoSecrets::updateServiceState); + connect(m_plugin, &FdoSecretsPlugin::secretServiceStopped, this, &SettingsWidgetFdoSecrets::updateServiceState); + connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::updateServiceState); + updateServiceState(); // background checking m_checkTimer.setInterval(2000); @@ -310,18 +313,6 @@ void SettingsWidgetFdoSecrets::saveSettings() FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); } -void SettingsWidgetFdoSecrets::showMessage(const QString& text, MessageWidget::MessageType type) -{ - // Show error messages for a longer time to make sure the user can read them - if (type == MessageWidget::Error) { - m_ui->warningMsg->setCloseButtonVisible(true); - m_ui->warningMsg->showMessage(text, type, -1); - } else { - m_ui->warningMsg->setCloseButtonVisible(false); - m_ui->warningMsg->showMessage(text, type, 2000); - } -} - void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) { QWidget::showEvent(event); @@ -344,14 +335,32 @@ void SettingsWidgetFdoSecrets::checkDBusName() auto reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral(DBUS_SERVICE_SECRET)); if (!reply.isValid()) { - showMessage(tr("<b>Error:</b> Failed to connect to DBus. Please check your DBus setup."), MessageWidget::Error); + m_ui->warningMsg->showMessage( + tr("<b>Error:</b> Failed to connect to DBus. Please check your DBus setup."), MessageWidget::Error, -1); + m_ui->enableFdoSecretService->setChecked(false); + m_ui->enableFdoSecretService->setEnabled(false); return; } if (reply.value()) { - showMessage(tr("<b>Warning:</b> ") + m_plugin->reportExistingService(), MessageWidget::Warning); + m_ui->warningMsg->showMessage( + tr("<b>Warning:</b> ") + m_plugin->reportExistingService(), MessageWidget::Warning, -1); + m_ui->enableFdoSecretService->setChecked(false); + m_ui->enableFdoSecretService->setEnabled(false); return; } m_ui->warningMsg->hideMessage(); + m_ui->enableFdoSecretService->setEnabled(true); +} + +void SettingsWidgetFdoSecrets::updateServiceState() +{ + m_ui->tabWidget->setEnabled(m_plugin->serviceInstance() != nullptr); + if (m_ui->enableFdoSecretService->isChecked() && !m_plugin->serviceInstance()) { + m_ui->tabWidget->setToolTip( + tr("Save current changes to activate the plugin and enable editing of this section.")); + } else { + m_ui->tabWidget->setToolTip(""); + } } #include "SettingsWidgetFdoSecrets.moc" diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index f6147cc246..c323b3900b 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -55,7 +55,7 @@ public slots: private slots: void checkDBusName(); - void showMessage(const QString& text, MessageWidget::MessageType type); + void updateServiceState(); protected: void showEvent(QShowEvent* event) override; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index ee7d494319..abc15d56e9 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -14,6 +14,18 @@ <string>Options</string> </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="MessageWidget" name="warningMsg" native="true"/> </item> From dce9af219fe906f2fc20e134bf7ee07c1ea51b32 Mon Sep 17 00:00:00 2001 From: Toni Spets <toni.spets@iki.fi> Date: Thu, 6 Feb 2020 10:15:50 +0200 Subject: [PATCH 083/215] SSH Agent: Integration tests against ssh-agent Windows testing is currently explicitly disabled due to too many different scenarios to run an agent and MSYS2 having its own. --- src/sshagent/AgentSettingsWidget.cpp | 3 +- src/sshagent/SSHAgent.cpp | 123 +++++++++++---- src/sshagent/SSHAgent.h | 3 +- tests/CMakeLists.txt | 4 + tests/TestSSHAgent.cpp | 214 +++++++++++++++++++++++++++ tests/TestSSHAgent.h | 46 ++++++ 6 files changed, 361 insertions(+), 32 deletions(-) create mode 100644 tests/TestSSHAgent.cpp create mode 100644 tests/TestSSHAgent.h diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp index e06929195e..f7d85ce779 100644 --- a/src/sshagent/AgentSettingsWidget.cpp +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -68,7 +68,8 @@ void AgentSettingsWidget::loadSettings() return; } #endif - if (sshAgent()->testConnection()) { + QList<QSharedPointer<OpenSSHKey>> keys; + if (sshAgent()->listIdentities(keys)) { m_ui->sshAuthSockMessageWidget->showMessage(tr("SSH Agent connection is working!"), MessageWidget::Positive); } else { diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 3bd3c8df31..6bed354f21 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -212,36 +212,6 @@ bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out) } #endif -/** - * Test if connection to SSH agent is working. - * - * @return true on success - */ -bool SSHAgent::testConnection() -{ - if (!isAgentRunning()) { - m_error = tr("No agent running, cannot test connection."); - return false; - } - - QByteArray requestData; - BinaryStream request(&requestData); - - request.write(SSH_AGENTC_REQUEST_IDENTITIES); - - QByteArray responseData; - if (!sendMessage(requestData, responseData)) { - return false; - } - - if (responseData.length() < 1 || static_cast<quint8>(responseData[0]) != SSH_AGENT_IDENTITIES_ANSWER) { - m_error = tr("Agent protocol error."); - return false; - } - - return true; -} - /** * Add the identity to the SSH agent. * @@ -328,6 +298,99 @@ bool SSHAgent::removeIdentity(OpenSSHKey& key) return sendMessage(requestData, responseData); } +/** + * Get a list of identities from the SSH agent. + * + * @param list list of keys to append + * @return true on success + */ +bool SSHAgent::listIdentities(QList<QSharedPointer<OpenSSHKey>>& list) +{ + if (!isAgentRunning()) { + m_error = tr("No agent running, cannot list identities."); + return false; + } + + QByteArray requestData; + BinaryStream request(&requestData); + + request.write(SSH_AGENTC_REQUEST_IDENTITIES); + + QByteArray responseData; + if (!sendMessage(requestData, responseData)) { + return false; + } + + BinaryStream response(&responseData); + + quint8 responseType; + if (!response.read(responseType) || responseType != SSH_AGENT_IDENTITIES_ANSWER) { + m_error = tr("Agent protocol error."); + return false; + } + + quint32 nKeys; + if (!response.read(nKeys)) { + m_error = tr("Agent protocol error."); + return false; + } + + for (quint32 i = 0; i < nKeys; i++) { + QByteArray publicData; + QString comment; + + if (!response.readString(publicData)) { + m_error = tr("Agent protocol error."); + return false; + } + + if (!response.readString(comment)) { + m_error = tr("Agent protocol error."); + return false; + } + + OpenSSHKey* key = new OpenSSHKey(); + key->setComment(comment); + + list.append(QSharedPointer<OpenSSHKey>(key)); + + BinaryStream publicDataStream(&publicData); + if (!key->readPublic(publicDataStream)) { + m_error = key->errorString(); + return false; + } + } + + return true; +} + +/** + * Check if this identity is loaded in the SSH Agent. + * + * @param key identity to remove + * @param loaded is the key laoded + * @return true on success + */ +bool SSHAgent::checkIdentity(OpenSSHKey& key, bool& loaded) +{ + QList<QSharedPointer<OpenSSHKey>> list; + + if (!listIdentities(list)) { + return false; + } + + loaded = false; + + for (const auto it : list) { + if (*it == key) { + loaded = true; + break; + } + } + + return true; +} + /** * Remove all identities known to this instance */ diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index a70af44c88..44ae88bb42 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -47,8 +47,9 @@ class SSHAgent : public QObject const QString errorString() const; bool isAgentRunning() const; - bool testConnection(); bool addIdentity(OpenSSHKey& key, KeeAgentSettings& settings); + bool listIdentities(QList<QSharedPointer<OpenSSHKey>>& list); + bool checkIdentity(OpenSSHKey& key, bool& loaded); bool removeIdentity(OpenSSHKey& key); void removeAllIdentities(); void setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c3f1c0e22b..96cb5267c1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -165,6 +165,10 @@ endif() if(WITH_XC_CRYPTO_SSH) add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp LIBS ${TEST_LIBRARIES}) + if(NOT WIN32) + add_unit_test(NAME testsshagent SOURCES TestSSHAgent.cpp + LIBS ${TEST_LIBRARIES}) + endif() endif() add_unit_test(NAME testentry SOURCES TestEntry.cpp diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp new file mode 100644 index 0000000000..4a13d64f88 --- /dev/null +++ b/tests/TestSSHAgent.cpp @@ -0,0 +1,214 @@ +/* + * 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 "TestSSHAgent.h" +#include "TestGlobal.h" +#include "core/Config.h" +#include "crypto/Crypto.h" +#include "sshagent/SSHAgent.h" + +QTEST_GUILESS_MAIN(TestSSHAgent) + +void TestSSHAgent::initTestCase() +{ + QVERIFY(Crypto::init()); + Config::createTempFileInstance(); + + m_agentSocketFile.setAutoRemove(true); + QVERIFY(m_agentSocketFile.open()); + + m_agentSocketFileName = m_agentSocketFile.fileName(); + QVERIFY(!m_agentSocketFileName.isEmpty()); + + // let ssh-agent re-create it as a socket + QVERIFY(m_agentSocketFile.remove()); + + QStringList arguments; + arguments << "-D" + << "-a" << m_agentSocketFileName; + + QElapsedTimer timer; + timer.start(); + + qDebug() << "ssh-agent starting with arguments" << arguments; + m_agentProcess.setProcessChannelMode(QProcess::ForwardedChannels); + m_agentProcess.start("ssh-agent", arguments); + m_agentProcess.closeWriteChannel(); + + if (!m_agentProcess.waitForStarted()) { + QSKIP("ssh-agent could not be started"); + } + + qDebug() << "ssh-agent started as pid" << m_agentProcess.pid(); + + // we need to wait for the agent to open the socket before going into real tests + QFileInfo socketFileInfo(m_agentSocketFileName); + while (!timer.hasExpired(2000)) { + if (socketFileInfo.exists()) { + break; + } + QTest::qWait(10); + } + + QVERIFY(socketFileInfo.exists()); + qDebug() << "ssh-agent initialized in" << timer.elapsed() << "ms"; + + // initialize test key + const QString keyString = QString("-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" + "QyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazAAAAKjgCfj94An4\n" + "/QAAAAtzc2gtZWQyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazA\n" + "AAAEBe1iilZFho8ZGAliiSj5URvFtGrgvmnEKdiLZow5hOR92U7kXaQXZbN52sEAcGL3AE\n" + "d4hLNdnQi4iqunQTN5rMAAAAH29wZW5zc2hrZXktdGVzdC1wYXJzZUBrZWVwYXNzeGMBAg\n" + "MEBQY=\n" + "-----END OPENSSH PRIVATE KEY-----\n"); + + const QByteArray keyData = keyString.toLatin1(); + + QVERIFY(m_key.parsePKCS1PEM(keyData)); +} + +void TestSSHAgent::testConfiguration() +{ + SSHAgent agent; + + // default config must not enable agent + QVERIFY(!agent.isEnabled()); + + agent.setEnabled(true); + QVERIFY(agent.isEnabled()); + + // this will either be an empty string or the real ssh-agent socket path, doesn't matter + QString defaultSocketPath = agent.socketPath(false); + + // overridden path must match default before setting an override + QCOMPARE(agent.socketPath(true), defaultSocketPath); + + agent.setAuthSockOverride(m_agentSocketFileName); + + // overridden path must match what we set + QCOMPARE(agent.socketPath(true), m_agentSocketFileName); + + // non-overridden path must match the default + QCOMPARE(agent.socketPath(false), defaultSocketPath); +} + +void TestSSHAgent::testIdentity() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + // test adding a key works + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + // test removing a key works + QVERIFY(agent.removeIdentity(m_key)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::testRemoveOnClose() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setRemoveAtDatabaseClose(true); + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + agent.setEnabled(false); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::testLifetimeConstraint() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setUseLifetimeConstraintWhenAdding(true); + settings.setLifetimeConstraintDuration(2); // two seconds + + // identity should be in agent immediately after adding + QVERIFY(agent.addIdentity(m_key, settings)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + QElapsedTimer timer; + timer.start(); + + // wait for the identity to time out + while (!timer.hasExpired(5000)) { + QVERIFY(agent.checkIdentity(m_key, keyInAgent)); + + if (!keyInAgent) { + break; + } + + QTest::qWait(100); + } + + QVERIFY(!keyInAgent); +} + +void TestSSHAgent::testConfirmConstraint() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + + QVERIFY(agent.isAgentRunning()); + + KeeAgentSettings settings; + bool keyInAgent; + + settings.setUseConfirmConstraintWhenAdding(true); + + QVERIFY(agent.addIdentity(m_key, settings)); + + // we can't test confirmation itself is working but we can test the agent accepts the key + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); + + QVERIFY(agent.removeIdentity(m_key)); + QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::cleanupTestCase() +{ + if (m_agentProcess.state() != QProcess::NotRunning) { + qDebug() << "Killing ssh-agent pid" << m_agentProcess.pid(); + m_agentProcess.terminate(); + m_agentProcess.waitForFinished(); + } + + m_agentSocketFile.remove(); +} diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h new file mode 100644 index 0000000000..6b99e8e654 --- /dev/null +++ b/tests/TestSSHAgent.h @@ -0,0 +1,46 @@ +/* + * 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 TESTSSHAGENT_H +#define TESTSSHAGENT_H + +#include "crypto/ssh/OpenSSHKey.h" +#include <QObject> +#include <QProcess> +#include <QTemporaryFile> + +class TestSSHAgent : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testConfiguration(); + void testIdentity(); + void testRemoveOnClose(); + void testLifetimeConstraint(); + void testConfirmConstraint(); + void cleanupTestCase(); + +private: + QTemporaryFile m_agentSocketFile; + QString m_agentSocketFileName; + QProcess m_agentProcess; + OpenSSHKey m_key; +}; + +#endif // TESTSSHAGENT_H From b2c2f42f302667143b863dfb87c152285a80ae92 Mon Sep 17 00:00:00 2001 From: Benedikt Rascher-Friesenhausen <benediktrascherfriesenhausen+git@gmail.com> Date: Sat, 16 Nov 2019 17:51:56 +0100 Subject: [PATCH 084/215] Allow defining additional characters for the password generator See issue #3271 for a motivation of this feature. This patch adds an additional text input to the advanced view of the password generator. All characters of this input field (if not empty) will be added as another group to the password generator. The characters from the excluded field have precedence over the characters from this new field, meaning any character added to both fields will *not* appear in any generated password. As the characters from this new field will be added as their own group to the password generator, checking the 'Include characters from every group' checkbox will force at least character to be chosen from the new input field. The `PasswordGenerator` class has also been changed so that the `isValid` method returns `true` if only characters from the new input field would be used. There is a new, simple test that covers the new feature. While the test only uses ASCII characters, any Unicode characters can be used with the new feature. --- src/browser/BrowserSettings.cpp | 10 + src/browser/BrowserSettings.h | 2 + src/core/PasswordGenerator.cpp | 18 +- src/core/PasswordGenerator.h | 3 + src/gui/PasswordGeneratorWidget.cpp | 17 +- src/gui/PasswordGeneratorWidget.ui | 905 +++++++++++++++------------- tests/TestPasswordGenerator.cpp | 13 + tests/TestPasswordGenerator.h | 1 + 8 files changed, 533 insertions(+), 436 deletions(-) diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index b49af7005b..eb5d81d29b 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -412,6 +412,16 @@ void BrowserSettings::setAdvancedMode(bool advancedMode) config()->set("generator/AdvancedMode", advancedMode); } +QString BrowserSettings::passwordAdditionalChars() +{ + return config()->get("generator/AdditionalChars", PasswordGenerator::DefaultAdditionalChars).toString(); +} + +void BrowserSettings::setPasswordAdditionalChars(const QString& chars) +{ + config()->set("generator/AdditionalChars", chars); +} + QString BrowserSettings::passwordExcludedChars() { return config()->get("generator/ExcludedChars", PasswordGenerator::DefaultExcludedChars).toString(); diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h index 395455cbcb..9340cd0a3a 100644 --- a/src/browser/BrowserSettings.h +++ b/src/browser/BrowserSettings.h @@ -107,6 +107,8 @@ class BrowserSettings void setPasswordUseEASCII(bool useEASCII); bool advancedMode(); void setAdvancedMode(bool advancedMode); + QString passwordAdditionalChars(); + void setPasswordAdditionalChars(const QString& chars); QString passwordExcludedChars(); void setPasswordExcludedChars(const QString& chars); int passPhraseWordCount(); diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index ff271a4533..efe6478809 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -20,12 +20,14 @@ #include "crypto/Random.h" +const char* PasswordGenerator::DefaultAdditionalChars = ""; const char* PasswordGenerator::DefaultExcludedChars = ""; PasswordGenerator::PasswordGenerator() : m_length(0) , m_classes(nullptr) , m_flags(nullptr) + , m_additional(PasswordGenerator::DefaultAdditionalChars) , m_excluded(PasswordGenerator::DefaultExcludedChars) { } @@ -53,6 +55,11 @@ void PasswordGenerator::setFlags(const GeneratorFlags& flags) m_flags = flags; } +void PasswordGenerator::setAdditionalChars(const QString& chars) +{ + m_additional = chars; +} + void PasswordGenerator::setExcludedChars(const QString& chars) { m_excluded = chars; @@ -107,7 +114,7 @@ QString PasswordGenerator::generatePassword() const bool PasswordGenerator::isValid() const { - if (m_classes == 0) { + if (m_classes == 0 && m_additional.isEmpty()) { return false; } else if (m_length == 0) { return false; @@ -259,6 +266,15 @@ QVector<PasswordGroup> PasswordGenerator::passwordGroups() const passwordGroups.append(group); } + if (!m_additional.isEmpty()) { + PasswordGroup group; + + for (auto ch : m_additional) { + group.append(ch); + } + + passwordGroups.append(group); + } // Loop over character groups and remove excluded characters from them; // remove empty groups diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h index 55418b4ba2..20681c2333 100644 --- a/src/core/PasswordGenerator.h +++ b/src/core/PasswordGenerator.h @@ -60,6 +60,7 @@ class PasswordGenerator void setLength(int length); void setCharClasses(const CharClasses& classes); void setFlags(const GeneratorFlags& flags); + void setAdditionalChars(const QString& chars); void setExcludedChars(const QString& chars); bool isValid() const; @@ -67,6 +68,7 @@ class PasswordGenerator QString generatePassword() const; static const int DefaultLength = 16; + static const char* DefaultAdditionalChars; static const char* DefaultExcludedChars; static constexpr bool DefaultLower = (DefaultCharset & LowerLetters) != 0; static constexpr bool DefaultUpper = (DefaultCharset & UpperLetters) != 0; @@ -90,6 +92,7 @@ class PasswordGenerator int m_length; CharClasses m_classes; GeneratorFlags m_flags; + QString m_additional; QString m_excluded; Q_DISABLE_COPY(PasswordGenerator) diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 4361248131..7ce45b1d1c 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -48,6 +48,7 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updatePasswordStrength(QString))); 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())); @@ -114,6 +115,8 @@ void PasswordGeneratorWidget::loadSettings() config()->get("generator/SpecialChars", PasswordGenerator::DefaultSpecial).toBool()); m_ui->checkBoxNumbersAdv->setChecked( config()->get("generator/Numbers", PasswordGenerator::DefaultNumbers).toBool()); + m_ui->editAdditionalChars->setText( + config()->get("generator/AdditionalChars", PasswordGenerator::DefaultAdditionalChars).toString()); m_ui->editExcludedChars->setText( config()->get("generator/ExcludedChars", PasswordGenerator::DefaultExcludedChars).toString()); @@ -315,6 +318,7 @@ void PasswordGeneratorWidget::setAdvancedMode(bool state) { 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()); @@ -325,15 +329,9 @@ void PasswordGeneratorWidget::setAdvancedMode(bool state) 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(); } else { - m_ui->advancedBar->hide(); - m_ui->excludedChars->hide(); - m_ui->checkBoxExcludeAlike->hide(); - m_ui->checkBoxEnsureEvery->hide(); + 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()); @@ -342,7 +340,6 @@ void PasswordGeneratorWidget::setAdvancedMode(bool state) | 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(); } } @@ -523,8 +520,10 @@ void PasswordGeneratorWidget::updateGenerator() m_passwordGenerator->setCharClasses(classes); 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(""); } diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index 62d96a2a1d..4981d3ccda 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -199,6 +199,89 @@ QProgressBar::chunk { <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>&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> + <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"> @@ -401,11 +484,8 @@ QProgressBar::chunk { </widget> </item> <item> - <widget class="QWidget" name="advancedBar" native="true"> - <property name="enabled"> - <bool>true</bool> - </property> - <layout class="QHBoxLayout" name="horizontalLayout_5"> + <widget class="QWidget" name="advancedContainer" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3"> <property name="leftMargin"> <number>0</number> </property> @@ -419,323 +499,454 @@ QProgressBar::chunk { <number>0</number> </property> <item> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <property name="sizeConstraint"> - <enum>QLayout::SetMinimumSize</enum> + <widget class="QWidget" name="advancedBar" native="true"> + <property name="enabled"> + <bool>true</bool> </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> + <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>" '</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><*+!?=</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>#$%&&@^`~</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="QVBoxLayout" name="verticalLayout_6"> - <property name="sizeConstraint"> - <enum>QLayout::SetMinimumSize</enum> + <layout class="QGridLayout" name="gridLayout"> + <property name="bottomMargin"> + <number>0</number> </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> + <item row="0" column="0"> + <widget class="QLabel" name="label"> <property name="text"> - <string>0-9</string> + <string>Also choose from:</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> + <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="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> + <width>200</width> + <height>0</height> </size> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> <property name="toolTip"> - <string>Punctuation</string> + <string>Additional characters to use for the generated password</string> </property> <property name="accessibleName"> - <string>Punctuation</string> + <string>Additional characters</string> </property> - <property name="text"> - <string>.,:;</string> - </property> - <property name="checkable"> + <property name="clearButtonEnabled"> <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>" '</string> + <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="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> + <width>200</width> + <height>0</height> </size> </property> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> <property name="toolTip"> - <string>Math Symbols</string> + <string>Character set to exclude from generated password</string> </property> <property name="accessibleName"> - <string>Math Symbols</string> + <string>Excluded characters</string> </property> - <property name="text"> - <string><*+!?=</string> - </property> - <property name="checkable"> + <property name="clearButtonEnabled"> <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> + <item row="1" column="0"> + <widget class="QLabel" name="labelExcludedChars"> <property name="text"> - <string>\_|-/</string> - </property> - <property name="checkable"> - <bool>true</bool> + <string>Do not include:</string> </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> + <item row="1" column="2"> + <widget class="QPushButton" name="buttonAddHex"> <property name="focusPolicy"> <enum>Qt::TabFocus</enum> </property> <property name="toolTip"> - <string>Logograms</string> + <string>Add non-hex letters to "do not include" list</string> </property> <property name="accessibleName"> - <string>Logograms</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="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> + <string>Hex Passwords</string> </property> <property name="text"> - <string>ExtendedASCII</string> + <string>Hex</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> + <widget class="QCheckBox" name="checkBoxExcludeAlike"> + <property name="toolTip"> + <string>Excluded characters: "0", "1", "l", "I", "O", "|", "﹒"</string> </property> - <property name="sizeHint" stdset="0"> - <size> - <width>0</width> - <height>0</height> - </size> + <property name="text"> + <string>Exclude look-alike characters</string> </property> - </spacer> + <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> @@ -758,166 +969,12 @@ QProgressBar::chunk { <property name="bottomMargin"> <number>0</number> </property> - <item> - <widget class="QLabel" name="labelExcludedChars"> - <property name="text"> - <string>Do not include:</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="editExcludedChars"> - <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="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> - <widget class="QPushButton" name="buttonAddHex"> - <property name="focusPolicy"> - <enum>Qt::TabFocus</enum> - </property> - <property name="toolTip"> - <string>Add non-hex letters to "do not include" list</string> - </property> - <property name="accessibleName"> - <string>Hex Passwords</string> - </property> - <property name="text"> - <string>Hex</string> - </property> - </widget> - </item> </layout> </widget> </item> - <item> - <widget class="QCheckBox" name="checkBoxExcludeAlike"> - <property name="toolTip"> - <string>Excluded characters: "0", "1", "l", "I", "O", "|", "﹒"</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> - <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>&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> - <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> </layout> </widget> <widget class="QWidget" name="dicewareWidget"> @@ -1158,10 +1215,6 @@ QProgressBar::chunk { <tabstop>checkBoxQuotes</tabstop> <tabstop>checkBoxDashes</tabstop> <tabstop>checkBoxExtASCIIAdv</tabstop> - <tabstop>editExcludedChars</tabstop> - <tabstop>buttonAddHex</tabstop> - <tabstop>checkBoxExcludeAlike</tabstop> - <tabstop>checkBoxEnsureEvery</tabstop> <tabstop>comboBoxWordList</tabstop> <tabstop>sliderWordCount</tabstop> <tabstop>spinBoxWordCount</tabstop> diff --git a/tests/TestPasswordGenerator.cpp b/tests/TestPasswordGenerator.cpp index b043a7cd08..89e2eb91ce 100644 --- a/tests/TestPasswordGenerator.cpp +++ b/tests/TestPasswordGenerator.cpp @@ -29,6 +29,19 @@ void TestPasswordGenerator::initTestCase() QVERIFY(Crypto::init()); } +void TestPasswordGenerator::testAdditionalChars() +{ + PasswordGenerator generator; + QVERIFY(!generator.isValid()); + generator.setAdditionalChars("aql"); + generator.setLength(2000); + QVERIFY(generator.isValid()); + QString password = generator.generatePassword(); + QCOMPARE(password.size(), 2000); + QRegularExpression regex(R"(^[aql]+$)"); + QVERIFY(regex.match(password).hasMatch()); +} + void TestPasswordGenerator::testCharClasses() { PasswordGenerator generator; diff --git a/tests/TestPasswordGenerator.h b/tests/TestPasswordGenerator.h index 56c4d65a13..454d16e068 100644 --- a/tests/TestPasswordGenerator.h +++ b/tests/TestPasswordGenerator.h @@ -26,6 +26,7 @@ class TestPasswordGenerator : public QObject private slots: void initTestCase(); + void testAdditionalChars(); void testCharClasses(); void testLookalikeExclusion(); }; From 4ff781fa4846c7e1d9d90230dac9bc2f14369fa4 Mon Sep 17 00:00:00 2001 From: Jonathan White <support@dmapps.us> Date: Tue, 10 Mar 2020 21:59:43 -0400 Subject: [PATCH 085/215] Version Bump and Deployment Fixes * Use KeePassXC executable icon for the start menu shortcut on Windows to prevent the icon from being deleted on installation of a new version. Fixes #4226 * Support improvements to windeployqt in Qt 5.14.1+ --- CHANGELOG.md | 2 +- CMakeLists.txt | 4 ++-- share/linux/org.keepassxc.KeePassXC.appdata.xml | 5 +++++ share/windows/wix-template.xml | 1 - snap/snapcraft.yaml | 2 +- src/CMakeLists.txt | 8 +++++--- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ec8f1f61..76bb5c38bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.6 (unreleased) +## 2.6.0 (unreleased) ### Added - Added CLI db-info command [#4231] diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b41f39248..f1f5c328a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,8 +99,8 @@ if(NOT WITH_XC_NETWORKING AND WITH_XC_UPDATECHECK) endif() set(KEEPASSXC_VERSION_MAJOR "2") -set(KEEPASSXC_VERSION_MINOR "5") -set(KEEPASSXC_VERSION_PATCH "3") +set(KEEPASSXC_VERSION_MINOR "6") +set(KEEPASSXC_VERSION_PATCH "0") set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION_MAJOR}.${KEEPASSXC_VERSION_MINOR}.${KEEPASSXC_VERSION_PATCH}") set(OVERRIDE_VERSION "" CACHE STRING "Override the KeePassXC Version for Snapshot builds") diff --git a/share/linux/org.keepassxc.KeePassXC.appdata.xml b/share/linux/org.keepassxc.KeePassXC.appdata.xml index f670ebce93..cb8d53cb1c 100644 --- a/share/linux/org.keepassxc.KeePassXC.appdata.xml +++ b/share/linux/org.keepassxc.KeePassXC.appdata.xml @@ -50,6 +50,11 @@ </screenshots> <releases> + <release version="2.6.0" date="2020-04-01"> + <description> + <ul><li>TBD</li></ul> + </description> + </release> <release version="2.5.3" date="2020-01-19"> <description> <ul> diff --git a/share/windows/wix-template.xml b/share/windows/wix-template.xml index 9693d63444..3f808723ab 100644 --- a/share/windows/wix-template.xml +++ b/share/windows/wix-template.xml @@ -64,7 +64,6 @@ <Shortcut Id="ApplicationStartMenuShortcut" Name="KeePassXC" Target="[#CM_FP_KeePassXC.exe]" - Icon="ProductIcon.ico" WorkingDirectory="INSTALL_ROOT"/> <RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/> <RegistryValue Root="HKCU" Key="Software\KeePassXC" Name="installed" Type="integer" Value="1" KeyPath="yes"/> diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d30349ae65..c03e302bf2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: keepassxc -version: 2.5.3 +version: 2.6.0 grade: stable summary: Community-driven port of the Windows application “KeePass Password Safe” description: | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5214242b27..327f088be1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -465,9 +465,11 @@ if(MINGW) COMPONENT Runtime) # Use windeployqt.exe to setup Qt dependencies - set(WINDEPLOYQT_MODE "--release") - if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") - set(WINDEPLOYQT_MODE "--debug") + if(Qt5Core_VERSION VERSION_LESS "5.14.1") + set(WINDEPLOYQT_MODE "--release") + if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") + set(WINDEPLOYQT_MODE "--debug") + endif() endif() install(CODE "execute_process(COMMAND ${WINDEPLOYQT_EXE} ${PROGNAME}.exe ${WINDEPLOYQT_MODE} WORKING_DIRECTORY \${CMAKE_INSTALL_PREFIX} OUTPUT_QUIET)" From b045160e4fcd612def4c5cff55f9469baf12a738 Mon Sep 17 00:00:00 2001 From: Janek Bevendorff <janek@jbev.net> Date: Mon, 9 Mar 2020 01:27:16 +0100 Subject: [PATCH 086/215] Bundle icons using the Qt resource system. Simplify resource loading logic and enable reproducible builds. Fixes #2582 --- .github/CONTRIBUTING.md | 2 +- share/CMakeLists.txt | 11 +- share/icons/application/index.theme | 45 +++ .../application/scalable/apps/freedesktop.svg | 93 +------ share/icons/icons.qrc | 152 ++++++++++ share/wizard/wizard.qrc | 6 + src/CMakeLists.txt | 6 +- src/autotype/AutoType.cpp | 4 +- src/autotype/AutoTypeSelectDialog.cpp | 4 +- src/browser/BrowserOptionDialog.cpp | 2 +- src/core/Bootstrap.cpp | 1 + src/core/DatabaseIcons.cpp | 13 +- src/core/FilePath.cpp | 259 ------------------ src/core/PassphraseGenerator.cpp | 4 +- src/core/Resources.cpp | 232 ++++++++++++++++ src/core/{FilePath.h => Resources.h} | 27 +- src/core/Translator.cpp | 6 +- .../DatabaseSettingsPageFdoSecrets.cpp | 4 +- src/fdosecrets/FdoSecretsPlugin.h | 4 +- src/fdosecrets/widgets/SettingsModels.cpp | 6 +- .../widgets/SettingsWidgetFdoSecrets.cpp | 12 +- src/gui/AboutDialog.cpp | 4 +- src/gui/ApplicationSettingsWidget.cpp | 6 +- src/gui/CloneDialog.cpp | 2 +- src/gui/DatabaseOpenWidget.cpp | 8 +- src/gui/DatabaseWidget.cpp | 2 +- src/gui/EditWidget.cpp | 2 +- src/gui/EntryPreviewWidget.cpp | 14 +- src/gui/KMessageWidget.cpp | 4 +- src/gui/LineEdit.cpp | 4 +- src/gui/MainWindow.cpp | 104 +++---- src/gui/PasswordEdit.cpp | 12 +- src/gui/PasswordGeneratorWidget.cpp | 10 +- src/gui/SearchWidget.cpp | 8 +- src/gui/URLEdit.cpp | 4 +- src/gui/UpdateCheckDialog.cpp | 4 +- src/gui/WelcomeWidget.cpp | 4 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 9 +- src/gui/entry/EditEntryWidget.cpp | 22 +- src/gui/entry/EntryModel.cpp | 10 +- src/gui/entry/EntryURLModel.cpp | 4 +- src/gui/group/EditGroupWidget.cpp | 8 +- src/gui/masterkey/PasswordEditWidget.cpp | 2 +- src/gui/reports/ReportsPageHealthcheck.cpp | 4 +- src/gui/reports/ReportsPageStatistics.cpp | 4 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 4 +- src/gui/reports/ReportsWidgetStatistics.cpp | 4 +- src/gui/styles/base/BaseStyle.cpp | 2 +- src/gui/wizard/NewDatabaseWizard.cpp | 5 +- src/keeshare/DatabaseSettingsPageKeeShare.cpp | 4 +- src/keeshare/SettingsPageKeeShare.cpp | 4 +- src/keeshare/group/EditGroupPageKeeShare.cpp | 4 +- .../group/EditGroupWidgetKeeShare.cpp | 2 +- src/sshagent/AgentSettingsPage.cpp | 4 +- tests/TestAutoType.cpp | 4 +- utils/makeicons.sh | 1 + 56 files changed, 634 insertions(+), 552 deletions(-) create mode 100644 share/icons/application/index.theme create mode 100644 share/icons/icons.qrc create mode 100644 share/wizard/wizard.qrc delete mode 100644 src/core/FilePath.cpp create mode 100644 src/core/Resources.cpp rename src/core/{FilePath.h => Resources.h} (68%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 454b5f5009..d588bced96 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -132,7 +132,7 @@ For **Qt-UI files** (*.ui*): 2 spaces // Application includes #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" // Global includes #include <QWidget> diff --git a/share/CMakeLists.txt b/share/CMakeLists.txt index 3a088bb886..e07e6f0823 100644 --- a/share/CMakeLists.txt +++ b/share/CMakeLists.txt @@ -19,10 +19,6 @@ add_subdirectory(translations) file(GLOB wordlists_files "wordlists/*.wordlist") install(FILES ${wordlists_files} DESTINATION ${DATA_INSTALL_DIR}/wordlists) -file(GLOB DATABASE_ICONS icons/database/*.png) - -install(FILES ${DATABASE_ICONS} DESTINATION ${DATA_INSTALL_DIR}/icons/database) - if(UNIX AND NOT APPLE AND NOT HAIKU) install(DIRECTORY icons/application/ DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor FILES_MATCHING PATTERN "keepassx*.png" PATTERN "keepassx*.svg" @@ -39,12 +35,9 @@ if(APPLE) install(FILES macosx/keepassxc.icns DESTINATION ${DATA_INSTALL_DIR}) endif() -install(DIRECTORY docs/ DESTINATION ${DATA_INSTALL_DIR}/docs FILES_MATCHING PATTERN "*.pdf") +install(FILES icons/application/256x256/apps/keepassxc.png DESTINATION ${DATA_INSTALL_DIR}/icons/application/256x256/apps) -install(DIRECTORY wizard/ DESTINATION ${DATA_INSTALL_DIR}/wizard FILES_MATCHING PATTERN "*.png") - -install(DIRECTORY icons/application/ DESTINATION ${DATA_INSTALL_DIR}/icons/application - FILES_MATCHING PATTERN "*.png" PATTERN "*.svg") +install(DIRECTORY docs/ DESTINATION ${DATA_INSTALL_DIR}/docs FILES_MATCHING PATTERN "*.pdf") add_custom_target(icons # SVG to PNGs for KeePassXC diff --git a/share/icons/application/index.theme b/share/icons/application/index.theme new file mode 100644 index 0000000000..ed1828763e --- /dev/null +++ b/share/icons/application/index.theme @@ -0,0 +1,45 @@ +[Icon Theme] +Name=application +Comment=KeePassXC Application Icon Theme + +Directories=256x256/apps,scalable/actions,scalable/apps,scalable/categories,scalable/mimetypes,scalable/status + +[scalable/actions] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 +Context=Actions + +[scalable/apps] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 +Context=Applications + +[scalable/categories] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 +Context=Categories + +[scalable/mimetypes] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 +Context=MimeTypes + +[scalable/status] +Size=48 +Type=Scalable +MinSize=1 +MaxSize=256 +Context=Status + +[256x256/apps] +Size=256 +Type=Fixed +Context=Applications diff --git a/share/icons/application/scalable/apps/freedesktop.svg b/share/icons/application/scalable/apps/freedesktop.svg index 19cda86784..1077e24ca9 100644 --- a/share/icons/application/scalable/apps/freedesktop.svg +++ b/share/icons/application/scalable/apps/freedesktop.svg @@ -1,92 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - version="1.1" - width="108.7505" - height="91.166321" - viewBox="0 0 87.000389 72.933061" - id="svg2" - sodipodi:docname="freedesktop.svg" - inkscape:version="0.92.4 5da689c313, 2019-01-14"> - <defs - id="defs14" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="3840" - inkscape:window-height="2106" - id="namedview12" - showgrid="false" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" - inkscape:zoom="1.7980996" - inkscape:cx="-97.45169" - inkscape:cy="25.551539" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="svg2" /> - <metadata - id="metadata57"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - transform="translate(0.01402783,0.01402783)" - id="g37" - style="fill:#ffffff;fill-rule:nonzero;stroke:#000000;stroke-width:2.45880008;stroke-miterlimit:4"> - <g - id="g39"> - <path - d="M 85.277,40.796 C 87.335,48.68 82.61,56.738 74.726,58.795 L 27.143,71.21 C 19.259,73.267 11.2,68.543 9.143,60.658 L 1.695,32.108 C -0.362,24.224 4.362,16.166 12.246,14.109 L 59.83,1.695 c 7.884,-2.057 15.942,2.667 17.999,10.551 l 7.449,28.55 z" - id="path41" - style="stroke:#bababa" - inkscape:connector-curvature="0" /> - <path - d="m 80.444,39.778 c 1.749,7.854 -1.816,13.621 -9.504,15.447 L 28.704,66.245 C 21.135,68.641 14.615,65.064 12.866,57.409 L 6.53,33.127 C 4.781,24.982 7.239,20.238 16.033,17.68 L 58.27,6.661 c 8.144,-1.826 14.089,1.363 15.838,8.835 z" - id="path43" - style="fill:#000000;stroke:none" - inkscape:connector-curvature="0" /> - </g> - <path - d="M 45.542,51.793 24.104,31.102 62.204,26.709 Z" - id="path45" - style="opacity:0.5;fill:none;stroke:#ffffff" - inkscape:connector-curvature="0" /> - <path - d="m 72.325,28.769 c 0.405,1.55 -0.525,3.136 -2.075,3.541 l -12.331,3.217 c -1.551,0.404 -3.137,-0.525 -3.542,-2.076 L 52.082,24.65 c -0.405,-1.551 0.524,-3.137 2.076,-3.542 l 12.33,-3.217 c 1.551,-0.405 3.137,0.525 3.542,2.076 l 2.295,8.801 z" - id="path47" - inkscape:connector-curvature="0" /> - <path - d="m 36.51,33.625 c 0.496,1.9 -0.645,3.844 -2.545,4.34 l -15.112,3.943 c -1.901,0.496 -3.845,-0.644 -4.34,-2.544 L 11.699,28.578 c -0.496,-1.901 0.644,-3.844 2.544,-4.34 l 15.113,-3.942 c 1.901,-0.496 3.845,0.643 4.34,2.544 l 2.814,10.786 z" - id="path49" - inkscape:connector-curvature="0" /> - <path - d="m 52.493,53.208 c 0.278,1.065 -0.36,2.154 -1.425,2.432 L 42.6,57.848 c -1.064,0.277 -2.153,-0.36 -2.431,-1.426 l -1.577,-6.043 c -0.277,-1.064 0.36,-2.153 1.425,-2.432 l 8.468,-2.209 c 1.064,-0.277 2.154,0.361 2.431,1.426 l 1.577,6.043 z" - id="path51" - inkscape:connector-curvature="0" /> - </g> -</svg> +<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>freedesktop.org icon \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc new file mode 100644 index 0000000000..61e2b618cb --- /dev/null +++ b/share/icons/icons.qrc @@ -0,0 +1,152 @@ + + + + application/index.theme + + application/256x256/apps/keepassxc.png + + application/scalable/actions/application-exit.svg + application/scalable/actions/auto-type.svg + application/scalable/actions/bugreport.svg + application/scalable/actions/chronometer.svg + application/scalable/actions/configure.svg + application/scalable/actions/database-change-key.svg + application/scalable/actions/database-lock.svg + application/scalable/actions/database-merge.svg + application/scalable/actions/dialog-close.svg + application/scalable/actions/dialog-ok.svg + application/scalable/actions/document-close.svg + application/scalable/actions/document-edit.svg + application/scalable/actions/document-new.svg + application/scalable/actions/document-open.svg + application/scalable/actions/document-properties.svg + application/scalable/actions/document-save.svg + application/scalable/actions/document-save-as.svg + application/scalable/actions/donate.svg + application/scalable/actions/edit-clear-locationbar-ltr.svg + application/scalable/actions/edit-clear-locationbar-rtl.svg + application/scalable/actions/entry-clone.svg + application/scalable/actions/entry-delete.svg + application/scalable/actions/entry-edit.svg + application/scalable/actions/entry-new.svg + application/scalable/actions/favicon-download.svg + application/scalable/actions/getting-started.svg + application/scalable/actions/group-delete.svg + application/scalable/actions/group-edit.svg + application/scalable/actions/group-empty-trash.svg + application/scalable/actions/group-new.svg + application/scalable/actions/health.svg + application/scalable/actions/help-about.svg + application/scalable/actions/key-enter.svg + application/scalable/actions/keyboard-shortcuts.svg + application/scalable/actions/message-close.svg + application/scalable/actions/object-locked.svg + application/scalable/actions/object-unlocked.svg + application/scalable/actions/paperclip.svg + application/scalable/actions/password-copy.svg + application/scalable/actions/password-generate.svg + application/scalable/actions/password-generator.svg + application/scalable/actions/password-show-off.svg + application/scalable/actions/password-show-on.svg + application/scalable/actions/sort-alphabetical-ascending.svg + application/scalable/actions/sort-alphabetical-descending.svg + application/scalable/actions/statistics.svg + application/scalable/actions/system-help.svg + application/scalable/actions/system-search.svg + application/scalable/actions/system-software-update.svg + application/scalable/actions/url-copy.svg + application/scalable/actions/user-guide.svg + application/scalable/actions/username-copy.svg + application/scalable/actions/view-history.svg + application/scalable/actions/web.svg + + application/scalable/apps/freedesktop.svg + application/scalable/apps/internet-web-browser.svg + application/scalable/apps/keepassxc.svg + application/scalable/apps/keepassxc-dark.svg + application/scalable/apps/keepassxc-locked.svg + application/scalable/apps/keepassxc-unlocked.svg + application/scalable/apps/preferences-desktop-icons.svg + application/scalable/apps/preferences-system-network-sharing.svg + application/scalable/apps/utilities-terminal.svg + + application/scalable/categories/preferences-other.svg + + application/scalable/mimetypes/application-x-keepassxc.svg + + application/scalable/status/dialog-error.svg + application/scalable/status/dialog-information.svg + application/scalable/status/dialog-warning.svg + application/scalable/status/security-high.svg + + database/C00_Password.png + database/C01_Package_Network.png + database/C02_MessageBox_Warning.png + database/C03_Server.png + database/C04_Klipper.png + database/C05_Edu_Languages.png + database/C06_KCMDF.png + database/C07_Kate.png + database/C08_Socket.png + database/C09_Identity.png + database/C10_Kontact.png + database/C11_Camera.png + database/C12_IRKickFlash.png + database/C13_KGPG_Key3.png + database/C14_Laptop_Power.png + database/C15_Scanner.png + database/C16_Mozilla_Firebird.png + database/C17_CDROM_Unmount.png + database/C18_Display.png + database/C19_Mail_Generic.png + database/C20_Misc.png + database/C21_KOrganizer.png + database/C22_ASCII.png + database/C23_Icons.png + database/C24_Connect_Established.png + database/C25_Folder_Mail.png + database/C26_FileSave.png + database/C27_NFS_Unmount.png + database/C28_QuickTime.png + database/C29_KGPG_Term.png + database/C30_Konsole.png + database/C31_FilePrint.png + database/C32_FSView.png + database/C33_Run.png + database/C34_Configure.png + database/C35_KRFB.png + database/C36_Ark.png + database/C37_KPercentage.png + database/C38_Samba_Unmount.png + database/C39_History.png + database/C40_Mail_Find.png + database/C41_VectorGfx.png + database/C42_KCMMemory.png + database/C43_EditTrash.png + database/C44_KNotes.png + database/C45_Cancel.png + database/C46_Help.png + database/C47_KPackage.png + database/C48_Folder.png + database/C49_Folder_Blue_Open.png + database/C50_Folder_Tar.png + database/C51_Decrypted.png + database/C52_Encrypted.png + database/C53_Apply.png + database/C54_Signature.png + database/C55_Thumbnail.png + database/C56_KAddressBook.png + database/C57_View_Text.png + database/C58_KGPG.png + database/C59_Package_Development.png + database/C60_KFM_Home.png + database/C61_Services.png + database/C62_Tux.png + database/C63_Feather.png + database/C64_Apple.png + database/C65_W.png + database/C66_Money.png + database/C67_Certificate.png + database/C68_BlackBerry.png + + diff --git a/share/wizard/wizard.qrc b/share/wizard/wizard.qrc new file mode 100644 index 0000000000..b2b5688156 --- /dev/null +++ b/share/wizard/wizard.qrc @@ -0,0 +1,6 @@ + + + + background-pixmap.png + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 327f088be1..e9e9a73345 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,7 +40,6 @@ set(keepassx_SOURCES core/EntryAttachments.cpp core/EntryAttributes.cpp core/EntrySearcher.cpp - core/FilePath.cpp core/FileWatcher.cpp core/Group.cpp core/HibpOffline.cpp @@ -50,6 +49,7 @@ set(keepassx_SOURCES core/PasswordGenerator.cpp core/PasswordHealth.cpp core/PassphraseGenerator.cpp + core/Resources.cpp core/SignalMultiplexer.cpp core/ScreenLockListener.cpp core/ScreenLockListenerPrivate.cpp @@ -210,6 +210,10 @@ if(MINGW OR (UNIX AND NOT APPLE)) core/OSEventFilter.cpp) endif() +set(keepassx_SOURCES ${keepassx_SOURCES} + ../share/icons/icons.qrc + ../share/wizard/wizard.qrc) + set(keepassx_SOURCES_MAINEXE main.cpp) add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index fa7537373e..76f465f1d8 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -31,9 +31,9 @@ #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" -#include "core/FilePath.h" #include "core/Group.h" #include "core/ListDeleter.h" +#include "core/Resources.h" #include "core/Tools.h" #include "gui/MessageBox.h" @@ -63,7 +63,7 @@ AutoType::AutoType(QObject* parent, bool test) pluginName += "test"; } - QString pluginPath = filePath()->pluginPath(pluginName); + QString pluginPath = resources()->pluginPath(pluginName); if (!pluginPath.isEmpty()) { #ifdef WITH_XC_AUTOTYPE diff --git a/src/autotype/AutoTypeSelectDialog.cpp b/src/autotype/AutoTypeSelectDialog.cpp index 997858f0d0..717b1eb5f5 100644 --- a/src/autotype/AutoTypeSelectDialog.cpp +++ b/src/autotype/AutoTypeSelectDialog.cpp @@ -34,7 +34,7 @@ #include "autotype/AutoTypeSelectView.h" #include "core/AutoTypeMatch.h" #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/entry/AutoTypeMatchModel.h" AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) @@ -49,7 +49,7 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) setAttribute(Qt::WA_X11BypassTransientForHint); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); setWindowTitle(tr("Auto-Type - KeePassXC")); - setWindowIcon(filePath()->applicationIcon()); + setWindowIcon(resources()->applicationIcon()); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) QRect screenGeometry = QApplication::screenAt(QCursor::pos())->availableGeometry(); diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp index f12c42bb7e..8a67d62dad 100644 --- a/src/browser/BrowserOptionDialog.cpp +++ b/src/browser/BrowserOptionDialog.cpp @@ -22,7 +22,7 @@ #include "BrowserSettings.h" #include "config-keepassx.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index c983253f0b..99b9509289 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -70,6 +70,7 @@ namespace Bootstrap #ifdef QT_NO_DEBUG disableCoreDumps(); #endif + setupSearchPaths(); applyEarlyQNetworkAccessManagerWorkaround(); Translator::installTranslators(); diff --git a/src/core/DatabaseIcons.cpp b/src/core/DatabaseIcons.cpp index 6219d41f59..70c5c19c9e 100644 --- a/src/core/DatabaseIcons.cpp +++ b/src/core/DatabaseIcons.cpp @@ -17,7 +17,7 @@ #include "DatabaseIcons.h" -#include "core/FilePath.h" +#include "core/Resources.h" DatabaseIcons* DatabaseIcons::m_instance(nullptr); const int DatabaseIcons::IconCount(69); @@ -103,18 +103,15 @@ QImage DatabaseIcons::icon(int index) { if (index < 0 || index >= IconCount) { qWarning("DatabaseIcons::icon: invalid icon index %d", index); - return QImage(); + return {}; } if (!m_iconCache[index].isNull()) { return m_iconCache[index]; - } else { - QString iconPath = QString("icons/database/").append(m_indexToName[index]); - QImage icon(filePath()->dataPath(iconPath)); - - m_iconCache[index] = icon; - return icon; } + QImage icon(QStringLiteral(":/icons/database/").append(m_indexToName[index])); + m_iconCache[index] = icon; + return icon; } QPixmap DatabaseIcons::iconPixmap(int index) diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp deleted file mode 100644 index 6ae52bd8f3..0000000000 --- a/src/core/FilePath.cpp +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (C) 2017 KeePassXC Team - * Copyright (C) 2011 Felix Geyer - * - * 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 . - */ - -#include "FilePath.h" - -#include -#include -#include -#include - -#include "config-keepassx.h" -#include "core/Config.h" -#include "core/Global.h" -#include "gui/MainWindow.h" - -FilePath* FilePath::m_instance(nullptr); - -QString FilePath::dataPath(const QString& name) -{ - if (name.isEmpty() || name.startsWith('/')) { - return m_dataPath + name; - } else { - return m_dataPath + "/" + name; - } -} - -QString FilePath::pluginPath(const QString& name) -{ - QStringList pluginPaths; - - QDir buildDir(QCoreApplication::applicationDirPath() + "/autotype"); - const QStringList buildDirEntryList = buildDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString& dir : buildDirEntryList) { - pluginPaths << QCoreApplication::applicationDirPath() + "/autotype/" + dir; - } - - // for TestAutoType - pluginPaths << QCoreApplication::applicationDirPath() + "/../src/autotype/test"; - -#if defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE) - pluginPaths << QCoreApplication::applicationDirPath() + "/../PlugIns"; -#endif - - pluginPaths << QCoreApplication::applicationDirPath(); - - QString configuredPluginDir = KEEPASSX_PLUGIN_DIR; - if (configuredPluginDir != ".") { - if (QDir(configuredPluginDir).isAbsolute()) { - pluginPaths << configuredPluginDir; - } else { - QString relativePluginDir = - QString("%1/../%2").arg(QCoreApplication::applicationDirPath(), configuredPluginDir); - pluginPaths << QDir(relativePluginDir).canonicalPath(); - - QString absolutePluginDir = QString("%1/%2").arg(KEEPASSX_PREFIX_DIR, configuredPluginDir); - pluginPaths << QDir(absolutePluginDir).canonicalPath(); - } - } - - QStringList dirFilter; - dirFilter << QString("*%1*").arg(name); - - for (const QString& path : asConst(pluginPaths)) { - const QStringList fileCandidates = QDir(path).entryList(dirFilter, QDir::Files); - - for (const QString& file : fileCandidates) { - QString filePath = path + "/" + file; - - if (QLibrary::isLibrary(filePath)) { - return filePath; - } - } - } - - return QString(); -} - -QString FilePath::wordlistPath(const QString& name) -{ - return dataPath("wordlists/" + name); -} - -QIcon FilePath::applicationIcon() -{ - return icon("apps", "keepassxc", false); -} - -QIcon FilePath::trayIcon() -{ - return useDarkIcon() ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc", false); -} - -QIcon FilePath::trayIconLocked() -{ - return icon("apps", "keepassxc-locked", false); -} - -QIcon FilePath::trayIconUnlocked() -{ - return useDarkIcon() ? icon("apps", "keepassxc-dark", false) : icon("apps", "keepassxc-unlocked", false); -} - -QIcon FilePath::icon(const QString& category, const QString& name, bool recolor) -{ - QString combinedName = category + "/" + name; - QIcon icon = m_iconCache.value(combinedName); - - if (!icon.isNull()) { - return icon; - } - - if (icon.isNull()) { - const QList pngSizes = {16, 22, 24, 32, 48, 64, 128}; - QString filename; - for (int size : pngSizes) { - filename = - QString("%1/icons/application/%2x%2/%3.png").arg(m_dataPath, QString::number(size), combinedName); - if (QFile::exists(filename)) { - icon.addFile(filename, QSize(size, size)); - } - } - filename = QString("%1/icons/application/scalable/%2.svg").arg(m_dataPath, combinedName); - if (QFile::exists(filename) && getMainWindow() && recolor) { - QPalette palette = getMainWindow()->palette(); - - QFile f(filename); - QIcon scalable(filename); - QPixmap pixmap = scalable.pixmap({128, 128}); - - auto mask = QBitmap::fromImage(pixmap.toImage().createAlphaMask()); - pixmap.fill(palette.color(QPalette::WindowText)); - pixmap.setMask(mask); - icon.addPixmap(pixmap, QIcon::Mode::Normal); - - pixmap.fill(palette.color(QPalette::HighlightedText)); - pixmap.setMask(mask); - icon.addPixmap(pixmap, QIcon::Mode::Selected); - - pixmap.fill(palette.color(QPalette::Disabled, QPalette::WindowText)); - pixmap.setMask(mask); - icon.addPixmap(pixmap, QIcon::Mode::Disabled); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) - icon.setIsMask(true); -#endif - } else if (QFile::exists(filename)) { - icon.addFile(filename); - } - } - - m_iconCache.insert(combinedName, icon); - - return icon; -} - -QIcon FilePath::onOffIcon(const QString& category, const QString& name, bool recolor) -{ - QString combinedName = category + "/" + name; - QString cacheName = "onoff/" + combinedName; - - QIcon icon = m_iconCache.value(cacheName); - - if (!icon.isNull()) { - return icon; - } - - QIcon on = FilePath::icon(category, name + "-on", recolor); - for (const auto& size : on.availableSizes()) { - icon.addPixmap(on.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::On); - icon.addPixmap(on.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::On); - icon.addPixmap(on.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::On); - } - QIcon off = FilePath::icon(category, name + "-off", recolor); - for (const auto& size : off.availableSizes()) { - icon.addPixmap(off.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::Off); - icon.addPixmap(off.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::Off); - icon.addPixmap(off.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::Off); - } - - m_iconCache.insert(cacheName, icon); - - return icon; -} - -FilePath::FilePath() -{ - const QString appDirPath = QCoreApplication::applicationDirPath(); - bool isDataDirAbsolute = QDir::isAbsolutePath(KEEPASSX_DATA_DIR); - Q_UNUSED(isDataDirAbsolute); - - if (false) { - } -#ifdef QT_DEBUG - else if (testSetDir(QString(KEEPASSX_SOURCE_DIR) + "/share")) { - } -#endif -#if defined(Q_OS_UNIX) && !(defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE)) - else if (isDataDirAbsolute && testSetDir(KEEPASSX_DATA_DIR)) { - } else if (!isDataDirAbsolute && testSetDir(QString("%1/../%2").arg(appDirPath, KEEPASSX_DATA_DIR))) { - } else if (!isDataDirAbsolute && testSetDir(QString("%1/%2").arg(KEEPASSX_PREFIX_DIR, KEEPASSX_DATA_DIR))) { - } -#endif -#if defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE) - else if (testSetDir(appDirPath + "/../Resources")) { - } -#endif -#ifdef Q_OS_WIN - else if (testSetDir(appDirPath + "/share")) { - } -#endif - // Last ditch test when running in the build directory (mainly for travis tests) - else if (testSetDir(QString(KEEPASSX_SOURCE_DIR) + "/share")) { - } - - if (m_dataPath.isEmpty()) { - qWarning("FilePath::DataPath: can't find data dir"); - } else { - m_dataPath = QDir::cleanPath(m_dataPath); - } -} - -bool FilePath::testSetDir(const QString& dir) -{ - if (QFile::exists(dir + "/icons/database/C00_Password.png")) { - m_dataPath = dir; - return true; - } else { - return false; - } -} - -bool FilePath::useDarkIcon() -{ - return config()->get("GUI/DarkTrayIcon").toBool(); -} - -FilePath* FilePath::instance() -{ - if (!m_instance) { - m_instance = new FilePath(); - } - - return m_instance; -} diff --git a/src/core/PassphraseGenerator.cpp b/src/core/PassphraseGenerator.cpp index b14886a1a8..57dd2bb575 100644 --- a/src/core/PassphraseGenerator.cpp +++ b/src/core/PassphraseGenerator.cpp @@ -21,7 +21,7 @@ #include #include -#include "core/FilePath.h" +#include "core/Resources.h" #include "crypto/Random.h" const char* PassphraseGenerator::DefaultSeparator = " "; @@ -80,7 +80,7 @@ void PassphraseGenerator::setWordList(const QString& path) void PassphraseGenerator::setDefaultWordList() { - const QString path = filePath()->wordlistPath(PassphraseGenerator::DefaultWordList); + const QString path = resources()->wordlistPath(PassphraseGenerator::DefaultWordList); setWordList(path); } diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp new file mode 100644 index 0000000000..d6c57d7ae3 --- /dev/null +++ b/src/core/Resources.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2011 Felix Geyer + * + * 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 . + */ + +#include "Resources.h" + +#include +#include +#include +#include + +#include "config-keepassx.h" +#include "core/Config.h" +#include "core/Global.h" +#include "gui/MainWindow.h" + +Resources* Resources::m_instance(nullptr); + +QString Resources::dataPath(const QString& name) +{ + if (name.isEmpty() || name.startsWith('/')) { + return m_dataPath + name; + } + return m_dataPath + "/" + name; +} + +QString Resources::pluginPath(const QString& name) +{ + QStringList pluginPaths; + + QDir buildDir(QCoreApplication::applicationDirPath() + "/autotype"); + const QStringList buildDirEntryList = buildDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& dir : buildDirEntryList) { + pluginPaths << QCoreApplication::applicationDirPath() + "/autotype/" + dir; + } + + // for TestAutoType + pluginPaths << QCoreApplication::applicationDirPath() + "/../src/autotype/test"; + +#if defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE) + pluginPaths << QCoreApplication::applicationDirPath() + "/../PlugIns"; +#endif + + pluginPaths << QCoreApplication::applicationDirPath(); + + QString configuredPluginDir = KEEPASSX_PLUGIN_DIR; + if (configuredPluginDir != ".") { + if (QDir(configuredPluginDir).isAbsolute()) { + pluginPaths << configuredPluginDir; + } else { + QString relativePluginDir = + QStringLiteral("%1/../%2").arg(QCoreApplication::applicationDirPath(), configuredPluginDir); + pluginPaths << QDir(relativePluginDir).canonicalPath(); + + QString absolutePluginDir = QStringLiteral("%1/%2").arg(KEEPASSX_PREFIX_DIR, configuredPluginDir); + pluginPaths << QDir(absolutePluginDir).canonicalPath(); + } + } + + QStringList dirFilter; + dirFilter << QStringLiteral("*%1*").arg(name); + + for (const QString& path : asConst(pluginPaths)) { + const QStringList fileCandidates = QDir(path).entryList(dirFilter, QDir::Files); + + for (const QString& file : fileCandidates) { + QString filePath = path + "/" + file; + + if (QLibrary::isLibrary(filePath)) { + return filePath; + } + } + } + + return {}; +} + +QString Resources::wordlistPath(const QString& name) +{ + return dataPath(QStringLiteral("wordlists/%1").arg(name)); +} + +QIcon Resources::applicationIcon() +{ + return icon("keepassxc", false); +} + +QIcon Resources::trayIcon() +{ + return useDarkIcon() ? icon("keepassxc-dark", false) : icon("keepassxc", false); +} + +QIcon Resources::trayIconLocked() +{ + return icon("keepassxc-locked", false); +} + +QIcon Resources::trayIconUnlocked() +{ + return useDarkIcon() ? icon("keepassxc-dark", false) : icon("keepassxc-unlocked", false); +} + +QIcon Resources::icon(const QString& name, bool recolor) +{ + QIcon icon = m_iconCache.value(name); + + if (!icon.isNull()) { + return icon; + } + + icon = QIcon::fromTheme(name); + if (getMainWindow() && recolor) { + QPixmap pixmap = icon.pixmap(128, 128); + icon = {}; + + QPalette palette = getMainWindow()->palette(); + + auto mask = QBitmap::fromImage(pixmap.toImage().createAlphaMask()); + pixmap.fill(palette.color(QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Normal); + + pixmap.fill(palette.color(QPalette::HighlightedText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Selected); + + pixmap.fill(palette.color(QPalette::Disabled, QPalette::WindowText)); + pixmap.setMask(mask); + icon.addPixmap(pixmap, QIcon::Mode::Disabled); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) + icon.setIsMask(true); +#endif + } + + m_iconCache.insert(name, icon); + + return icon; +} + +QIcon Resources::onOffIcon(const QString& name, bool recolor) +{ + QString cacheName = "onoff/" + name; + + QIcon icon = m_iconCache.value(cacheName); + + if (!icon.isNull()) { + return icon; + } + + QIcon on = Resources::icon(name + "-on", recolor); + for (const auto& size : on.availableSizes()) { + icon.addPixmap(on.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::On); + icon.addPixmap(on.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::On); + icon.addPixmap(on.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::On); + } + QIcon off = Resources::icon(name + "-off", recolor); + for (const auto& size : off.availableSizes()) { + icon.addPixmap(off.pixmap(size, QIcon::Mode::Normal), QIcon::Mode::Normal, QIcon::Off); + icon.addPixmap(off.pixmap(size, QIcon::Mode::Selected), QIcon::Mode::Selected, QIcon::Off); + icon.addPixmap(off.pixmap(size, QIcon::Mode::Disabled), QIcon::Mode::Disabled, QIcon::Off); + } + + m_iconCache.insert(cacheName, icon); + + return icon; +} + +Resources::Resources() +{ + const QString appDirPath = QCoreApplication::applicationDirPath(); +#if defined(Q_OS_UNIX) && !(defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE)) + testResourceDir(KEEPASSX_DATA_DIR) || testResourceDir(QStringLiteral("%1/../%2").arg(appDirPath, KEEPASSX_DATA_DIR)) + || testResourceDir(QStringLiteral("%1/%2").arg(KEEPASSX_PREFIX_DIR, KEEPASSX_DATA_DIR)); +#elif defined(Q_OS_MACOS) && defined(WITH_APP_BUNDLE) + testResourceDir(appDirPath + QStringLiteral("/../Resources")); +#elif defined(Q_OS_WIN) + testResourceDir(appDirPath + QStringLiteral("/share")); +#endif + + if (m_dataPath.isEmpty()) { + // Last ditch check if we are running from inside the src or test build directory + testResourceDir(appDirPath + QStringLiteral("/../../share")) + || testResourceDir(appDirPath + QStringLiteral("/../share")) + || testResourceDir(appDirPath + QStringLiteral("/../../../share")); + } + + if (m_dataPath.isEmpty()) { + qWarning("Resources::DataPath: can't find data dir"); + } +} + +bool Resources::testResourceDir(const QString& dir) +{ + if (QFile::exists(dir + QStringLiteral("/icons/application/256x256/apps/keepassxc.png"))) { + m_dataPath = QDir::cleanPath(dir); + return true; + } + return false; +} + +bool Resources::useDarkIcon() +{ + return config()->get("GUI/DarkTrayIcon").toBool(); +} + +Resources* Resources::instance() +{ + if (!m_instance) { + m_instance = new Resources(); + + Q_INIT_RESOURCE(icons); + QIcon::setThemeSearchPaths({":/icons"}); + QIcon::setThemeName("application"); + } + + return m_instance; +} diff --git a/src/core/FilePath.h b/src/core/Resources.h similarity index 68% rename from src/core/FilePath.h rename to src/core/Resources.h index 008dfc33e9..1f506b78e4 100644 --- a/src/core/FilePath.h +++ b/src/core/Resources.h @@ -1,4 +1,5 @@ /* + * Copyright (C) 2020 KeePassXC Team * Copyright (C) 2011 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -15,14 +16,14 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_FILEPATH_H -#define KEEPASSX_FILEPATH_H +#ifndef KEEPASSX_RESOURCES_H +#define KEEPASSX_RESOURCES_H #include #include #include -class FilePath +class Resources { public: QString dataPath(const QString& name); @@ -32,27 +33,27 @@ class FilePath QIcon trayIcon(); QIcon trayIconLocked(); QIcon trayIconUnlocked(); - QIcon icon(const QString& category, const QString& name, bool recolor = true); - QIcon onOffIcon(const QString& category, const QString& name, bool recolor = true); + QIcon icon(const QString& name, bool recolor = true); + QIcon onOffIcon(const QString& name, bool recolor = true); - static FilePath* instance(); + static Resources* instance(); private: - FilePath(); - bool testSetDir(const QString& dir); + Resources(); + bool testResourceDir(const QString& dir); bool useDarkIcon(); - static FilePath* m_instance; + static Resources* m_instance; QString m_dataPath; QHash m_iconCache; - Q_DISABLE_COPY(FilePath) + Q_DISABLE_COPY(Resources) }; -inline FilePath* filePath() +inline Resources* resources() { - return FilePath::instance(); + return Resources::instance(); } -#endif // KEEPASSX_FILEPATH_H +#endif // KEEPASSX_RESOURCES_H diff --git a/src/core/Translator.cpp b/src/core/Translator.cpp index 4e3f568cbe..28dc6aa683 100644 --- a/src/core/Translator.cpp +++ b/src/core/Translator.cpp @@ -27,7 +27,7 @@ #include "config-keepassx.h" #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" /** * Install all KeePassXC and Qt translators. @@ -53,7 +53,7 @@ void Translator::installTranslators() #ifdef QT_DEBUG QString("%1/share/translations").arg(KEEPASSX_BINARY_DIR), #endif - filePath()->dataPath("translations")}; + resources()->dataPath("translations")}; bool translationsLoaded = false; for (const QString& path : paths) { @@ -121,7 +121,7 @@ QList> Translator::availableLanguages() #ifdef QT_DEBUG QString("%1/share/translations").arg(KEEPASSX_BINARY_DIR), #endif - filePath()->dataPath("translations")}; + resources()->dataPath("translations")}; QList> languages; languages.append(QPair("system", "System default")); diff --git a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp index afd888ed27..737c558d37 100644 --- a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp +++ b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp @@ -19,7 +19,7 @@ #include "fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h" -#include "core/FilePath.h" +#include "core/Resources.h" QString DatabaseSettingsPageFdoSecrets::name() { @@ -28,7 +28,7 @@ QString DatabaseSettingsPageFdoSecrets::name() QIcon DatabaseSettingsPageFdoSecrets::icon() { - return filePath()->icon(QStringLiteral("apps"), QStringLiteral("freedesktop")); + return resources()->icon(QStringLiteral("freedesktop")); } QWidget* DatabaseSettingsPageFdoSecrets::createWidget() diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h index 4f284f4692..674c3c3b83 100644 --- a/src/fdosecrets/FdoSecretsPlugin.h +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -18,7 +18,7 @@ #ifndef KEEPASSXC_FDOSECRETSPLUGIN_H #define KEEPASSXC_FDOSECRETSPLUGIN_H -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/ApplicationSettingsWidget.h" #include @@ -45,7 +45,7 @@ class FdoSecretsPlugin : public QObject, public ISettingsPage QIcon icon() override { - return FilePath::instance()->icon("apps", "freedesktop"); + return Resources::instance()->icon("freedesktop"); } QWidget* createWidget() override; diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index 2921182c5f..a8e513e29b 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -24,7 +24,7 @@ #include "core/Database.h" #include "core/DatabaseIcons.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/DatabaseTabWidget.h" #include "gui/DatabaseWidget.h" @@ -130,7 +130,7 @@ namespace FdoSecrets case Qt::DisplayRole: return tr("Unlock to show"); case Qt::DecorationRole: - return filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked")); + return resources()->icon(QStringLiteral("object-locked")); case Qt::FontRole: { QFont font; font.setItalic(true); @@ -165,7 +165,7 @@ namespace FdoSecrets case Qt::DisplayRole: return tr("None"); case Qt::DecorationRole: - return filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none")); + return resources()->icon(QStringLiteral("paint-none")); default: return {}; } diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 838f6d20fc..7fb971c0e5 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -23,7 +23,7 @@ #include "fdosecrets/objects/Session.h" #include "fdosecrets/widgets/SettingsModels.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/DatabaseWidget.h" #include @@ -63,7 +63,7 @@ namespace // db settings m_dbSettingsAct = new QAction(tr("Database settings"), this); - m_dbSettingsAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("document-edit"))); + m_dbSettingsAct->setIcon(resources()->icon(QStringLiteral("document-edit"))); m_dbSettingsAct->setToolTip(tr("Edit database settings")); m_dbSettingsAct->setEnabled(false); connect(m_dbSettingsAct, &QAction::triggered, this, [this]() { @@ -77,7 +77,7 @@ namespace // unlock/lock m_lockAct = new QAction(tr("Unlock database"), this); - m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"))); + m_lockAct->setIcon(resources()->icon(QStringLiteral("object-locked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); connect(m_lockAct, &QAction::triggered, this, [this]() { if (!m_dbWidget) { @@ -135,13 +135,13 @@ namespace } connect(m_dbWidget, &DatabaseWidget::databaseLocked, this, [this]() { m_lockAct->setText(tr("Unlock database")); - m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"))); + m_lockAct->setIcon(resources()->icon(QStringLiteral("object-locked"))); m_lockAct->setToolTip(tr("Unlock database to show more information")); m_dbSettingsAct->setEnabled(false); }); connect(m_dbWidget, &DatabaseWidget::databaseUnlocked, this, [this]() { m_lockAct->setText(tr("Lock database")); - m_lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"))); + m_lockAct->setIcon(resources()->icon(QStringLiteral("object-unlocked"))); m_lockAct->setToolTip(tr("Lock database")); m_dbSettingsAct->setEnabled(true); }); @@ -174,7 +174,7 @@ namespace addWidget(spacer); m_disconnectAct = new QAction(tr("Disconnect"), this); - m_disconnectAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("dialog-close"))); + m_disconnectAct->setIcon(resources()->icon(QStringLiteral("dialog-close"))); m_disconnectAct->setToolTip(tr("Disconnect this application")); connect(m_disconnectAct, &QAction::triggered, this, [this]() { if (m_session) { diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index bd24cf165b..f9b85ac63b 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" @@ -207,7 +207,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); diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index b17c44ecc1..3424a46fb1 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -24,8 +24,8 @@ #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 "MessageBox.h" @@ -91,8 +91,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); diff --git a/src/gui/CloneDialog.cpp b/src/gui/CloneDialog.cpp index e91df62c73..2441b3f170 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/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index c58b2df40d..31391b12b4 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" @@ -61,14 +61,14 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) 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")); + m_ui->keyFileClearIcon->setIcon(resources()->icon("edit-clear-locationbar-rtl")); m_ui->keyFileClearIcon->setVisible(false); connect(m_ui->keyFileClearIcon, SIGNAL(triggered(bool)), SLOT(clearKeyFileEdit())); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 6434ba9236..e6cc9f309e 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -36,11 +36,11 @@ #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" diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index f9bcbb5af6..68a8d7d4af 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -22,7 +22,7 @@ #include #include -#include "core/FilePath.h" +#include "core/Resources.h" EditWidget::EditWidget(QWidget* parent) : DialogyWidget(parent) diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 2e2e37dbc3..08627ac3cb 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -24,7 +24,7 @@ #include #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); @@ -78,7 +78,7 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent) connect(&m_totpTimer, SIGNAL(timeout()), SLOT(updateTotpLabel())); // 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); diff --git a/src/gui/KMessageWidget.cpp b/src/gui/KMessageWidget.cpp index 01925b7dd8..5e11b354c2 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 @@ -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")); + closeAction->setIcon(Resources::instance()->icon("message-close")); QObject::connect(closeAction, SIGNAL(triggered(bool)), q, SLOT(animatedHide())); diff --git a/src/gui/LineEdit.cpp b/src/gui/LineEdit.cpp index 98a5c09e86..ec5cb7f9c7 100644 --- a/src/gui/LineEdit.cpp +++ b/src/gui/LineEdit.cpp @@ -22,7 +22,7 @@ #include #include -#include "core/FilePath.h" +#include "core/Resources.h" LineEdit::LineEdit(QWidget* parent) : QLineEdit(parent) @@ -33,7 +33,7 @@ LineEdit::LineEdit(QWidget* parent) QString iconNameDirected = QString("edit-clear-locationbar-").append((layoutDirection() == Qt::LeftToRight) ? "rtl" : "ltr"); - const auto 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 7623a13f8d..485c211259 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -31,9 +31,9 @@ #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" @@ -103,7 +103,7 @@ class BrowserPlugin : public ISettingsPage QIcon icon() override { - return FilePath::instance()->icon("apps", "internet-web-browser"); + return Resources::instance()->icon("internet-web-browser"); } QWidget* createWidget() override @@ -210,7 +210,7 @@ MainWindow::MainWindow() m_ui->settingsWidget->addSettingsPage(fdoSS); #endif - setWindowIcon(filePath()->applicationIcon()); + setWindowIcon(resources()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); // clang-format off connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); @@ -334,48 +334,48 @@ MainWindow::MainWindow() 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->actionReports->setIcon(filePath()->icon("actions", "help-about")); - 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->actionDatabaseMerge->setIcon(filePath()->icon("actions", "database-merge")); - - 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->actionGroupSortAsc->setIcon(filePath()->icon("actions", "sort-alphabetical-ascending")); - m_ui->actionGroupSortDesc->setIcon(filePath()->icon("actions", "sort-alphabetical-descending")); - - 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->actionEntryOpenUrl->setIcon(filePath()->icon("actions", "web")); - 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->actionDonate->setIcon(filePath()->icon("actions", "donate")); - m_ui->actionBugReport->setIcon(filePath()->icon("actions", "bugreport")); - m_ui->actionGettingStarted->setIcon(filePath()->icon("actions", "getting-started")); - m_ui->actionUserGuide->setIcon(filePath()->icon("actions", "user-guide")); - m_ui->actionOnlineHelp->setIcon(filePath()->icon("actions", "system-help")); - m_ui->actionKeyboardShortcuts->setIcon(filePath()->icon("actions", "keyboard-shortcuts")); - 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->actionDatabaseSave->setIcon(resources()->icon("document-save")); + m_ui->actionDatabaseSaveAs->setIcon(resources()->icon("document-save-as")); + m_ui->actionDatabaseClose->setIcon(resources()->icon("document-close")); + m_ui->actionReports->setIcon(resources()->icon("help-about")); + m_ui->actionChangeDatabaseSettings->setIcon(resources()->icon("document-edit")); + m_ui->actionChangeMasterKey->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->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->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))); @@ -894,12 +894,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.pdf"))); } void MainWindow::openUserGuide() { - customOpenUrl(QString("file:///%1").arg(filePath()->dataPath("docs/KeePassXC_UserGuide.pdf"))); + customOpenUrl(QString("file:///%1").arg(resources()->dataPath("docs/KeePassXC_UserGuide.pdf"))); } void MainWindow::openOnlineHelp() @@ -1138,7 +1138,7 @@ void MainWindow::updateTrayIcon() QAction* actionToggle = new QAction(tr("Toggle window"), menu); menu->addAction(actionToggle); - actionToggle->setIcon(filePath()->icon("apps", "keepassxc-dark", false)); + actionToggle->setIcon(resources()->icon("keepassxc-dark", false)); menu->addAction(m_ui->actionLockDatabases); @@ -1158,13 +1158,13 @@ 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()); + m_trayIcon->setIcon(resources()->trayIconUnlocked()); } else { - m_trayIcon->setIcon(filePath()->trayIconLocked()); + m_trayIcon->setIcon(resources()->trayIconLocked()); } } else { if (m_trayIcon) { @@ -1525,7 +1525,7 @@ 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 diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index 9474adb16e..ebe9e34ad7 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -19,7 +19,7 @@ #include "PasswordEdit.h" #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/Application.h" #include "gui/Font.h" #include "gui/PasswordGeneratorWidget.h" @@ -39,12 +39,12 @@ namespace PasswordEdit::PasswordEdit(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("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")); @@ -57,7 +57,7 @@ PasswordEdit::PasswordEdit(QWidget* parent) setFont(passwordFont); m_toggleVisibleAction = new QAction( - filePath()->icon("actions", "password-show-off"), + resources()->icon("password-show"), tr("Toggle Password (%1)").arg(QKeySequence(Qt::CTRL + Qt::Key_H).toString(QKeySequence::NativeText)), nullptr); m_toggleVisibleAction->setCheckable(true); @@ -67,7 +67,7 @@ PasswordEdit::PasswordEdit(QWidget* parent) connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordEdit::setShowPassword); m_passwordGeneratorAction = new QAction( - filePath()->icon("actions", "password-generator"), + 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); @@ -105,7 +105,7 @@ void PasswordEdit::enablePasswordGenerator() void PasswordEdit::setShowPassword(bool show) { setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password); - m_toggleVisibleAction->setIcon(filePath()->icon("actions", show ? "password-show-on" : "password-show-off")); + m_toggleVisibleAction->setIcon(resources()->icon(show ? "password-show-on" : "password-show-off")); m_toggleVisibleAction->setChecked(show); if (m_repeatPasswordEdit) { diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 7ce45b1d1c..1de51ec0f3 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -24,9 +24,9 @@ #include #include "core/Config.h" -#include "core/FilePath.h" #include "core/PasswordGenerator.h" #include "core/PasswordHealth.h" +#include "core/Resources.h" #include "gui/Application.h" #include "gui/Clipboard.h" @@ -38,10 +38,10 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) { m_ui->setupUi(this); - m_ui->buttonGenerate->setIcon(filePath()->icon("actions", "refresh")); + 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(filePath()->icon("actions", "clipboard-text")); + m_ui->buttonCopy->setIcon(resources()->icon("clipboard-text")); m_ui->buttonClose->setShortcut(Qt::Key_Escape); connect(m_ui->editNewPassword, SIGNAL(textChanged(QString)), SLOT(updateButtonsEnabled(QString))); @@ -85,7 +85,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) { @@ -538,7 +538,7 @@ void PasswordGeneratorWidget::updateGenerator() 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/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 001c3d861e..3702266b49 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -25,7 +25,7 @@ #include #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "gui/widgets/PopupHelpWidget.h" SearchWidget::SearchWidget(QWidget* parent) @@ -69,13 +69,13 @@ SearchWidget::SearchWidget(QWidget* parent) m_actionLimitGroup->setCheckable(true); m_actionLimitGroup->setChecked(config()->get("SearchLimitGroup", false).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); diff --git a/src/gui/URLEdit.cpp b/src/gui/URLEdit.cpp index 4dc2a55c27..428a918db5 100644 --- a/src/gui/URLEdit.cpp +++ b/src/gui/URLEdit.cpp @@ -21,7 +21,7 @@ #include #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 2f6d1fc487..db817a74b3 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 35cce553e1..25d261ea7d 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,7 +36,7 @@ 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(); diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index e0e6765a46..d2eece319b 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -34,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 @@ -76,8 +76,8 @@ 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); @@ -100,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 diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 201c9628a5..d526574304 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -41,8 +41,8 @@ #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" -#include "core/FilePath.h" #include "core/Metadata.h" +#include "core/Resources.h" #include "core/TimeDelta.h" #include "core/Tools.h" #ifdef WITH_XC_SSHAGENT @@ -138,7 +138,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); @@ -147,7 +147,7 @@ void EditEntryWidget::setupMain() m_mainUi->usernameComboBox->setCompleter(m_usernameCompleter); #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); @@ -168,7 +168,7 @@ void EditEntryWidget::setupMain() 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); @@ -198,7 +198,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())); } @@ -211,9 +211,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); @@ -252,7 +252,7 @@ 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); + addPage(tr("Browser Integration"), Resources::instance()->icon("internet-web-browser"), m_browserWidget); m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes); m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel); @@ -370,13 +370,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); @@ -521,7 +521,7 @@ 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() diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 9bbf7d56d8..405de9495e 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -26,10 +26,10 @@ #include "core/Config.h" #include "core/DatabaseIcons.h" #include "core/Entry.h" -#include "core/FilePath.h" #include "core/Global.h" #include "core/Group.h" #include "core/Metadata.h" +#include "core/Resources.h" #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" #endif @@ -259,12 +259,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return entry->iconScaledPixmap(); case Paperclip: if (!entry->attachments()->isEmpty()) { - return filePath()->icon("actions", "paperclip"); + return resources()->icon("paperclip"); } break; case Totp: if (entry->hasTotp()) { - return filePath()->icon("actions", "chronometer"); + return resources()->icon("chronometer"); } break; } @@ -336,9 +336,9 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro } else if (role == Qt::DecorationRole) { switch (section) { case Paperclip: - return filePath()->icon("actions", "paperclip"); + return resources()->icon("paperclip"); case Totp: - return filePath()->icon("actions", "chronometer"); + return resources()->icon("chronometer"); } } else if (role == Qt::ToolTipRole) { switch (section) { diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp index 3e6fb839c9..7bf673a99f 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 @@ -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/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 30d8fb9136..8d81156491 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, diff --git a/src/gui/masterkey/PasswordEditWidget.cpp b/src/gui/masterkey/PasswordEditWidget.cpp index 60689e9204..2d355cc10b 100644 --- a/src/gui/masterkey/PasswordEditWidget.cpp +++ b/src/gui/masterkey/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" diff --git a/src/gui/reports/ReportsPageHealthcheck.cpp b/src/gui/reports/ReportsPageHealthcheck.cpp index 41fa406258..1dfe793a60 100644 --- a/src/gui/reports/ReportsPageHealthcheck.cpp +++ b/src/gui/reports/ReportsPageHealthcheck.cpp @@ -18,7 +18,7 @@ #include "ReportsPageHealthcheck.h" #include "ReportsWidgetHealthcheck.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include @@ -34,7 +34,7 @@ QString ReportsPageHealthcheck::name() QIcon ReportsPageHealthcheck::icon() { - return FilePath::instance()->icon("actions", "health"); + return Resources::instance()->icon("health"); } QWidget* ReportsPageHealthcheck::createWidget() diff --git a/src/gui/reports/ReportsPageStatistics.cpp b/src/gui/reports/ReportsPageStatistics.cpp index e4570e172d..90cd338df6 100644 --- a/src/gui/reports/ReportsPageStatistics.cpp +++ b/src/gui/reports/ReportsPageStatistics.cpp @@ -18,7 +18,7 @@ #include "ReportsPageStatistics.h" #include "ReportsWidgetStatistics.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include @@ -29,7 +29,7 @@ QString ReportsPageStatistics::name() QIcon ReportsPageStatistics::icon() { - return FilePath::instance()->icon("actions", "statistics"); + return Resources::instance()->icon("statistics"); } QWidget* ReportsPageStatistics::createWidget() diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index c668b3495d..49370d5f81 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -20,9 +20,9 @@ #include "core/AsyncTask.h" #include "core/Database.h" -#include "core/FilePath.h" #include "core/Group.h" #include "core/PasswordHealth.h" +#include "core/Resources.h" #include #include @@ -102,7 +102,7 @@ Health::Health(QSharedPointer db) ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) : QWidget(parent) , m_ui(new Ui::ReportsWidgetHealthcheck()) - , m_errorIcon(FilePath::instance()->icon("status", "dialog-error")) + , m_errorIcon(Resources::instance()->icon("dialog-error")) { m_ui->setupUi(this); diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp index bc642af786..f5a99b3632 100644 --- a/src/gui/reports/ReportsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -20,10 +20,10 @@ #include "core/AsyncTask.h" #include "core/Database.h" -#include "core/FilePath.h" #include "core/Group.h" #include "core/Metadata.h" #include "core/PasswordHealth.h" +#include "core/Resources.h" #include #include @@ -150,7 +150,7 @@ namespace ReportsWidgetStatistics::ReportsWidgetStatistics(QWidget* parent) : QWidget(parent) , m_ui(new Ui::ReportsWidgetStatistics()) - , m_errIcon(FilePath::instance()->icon("status", "dialog-error")) + , m_errIcon(Resources::instance()->icon("dialog-error")) { m_ui->setupUi(this); diff --git a/src/gui/styles/base/BaseStyle.cpp b/src/gui/styles/base/BaseStyle.cpp index b3e22efc9d..a816e90c9e 100644 --- a/src/gui/styles/base/BaseStyle.cpp +++ b/src/gui/styles/base/BaseStyle.cpp @@ -4160,8 +4160,8 @@ void BaseStyle::polish(QApplication* app) } Q_INIT_RESOURCE(styles); - QString stylesheet; + QString stylesheet; QFile baseStylesheetFile(":/styles/base/basestyle.qss"); if (baseStylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) { stylesheet = baseStylesheetFile.readAll(); diff --git a/src/gui/wizard/NewDatabaseWizard.cpp b/src/gui/wizard/NewDatabaseWizard.cpp index 34c594046f..0e9d6ba1ed 100644 --- a/src/gui/wizard/NewDatabaseWizard.cpp +++ b/src/gui/wizard/NewDatabaseWizard.cpp @@ -21,9 +21,9 @@ #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 @@ -48,7 +48,8 @@ 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")); } NewDatabaseWizard::~NewDatabaseWizard() diff --git a/src/keeshare/DatabaseSettingsPageKeeShare.cpp b/src/keeshare/DatabaseSettingsPageKeeShare.cpp index 12ac2ad9ed..fca32e247e 100644 --- a/src/keeshare/DatabaseSettingsPageKeeShare.cpp +++ b/src/keeshare/DatabaseSettingsPageKeeShare.cpp @@ -18,8 +18,8 @@ #include "DatabaseSettingsPageKeeShare.h" #include "core/Database.h" -#include "core/FilePath.h" #include "core/Group.h" +#include "core/Resources.h" #include "keeshare/DatabaseSettingsWidgetKeeShare.h" #include "keeshare/KeeShare.h" @@ -32,7 +32,7 @@ QString DatabaseSettingsPageKeeShare::name() QIcon DatabaseSettingsPageKeeShare::icon() { - return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); + return Resources::instance()->icon("preferences-system-network-sharing"); } QWidget* DatabaseSettingsPageKeeShare::createWidget() diff --git a/src/keeshare/SettingsPageKeeShare.cpp b/src/keeshare/SettingsPageKeeShare.cpp index 04a0f1058d..a74c4a411d 100644 --- a/src/keeshare/SettingsPageKeeShare.cpp +++ b/src/keeshare/SettingsPageKeeShare.cpp @@ -18,8 +18,8 @@ #include "SettingsPageKeeShare.h" #include "core/Database.h" -#include "core/FilePath.h" #include "core/Group.h" +#include "core/Resources.h" #include "gui/DatabaseTabWidget.h" #include "gui/MessageWidget.h" #include "keeshare/KeeShare.h" @@ -39,7 +39,7 @@ QString SettingsPageKeeShare::name() QIcon SettingsPageKeeShare::icon() { - return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); + return Resources::instance()->icon("preferences-system-network-sharing"); } QWidget* SettingsPageKeeShare::createWidget() diff --git a/src/keeshare/group/EditGroupPageKeeShare.cpp b/src/keeshare/group/EditGroupPageKeeShare.cpp index dbc3e11866..e310da1772 100644 --- a/src/keeshare/group/EditGroupPageKeeShare.cpp +++ b/src/keeshare/group/EditGroupPageKeeShare.cpp @@ -17,7 +17,7 @@ #include "EditGroupPageKeeShare.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "keeshare/group/EditGroupWidgetKeeShare.h" #include @@ -34,7 +34,7 @@ QString EditGroupPageKeeShare::name() QIcon EditGroupPageKeeShare::icon() { - return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); + return Resources::instance()->icon("preferences-system-network-sharing"); } QWidget* EditGroupPageKeeShare::createWidget() diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 30098ef5d4..0ae732df4c 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -20,9 +20,9 @@ #include "core/Config.h" #include "core/CustomData.h" -#include "core/FilePath.h" #include "core/Group.h" #include "core/Metadata.h" +#include "core/Resources.h" #include "crypto/ssh/OpenSSHKey.h" #include "gui/FileDialog.h" #include "keeshare/KeeShare.h" diff --git a/src/sshagent/AgentSettingsPage.cpp b/src/sshagent/AgentSettingsPage.cpp index 1f04d61b1f..eb86f3fce9 100644 --- a/src/sshagent/AgentSettingsPage.cpp +++ b/src/sshagent/AgentSettingsPage.cpp @@ -18,7 +18,7 @@ #include "AgentSettingsPage.h" #include "AgentSettingsWidget.h" -#include "core/FilePath.h" +#include "core/Resources.h" AgentSettingsPage::AgentSettingsPage(DatabaseTabWidget* tabWidget) { @@ -36,7 +36,7 @@ QString AgentSettingsPage::name() QIcon AgentSettingsPage::icon() { - return FilePath::instance()->icon("apps", "utilities-terminal"); + return Resources::instance()->icon("utilities-terminal"); } QWidget* AgentSettingsPage::createWidget() diff --git a/tests/TestAutoType.cpp b/tests/TestAutoType.cpp index 69ae955b34..e2ab5db1c1 100644 --- a/tests/TestAutoType.cpp +++ b/tests/TestAutoType.cpp @@ -25,7 +25,7 @@ #include "autotype/AutoTypePlatformPlugin.h" #include "autotype/test/AutoTypeTestInterface.h" #include "core/Config.h" -#include "core/FilePath.h" +#include "core/Resources.h" #include "crypto/Crypto.h" #include "gui/MessageBox.h" @@ -39,7 +39,7 @@ void TestAutoType::initTestCase() config()->set("security/autotypeask", false); AutoType::createTestInstance(); - QPluginLoader loader(filePath()->pluginPath("keepassx-autotype-test")); + QPluginLoader loader(resources()->pluginPath("keepassx-autotype-test")); loader.setLoadHints(QLibrary::ResolveAllSymbolsHint); QVERIFY(loader.instance()); diff --git a/utils/makeicons.sh b/utils/makeicons.sh index 887874161b..d4a8848e18 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -94,6 +94,7 @@ map() { entry-edit) echo comment-edit-outline ;; entry-new) echo comment-plus-outline ;; favicon-download) echo download ;; + freedesktop) echo freedesktop-dot-org ;; getting-started) echo lightbulb-on-outline ;; group-delete) echo folder-remove-outline ;; group-edit) echo folder-edit-outline ;; From e6c2c7ed93ee019f4113217bbe6e3b8fb2b3f208 Mon Sep 17 00:00:00 2001 From: louib Date: Wed, 18 Mar 2020 21:51:36 -0400 Subject: [PATCH 087/215] CLI: Cleanup create options (#4313) * Add ability to create database with an empty password * Add password repeat check * Standardize process between `db-create` and `import` commands * Improve db-create tests with new password repeat Co-authored-by: Jonathan White --- CHANGELOG.md | 2 + share/docs/man/keepassxc-cli.1 | 8 +- src/cli/Create.cpp | 37 ++++++--- src/cli/Create.h | 2 + src/cli/Import.cpp | 8 +- src/cli/Utils.cpp | 32 +++++++- src/cli/Utils.h | 2 +- tests/TestCli.cpp | 134 ++++++++++++++++++++++++--------- 8 files changed, 171 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76bb5c38bd..6bdcb5bf0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Changed - Renamed CLI create command to db-create [#4231] +- Added --set-password option for CLI db-create command +- Added --set-key-file option for CLI db-create command (replacing --key-file option) ## 2.5.3 (2020-01-19) diff --git a/share/docs/man/keepassxc-cli.1 b/share/docs/man/keepassxc-cli.1 index d1360cd65c..bac9a8d377 100644 --- a/share/docs/man/keepassxc-cli.1 +++ b/share/docs/man/keepassxc-cli.1 @@ -29,7 +29,7 @@ Copies an attribute or the current TOTP (if the \fI-t\fP option is specified) of In interactive mode, closes the currently opened database (see \fIopen\fP). .IP "\fBdb-create\fP [options] " -Creates a new database with a key file and/or password. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. +Creates a new database with a password and/or a key file. The key file will be created if the file that is referred to does not exist. If both the key file and password are empty, no database will be created. .IP "\fBdb-info\fP [options] " Show a database's information. @@ -185,6 +185,12 @@ Will report an error if no TOTP is configured for the entry. .SS "Create options" +.IP "\fB-k\fP, \fB--set-key-file\fP " +Set the key file for the database. + +.IP "\fB-p\fP, \fB--set-password\fP" +Set a password for the database. + .IP "\fB-t\fP, \fB--decryption-time\fP