diff options
author | Jonathan White <support@dmapps.us> | 2021-06-08 06:18:37 +0300 |
---|---|---|
committer | Jonathan White <support@dmapps.us> | 2021-06-08 06:18:37 +0300 |
commit | 2b262c5b2438d8ef8febcde5ede2043a4e00c192 (patch) | |
tree | bf7c1413a9033178e8dfc0bf3c452cc79f5b461e /src | |
parent | 34a78f0ec3a3c030218c71a57c6fa56e6cb0bc44 (diff) | |
parent | ec33474845942e5210d734b9e9e4c6019860aa79 (diff) |
Release 2.6.52.6.5
Added
- Show search bar when toolbar is hidden or in overflow [#6279]
- Show countdown for clipboard clearing in status bar [#6333]
- Command line option to lock all open databases [#6511]
- Allow CSV import of bare TOTP secrets [#6211]
- Retain file creation time when saving database [#6576]
- Set permissions of saved attachments to be private to the current user [#6363]
- OPVault: Use Text instead of Name for attribute names [#6334]
Changed
- Reports: Allow resizing of reports columns [#6435]
- Reports: Toggle showing expired entries [#6534]
- Save Always on Top setting [#6236]
- Password generator can exclude additional lookalike characters (6/G, 8/B) [#6196]
Fixed
- Allow setting MSI properties in unattended install [#6196]
- Update MainWindow minimum size to enable smaller verticle space [#6196]
- Use application font size when setting default or monospace fonts [#6332]
- Fix notes not clearing in entry preview panel in some cases [#6481]
- macOS: Correct window activation when restoring from tray [#6575]
- macOS: Better handling of minimize after unlock when using browser integration [#6338]
- Linux: Start after the system tray is available on LXQt [#6216]
- Linux: Allow selection of modal dialogs on X11 in Auto-Type [#6204]
- KeeShare: prevent crash when file extension is missing [#6174]
Diffstat (limited to 'src')
49 files changed, 518 insertions, 152 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0ceccdd9a..a441b475b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -173,6 +173,7 @@ set(keepassx_SOURCES gui/osutils/OSUtilsBase.cpp gui/settings/SettingsWidget.cpp gui/widgets/ElidedLabel.cpp + gui/widgets/KPToolBar.cpp gui/widgets/PopupHelpWidget.cpp gui/wizard/NewDatabaseWizard.cpp gui/wizard/NewDatabaseWizardPage.cpp diff --git a/src/autotype/AutoTypeSelectDialog.cpp b/src/autotype/AutoTypeSelectDialog.cpp index 3b264b7bc..af11fb8f8 100644 --- a/src/autotype/AutoTypeSelectDialog.cpp +++ b/src/autotype/AutoTypeSelectDialog.cpp @@ -52,18 +52,24 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) setWindowIcon(resources()->applicationIcon()); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - QRect screenGeometry = QApplication::screenAt(QCursor::pos())->availableGeometry(); + auto screen = QApplication::screenAt(QCursor::pos()); + if (!screen) { + // screenAt can return a nullptr, default to the primary screen + screen = QApplication::primaryScreen(); + } + QRect screenGeometry = screen->availableGeometry(); #else QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos()); #endif + + // Resize to last used size QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize(); size.setWidth(qMin(size.width(), screenGeometry.width())); size.setHeight(qMin(size.height(), screenGeometry.height())); resize(size); // move dialog to the center of the screen - QPoint screenCenter = screenGeometry.center(); - move(screenCenter.x() - (size.width() / 2), screenCenter.y() - (size.height() / 2)); + move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2)); QVBoxLayout* layout = new QVBoxLayout(this); diff --git a/src/autotype/xcb/AutoTypeXCB.cpp b/src/autotype/xcb/AutoTypeXCB.cpp index d2d757b4e..2f320f73b 100644 --- a/src/autotype/xcb/AutoTypeXCB.cpp +++ b/src/autotype/xcb/AutoTypeXCB.cpp @@ -39,6 +39,8 @@ AutoTypePlatformX11::AutoTypePlatformX11() m_atomString = XInternAtom(m_dpy, "STRING", True); m_atomUtf8String = XInternAtom(m_dpy, "UTF8_STRING", True); m_atomNetActiveWindow = XInternAtom(m_dpy, "_NET_ACTIVE_WINDOW", True); + m_atomTransientFor = XInternAtom(m_dpy, "WM_TRANSIENT_FOR", True); + m_atomWindow = XInternAtom(m_dpy, "WINDOW", True); m_classBlacklist << "desktop_window" << "gnome-panel"; // Gnome @@ -373,23 +375,31 @@ QStringList AutoTypePlatformX11::windowTitlesRecursive(Window window) bool AutoTypePlatformX11::isTopLevelWindow(Window window) { + bool result = false; + Atom type = None; int format; unsigned long nitems; unsigned long after; - unsigned char* data = Q_NULLPTR; + unsigned char* data = nullptr; + + // Check if the window has WM_STATE atom and it is not Withdrawn int retVal = XGetWindowProperty( m_dpy, window, m_atomWmState, 0, 2, False, m_atomWmState, &type, &format, &nitems, &after, &data); - bool result = false; - if (retVal == 0 && data) { if (type == m_atomWmState && format == 32 && nitems > 0) { - qint32 state = static_cast<qint32>(*data); - result = (state != WithdrawnState); + result = (static_cast<quint32>(*data) != WithdrawnState); } - XFree(data); + } else { + // See if this is a transient window without WM_STATE + retVal = XGetWindowProperty( + m_dpy, window, m_atomTransientFor, 0, 1, False, m_atomWindow, &type, &format, &nitems, &after, &data); + if (retVal == 0 && data) { + result = true; + XFree(data); + } } return result; diff --git a/src/autotype/xcb/AutoTypeXCB.h b/src/autotype/xcb/AutoTypeXCB.h index 221d2ba7b..327359d3c 100644 --- a/src/autotype/xcb/AutoTypeXCB.h +++ b/src/autotype/xcb/AutoTypeXCB.h @@ -77,7 +77,6 @@ private: void updateKeymap(); bool isRemapKeycodeValid(); int AddKeysym(KeySym keysym); - void AddModifier(KeySym keysym); void SendKeyEvent(unsigned keycode, bool press); void SendModifiers(unsigned int mask, bool press); int GetKeycode(KeySym keysym, unsigned int* mask); @@ -93,6 +92,8 @@ private: Atom m_atomString; Atom m_atomUtf8String; Atom m_atomNetActiveWindow; + Atom m_atomTransientFor; + Atom m_atomWindow; QSet<QString> m_classBlacklist; Qt::Key m_currentGlobalKey; Qt::KeyboardModifiers m_currentGlobalModifiers; diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 5e1173df4..703ef9612 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -55,6 +55,7 @@ static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwo const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit"); const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry"); const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth"); +const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth"); // Multiple URL's const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL"); @@ -397,6 +398,11 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, continue; } + if (httpAuth && entry->customData()->contains(BrowserService::OPTION_NOT_HTTP_AUTH) + && entry->customData()->value(BrowserService::OPTION_NOT_HTTP_AUTH) == TRUE_STR) { + continue; + } + // HTTP Basic Auth always needs a confirmation if (!ignoreHttpAuth && httpAuth) { pwEntriesToConfirm.append(entry); diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index f84bf2880..d45556a73 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -90,6 +90,7 @@ public: static const QString OPTION_SKIP_AUTO_SUBMIT; static const QString OPTION_HIDE_ENTRY; static const QString OPTION_ONLY_HTTP_AUTH; + static const QString OPTION_NOT_HTTP_AUTH; static const QString ADDITIONAL_URL; signals: diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 4c00dabbc..9c69ea578 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -92,6 +92,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = { {Config::GUI_HideToolbar, {QS("GUI/HideToolbar"), Roaming, false}}, {Config::GUI_MovableToolbar, {QS("GUI/MovableToolbar"), Roaming, false}}, {Config::GUI_HidePreviewPanel, {QS("GUI/HidePreviewPanel"), Roaming, false}}, + {Config::GUI_AlwaysOnTop, {QS("GUI/GUI_AlwaysOnTop"), Local, false}}, {Config::GUI_ToolButtonStyle, {QS("GUI/ToolButtonStyle"), Roaming, Qt::ToolButtonIconOnly}}, {Config::GUI_ShowTrayIcon, {QS("GUI/ShowTrayIcon"), Roaming, false}}, {Config::GUI_TrayIconAppearance, {QS("GUI/TrayIconAppearance"), Roaming, {}}}, diff --git a/src/core/Config.h b/src/core/Config.h index 423e1ee81..b6a31bae7 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -74,6 +74,7 @@ public: GUI_HideToolbar, GUI_MovableToolbar, GUI_HidePreviewPanel, + GUI_AlwaysOnTop, GUI_ToolButtonStyle, GUI_ShowTrayIcon, GUI_TrayIconAppearance, diff --git a/src/core/Database.cpp b/src/core/Database.cpp index ccd0ffb21..f13c00796 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -272,6 +272,11 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool bool Database::performSave(const QString& filePath, QString* error, bool atomic, bool backup) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + QFileInfo info(filePath); + auto createTime = info.exists() ? info.birthTime() : QDateTime::currentDateTime(); +#endif + if (atomic) { QSaveFile saveFile(filePath); if (saveFile.open(QIODevice::WriteOnly)) { @@ -284,6 +289,11 @@ bool Database::performSave(const QString& filePath, QString* error, bool atomic, backupDatabase(filePath); } +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // Retain orginal creation time + saveFile.setFileTime(createTime, QFile::FileBirthTime); +#endif + if (saveFile.commit()) { // successfully saved database file return true; @@ -318,6 +328,10 @@ bool Database::performSave(const QString& filePath, QString* error, bool atomic, // successfully saved the database tempFile.setAutoRemove(false); QFile::setPermissions(filePath, perms); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + // Retain orginal creation time + tempFile.setFileTime(createTime, QFile::FileBirthTime); +#endif return true; } else if (!backup || !restoreDatabase(filePath)) { // Failed to copy new database in place, and @@ -486,8 +500,9 @@ bool Database::restoreDatabase(const QString& filePath) // Only try to restore if the backup file actually exists if (QFile::exists(backupFilePath)) { QFile::remove(filePath); - return QFile::copy(backupFilePath, filePath); - QFile::setPermissions(filePath, perms); + if (QFile::copy(backupFilePath, filePath)) { + return QFile::setPermissions(filePath, perms); + } } return false; } diff --git a/src/core/MacPasteboard.h b/src/core/MacPasteboard.h index f2a71e73f..503741ca8 100644 --- a/src/core/MacPasteboard.h +++ b/src/core/MacPasteboard.h @@ -18,9 +18,9 @@ #ifndef KEEPASSXC_MACPASTEBOARD_H #define KEEPASSXC_MACPASTEBOARD_H -#include <QMacPasteboardMime> #include <QObject> #include <QTextCodec> +#include <QtMacExtras/QMacPasteboardMime> class MacPasteboard : public QObject, public QMacPasteboardMime { diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index efe647880..bd9dcc67b 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -150,7 +150,7 @@ QVector<PasswordGroup> PasswordGenerator::passwordGroups() const for (int i = 65; i <= (65 + 25); i++) { - if ((m_flags & ExcludeLookAlike) && (i == 73 || i == 79)) { // "I" and "O" + if ((m_flags & ExcludeLookAlike) && (i == 66 || i == 71 || i == 73 || i == 79)) { //"B", "G", "I" and "O" continue; } @@ -163,7 +163,7 @@ QVector<PasswordGroup> PasswordGenerator::passwordGroups() const PasswordGroup group; for (int i = 48; i < (48 + 10); i++) { - if ((m_flags & ExcludeLookAlike) && (i == 48 || i == 49)) { // "0" and "1" + if ((m_flags & ExcludeLookAlike) && (i == 48 || i == 49 || i == 54 || i == 56)) { // "0", "1", "6", and "8" continue; } diff --git a/src/format/OpVaultReader.h b/src/format/OpVaultReader.h index 585415854..846e65dd3 100644 --- a/src/format/OpVaultReader.h +++ b/src/format/OpVaultReader.h @@ -98,7 +98,7 @@ private: bool fillAttributes(Entry* entry, const QJsonObject& bandEntry); void fillFromSection(Entry* entry, const QJsonObject& section); - void fillFromSectionField(Entry* entry, const QString& sectionName, QJsonObject& field); + void fillFromSectionField(Entry* entry, const QString& sectionName, const QJsonObject& field); QString resolveAttributeName(const QString& section, const QString& name, const QString& text); void populateCategoryGroups(Group* rootGroup); diff --git a/src/format/OpVaultReaderSections.cpp b/src/format/OpVaultReaderSections.cpp index e59dd9f02..b7677ec05 100644 --- a/src/format/OpVaultReaderSections.cpp +++ b/src/format/OpVaultReaderSections.cpp @@ -53,12 +53,11 @@ namespace void OpVaultReader::fillFromSection(Entry* entry, const QJsonObject& section) { const auto uuid = entry->uuid(); - QString sectionName = section["name"].toString(); + auto sectionTitle = section["title"].toString(); if (!section.contains("fields")) { - auto sectionNameLC = sectionName.toLower(); - auto sectionTitleLC = section["title"].toString("").toLower(); - if (!(sectionNameLC == "linked items" && sectionTitleLC == "related items")) { + auto sectionName = section["name"].toString(); + if (!(sectionName.toLower() == "linked items" && sectionTitle.toLower() == "related items")) { qWarning() << R"(Skipping "fields"-less Section in UUID ")" << uuid << "\": <<" << section << ">>"; } return; @@ -67,23 +66,17 @@ void OpVaultReader::fillFromSection(Entry* entry, const QJsonObject& section) return; } - // If we have a default section name then replace with the section title if not empty - if (sectionName.startsWith("Section_") && !section["title"].toString().isEmpty()) { - sectionName = section["title"].toString(); - } - QJsonArray sectionFields = section["fields"].toArray(); for (const QJsonValue sectionField : sectionFields) { if (!sectionField.isObject()) { qWarning() << R"(Skipping non-Object "fields" in UUID ")" << uuid << "\": << " << sectionField << ">>"; continue; } - QJsonObject field = sectionField.toObject(); - fillFromSectionField(entry, sectionName, field); + fillFromSectionField(entry, sectionTitle, sectionField.toObject()); } } -void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionName, QJsonObject& field) +void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionName, const QJsonObject& field) { if (!field.contains("v")) { // for our purposes, we don't care if there isn't a value in the field @@ -161,8 +154,8 @@ QString OpVaultReader::resolveAttributeName(const QString& section, const QStrin || lowName == "website") { return EntryAttributes::URLKey; } - return name; + return text; } - return QString("%1_%2").arg(section, name); + return QString("%1_%2").arg(section, text); } diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 7c89b8804..93bb4f1eb 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -60,6 +60,10 @@ static const QString aboutContributors = R"( <li>Micha Ober</li> <li>PublicByte</li> <li>Clayton Casciato</li> + <li>Dominik</li> + <li>Paul Ammann</li> + <li>Steve Isom</li> + <li>Matt Cardarelli</li> </ul> <h3>Notable Code Contributions:</h3> <ul> @@ -118,6 +122,8 @@ static const QString aboutContributors = R"( <li>Larry Siden</li> <li>Thammachart Chinvarapon</li> <li>Patrick Evans</li> + <li>Johannes Erchen</li> + <li>Ralph Azucena</li> </ul> <h3>Translations:</h3> <ul> diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 784528294..973e29032 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -286,13 +286,26 @@ void Application::socketReadyRead() } QStringList fileNames; - in >> fileNames; - for (const QString& fileName : asConst(fileNames)) { - const QFileInfo fInfo(fileName); - if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") { - emit openFile(fileName); + quint32 id; + in >> id; + + // TODO: move constants to enum + switch (id) { + case 1: + in >> fileNames; + for (const QString& fileName : asConst(fileNames)) { + const QFileInfo fInfo(fileName); + if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") { + emit openFile(fileName); + } } + + break; + case 2: + getMainWindow()->lockAllDatabases(); + break; } + socket->deleteLater(); } @@ -305,6 +318,12 @@ bool Application::isAlreadyRunning() const return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning; } +/** + * Send to-open file names to the running UI instance + * + * @param fileNames - list of file names to open + * @return true if all operations succeeded (connection made, data sent, connection closed) + */ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) { QLocalSocket client; @@ -317,13 +336,48 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) QByteArray data; QDataStream out(&data, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_0); - out << quint32(0) << fileNames; + out << quint32(0); // reserve space for block size + out << quint32(1); // ID for file name send. TODO: move to enum + out << fileNames; // send file names to be opened + out.device()->seek(0); + out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size + + const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec); + client.disconnectFromServer(); + const bool disconnected = + client.state() == QLocalSocket::UnconnectedState || client.waitForDisconnected(WaitTimeoutMSec); + return writeOk && disconnected; +} + +/** + * Locks all open databases in the running instance + * + * @return true if the "please lock" signal was sent successfully + */ +bool Application::sendLockToInstance() +{ + // Make a connection to avoid SIGSEGV + QLocalSocket client; + client.connectToServer(m_socketName); + const bool connected = client.waitForConnected(WaitTimeoutMSec); + if (!connected) { + return false; + } + + // Send lock signal + QByteArray data; + QDataStream out(&data, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_5_0); + out << quint32(0); // reserve space for block size + out << quint32(2); // ID for database lock. TODO: move to enum out.device()->seek(0); - out << quint32(data.size() - sizeof(quint32)); + out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size + // Finish gracefully const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec); client.disconnectFromServer(); - const bool disconnected = client.waitForDisconnected(WaitTimeoutMSec); + const bool disconnected = + client.state() == QLocalSocket::UnconnectedState || client.waitForConnected(WaitTimeoutMSec); return writeOk && disconnected; } diff --git a/src/gui/Application.h b/src/gui/Application.h index 9f694f8c3..f6c35a037 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -48,6 +48,7 @@ public: bool isDarkTheme() const; bool sendFileNamesToRunningInstance(const QStringList& fileNames); + bool sendLockToInstance(); void restart(); diff --git a/src/gui/Clipboard.cpp b/src/gui/Clipboard.cpp index ab4a84cf0..804a53143 100644 --- a/src/gui/Clipboard.cpp +++ b/src/gui/Clipboard.cpp @@ -39,8 +39,7 @@ Clipboard::Clipboard(QObject* parent) m_pasteboard = new MacPasteboard(); } #endif - m_timer->setSingleShot(true); - connect(m_timer, SIGNAL(timeout()), SLOT(clearClipboard())); + connect(m_timer, SIGNAL(timeout()), SLOT(countdownTick())); connect(qApp, SIGNAL(aboutToQuit()), SLOT(clearCopiedText())); } @@ -77,7 +76,9 @@ void Clipboard::setText(const QString& text, bool clear) if (config()->get(Config::Security_ClearClipboard).toBool()) { int timeout = config()->get(Config::Security_ClearClipboardTimeout).toInt(); if (timeout > 0) { - m_timer->start(timeout * 1000); + m_secondsElapsed = -1; + countdownTick(); + m_timer->start(1000); } } } @@ -85,15 +86,9 @@ void Clipboard::setText(const QString& text, bool clear) void Clipboard::clearCopiedText() { - if (m_timer->isActive()) { - m_timer->stop(); - } - - clearClipboard(); -} + m_timer->stop(); + emit updateCountdown(-1, ""); -void Clipboard::clearClipboard() -{ auto* clipboard = QApplication::clipboard(); if (!clipboard) { qWarning("Unable to access the clipboard."); @@ -108,6 +103,19 @@ void Clipboard::clearClipboard() m_lastCopied.clear(); } +void Clipboard::countdownTick() +{ + m_secondsElapsed++; + int timeout = config()->get(Config::Security_ClearClipboardTimeout).toInt(); + int timeLeft = timeout - m_secondsElapsed; + if (timeLeft <= 0) { + clearCopiedText(); + } else { + emit updateCountdown(100 * timeLeft / timeout, + QObject::tr("Clearing the clipboard in %1 second(s)…", "", timeLeft).arg(timeLeft)); + } +} + Clipboard* Clipboard::instance() { if (!m_instance) { diff --git a/src/gui/Clipboard.h b/src/gui/Clipboard.h index 7465f30a5..c97d91881 100644 --- a/src/gui/Clipboard.h +++ b/src/gui/Clipboard.h @@ -19,6 +19,7 @@ #ifndef KEEPASSX_CLIPBOARD_H #define KEEPASSX_CLIPBOARD_H +#include <QElapsedTimer> #include <QObject> #ifdef Q_OS_MACOS #include "core/MacPasteboard.h" @@ -39,8 +40,11 @@ public: public slots: void clearCopiedText(); +signals: + void updateCountdown(int percentage, QString message); + private slots: - void clearClipboard(); + void countdownTick(); private: explicit Clipboard(QObject* parent = nullptr); @@ -48,6 +52,8 @@ private: static Clipboard* m_instance; QTimer* m_timer; + int m_secondsElapsed = 0; + #ifdef Q_OS_MACOS // This object lives for the whole program lifetime and we cannot delete it on exit, // so ignore leak warnings. See https://bugreports.qt.io/browse/QTBUG-54832 diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index bb0a6ec41..0f0037ced 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1176,6 +1176,10 @@ void DatabaseWidget::unlockDatabase(bool accepted) sshAgent()->databaseUnlocked(m_db); #endif + if (config()->get(Config::MinimizeAfterUnlock).toBool()) { + getMainWindow()->minimizeOrHide(); + } + if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::AutoType) { QList<QSharedPointer<Database>> dbList; dbList.append(m_db); @@ -1391,9 +1395,8 @@ void DatabaseWidget::onGroupChanged() // Intercept group changes if in search mode if (isSearchActive() && m_searchLimitGroup) { search(m_lastSearchText); - } else if (isSearchActive()) { - endSearch(); } else { + endSearch(); m_entryView->displayGroup(group); } diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index fbd07a82e..c916582d2 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -102,7 +102,7 @@ void EditWidget::setPageHidden(QWidget* widget, bool hidden) m_ui->categoryList->setCategoryHidden(index, hidden); } - if (index == m_ui->stackedWidget->currentIndex()) { + if (hidden && index == m_ui->stackedWidget->currentIndex()) { int newIndex = m_ui->stackedWidget->currentIndex() - 1; if (newIndex < 0) { newIndex = m_ui->stackedWidget->count() - 1; diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index e240f7ae4..141f25d4c 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -245,14 +245,14 @@ void EntryPreviewWidget::updateEntryGeneralTab() m_ui->togglePasswordButton->setVisible(false); } - if (config()->get(Config::Security_HideNotes).toBool()) { - setEntryNotesVisible(false); - m_ui->toggleEntryNotesButton->setVisible(!m_ui->entryNotesTextEdit->toPlainText().isEmpty()); - m_ui->toggleEntryNotesButton->setChecked(false); - } else { - setEntryNotesVisible(true); - m_ui->toggleEntryNotesButton->setVisible(false); - } + auto hasNotes = !m_currentEntry->notes().isEmpty(); + auto hideNotes = config()->get(Config::Security_HideNotes).toBool(); + + m_ui->entryNotesTextEdit->setVisible(hasNotes); + setEntryNotesVisible(hasNotes && !hideNotes); + m_ui->toggleEntryNotesButton->setVisible(hasNotes && hideNotes + && !m_ui->entryNotesTextEdit->toPlainText().isEmpty()); + m_ui->toggleEntryNotesButton->setChecked(false); if (config()->get(Config::GUI_MonospaceNotes).toBool()) { m_ui->entryNotesTextEdit->setFont(Font::fixedFont()); diff --git a/src/gui/Font.cpp b/src/gui/Font.cpp index 021380c29..8bb54863b 100644 --- a/src/gui/Font.cpp +++ b/src/gui/Font.cpp @@ -18,29 +18,29 @@ #include "Font.h" #include <QFontDatabase> +#include <QGuiApplication> QFont Font::defaultFont() { - return QFontDatabase::systemFont(QFontDatabase::GeneralFont); + return qApp->font(); } QFont Font::fixedFont() { - QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); #ifdef Q_OS_WIN // try to use Consolas on Windows, because the default Courier New has too many similar characters - QFont consolasFont = QFontDatabase().font("Consolas", fixedFont.styleName(), fixedFont.pointSize()); - const QFont defaultFont; - if (fixedFont != defaultFont) { + auto consolasFont = QFontDatabase().font("Consolas", fixedFont.styleName(), fixedFont.pointSize()); + if (consolasFont.family().contains("consolas", Qt::CaseInsensitive)) { fixedFont = consolasFont; } #endif #ifdef Q_OS_MACOS // Qt doesn't choose a monospace font correctly on macOS - const QFont defaultFont; - fixedFont = QFontDatabase().font("Menlo", defaultFont.styleName(), defaultFont.pointSize()); + fixedFont = QFontDatabase().font("Menlo", fixedFont.styleName(), fixedFont.pointSize()); #endif + fixedFont.setPointSize(qApp->font().pointSize()); return fixedFont; } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c08f6e677..659ac8457 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -118,6 +118,17 @@ MainWindow::MainWindow() m_searchWidgetAction = m_ui->toolBar->addWidget(m_searchWidget); m_searchWidgetAction->setEnabled(false); + new QShortcut(QKeySequence::Find, this, SLOT(focusSearchWidget())); + + connect(m_searchWidget, &SearchWidget::searchCanceled, this, [this] { + m_ui->toolBar->setExpanded(false); + m_ui->toolBar->setVisible(!config()->get(Config::GUI_HideToolbar).toBool()); + }); + connect(m_searchWidget, &SearchWidget::lostFocus, this, [this] { + m_ui->toolBar->setExpanded(false); + m_ui->toolBar->setVisible(!config()->get(Config::GUI_HideToolbar).toBool()); + }); + m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); m_entryContextMenu = new QMenu(this); @@ -607,6 +618,19 @@ MainWindow::MainWindow() QObject::connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront())); QObject::connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString))); QObject::connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection); + + statusBar()->setFixedHeight(24); + m_progressBarLabel = new QLabel(statusBar()); + m_progressBarLabel->setVisible(false); + statusBar()->addPermanentWidget(m_progressBarLabel); + m_progressBar = new QProgressBar(statusBar()); + m_progressBar->setVisible(false); + m_progressBar->setTextVisible(false); + m_progressBar->setMaximumWidth(100); + m_progressBar->setFixedHeight(15); + m_progressBar->setMaximum(100); + statusBar()->addPermanentWidget(m_progressBar); + connect(clipboard(), SIGNAL(updateCountdown(int, QString)), this, SLOT(updateProgressBar(int, QString))); } MainWindow::~MainWindow() @@ -1214,7 +1238,10 @@ void MainWindow::keyPressEvent(QKeyEvent* event) dbWidget->focusOnEntries(true); return; } else if (event->key() == Qt::Key_F3) { - m_searchWidget->searchFocus(); + focusSearchWidget(); + return; + } else if (event->key() == Qt::Key_Escape && dbWidget->isSearchActive()) { + m_searchWidget->clearSearch(); return; } } @@ -1235,13 +1262,13 @@ bool MainWindow::focusNextPrevChild(bool next) } else if (m_ui->tabWidget->hasFocus()) { dbWidget->setFocus(Qt::TabFocusReason); } else { - m_searchWidget->setFocus(Qt::TabFocusReason); + focusSearchWidget(); } } else { if (m_searchWidget->hasFocus()) { dbWidget->setFocus(Qt::BacktabFocusReason); } else if (m_ui->tabWidget->hasFocus()) { - m_searchWidget->setFocus(Qt::BacktabFocusReason); + focusSearchWidget(); } else { m_ui->tabWidget->setFocus(Qt::BacktabFocusReason); } @@ -1253,6 +1280,15 @@ bool MainWindow::focusNextPrevChild(bool next) return QMainWindow::focusNextPrevChild(next); } +void MainWindow::focusSearchWidget() +{ + if (m_searchWidgetAction->isEnabled()) { + m_ui->toolBar->setVisible(true); + m_ui->toolBar->setExpanded(true); + m_searchWidget->focusSearch(); + } +} + void MainWindow::saveWindowInformation() { if (isVisible()) { @@ -1342,6 +1378,19 @@ void MainWindow::updateTrayIcon() QApplication::setQuitOnLastWindowClosed(!isTrayIconEnabled()); } +void MainWindow::updateProgressBar(int percentage, QString message) +{ + if (percentage < 0) { + m_progressBar->setVisible(false); + m_progressBarLabel->setVisible(false); + } else { + m_progressBar->setValue(percentage); + m_progressBar->setVisible(true); + m_progressBarLabel->setText(message); + m_progressBarLabel->setVisible(true); + } +} + void MainWindow::obtainContextFocusLock() { m_contextMenuFocusLock = true; @@ -1773,6 +1822,7 @@ void MainWindow::initViewMenu() }); connect(m_ui->actionAlwaysOnTop, &QAction::toggled, this, [this](bool checked) { + config()->set(Config::GUI_AlwaysOnTop, checked); if (checked) { setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); } else { @@ -1780,6 +1830,8 @@ void MainWindow::initViewMenu() } show(); }); + // Set checked after connecting to act on a toggle in state (default state is unchecked) + m_ui->actionAlwaysOnTop->setChecked(config()->get(Config::GUI_AlwaysOnTop).toBool()); m_ui->actionHideUsernames->setChecked(config()->get(Config::GUI_HideUsernames).toBool()); connect(m_ui->actionHideUsernames, &QAction::toggled, this, [](bool checked) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 7db347fbd..a80346dfe 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -20,12 +20,16 @@ #define KEEPASSX_MAINWINDOW_H #include <QActionGroup> +#include <QLabel> #include <QMainWindow> +#include <QProgressBar> +#include <QStatusBar> #include <QSystemTrayIcon> #include "core/ScreenLockListener.h" #include "core/SignalMultiplexer.h" #include "gui/Application.h" +#include "gui/Clipboard.h" #include "gui/DatabaseWidget.h" namespace Ui @@ -138,6 +142,8 @@ private slots: private slots: void updateTrayIcon(); + void updateProgressBar(int percentage, QString message); + void focusSearchWidget(); private: static void setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback = 0); @@ -169,6 +175,8 @@ private: QPointer<QSystemTrayIcon> m_trayIcon; QPointer<ScreenLockListener> m_screenLockListener; QPointer<SearchWidget> m_searchWidget; + QPointer<QProgressBar> m_progressBar; + QPointer<QLabel> m_progressBarLabel; Q_DISABLE_COPY(MainWindow) diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index e44d3d217..679578f1b 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -16,7 +16,7 @@ <property name="minimumSize"> <size> <width>800</width> - <height>0</height> + <height>400</height> </size> </property> <property name="windowTitle"> @@ -394,7 +394,7 @@ <addaction name="menuView"/> <addaction name="menuHelp"/> </widget> - <widget class="QToolBar" name="toolBar"> + <widget class="KPToolBar" name="toolBar"> <property name="contextMenuPolicy"> <enum>Qt::PreventContextMenu</enum> </property> @@ -1043,6 +1043,11 @@ <header>gui/WelcomeWidget.h</header> <container>1</container> </customwidget> + <customwidget> + <class>KPToolBar</class> + <extends>QToolBar</extends> + <header>gui/widgets/KPToolBar.h</header> + </customwidget> </customwidgets> <resources/> <connections/> diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index b4ba97193..d00448096 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -666,7 +666,7 @@ QProgressBar::chunk { <item> <widget class="QCheckBox" name="checkBoxExcludeAlike"> <property name="toolTip"> - <string>Excluded characters: "0", "1", "l", "I", "O", "|", "﹒"</string> + <string>Excluded characters: "0", "O", "1", "l", "I", "|", "G", "6", "B", "8", "﹒"</string> </property> <property name="text"> <string>Exclude look-alike characters</string> diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 6ade47e2e..4cf53a9ac 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -48,8 +48,8 @@ SearchWidget::SearchWidget(QWidget* parent) connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp())); connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu())); connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch())); - connect(m_clearSearchTimer, SIGNAL(timeout()), m_ui->searchEdit, SLOT(clear())); - connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); + connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch())); + connect(this, SIGNAL(escapePressed()), SLOT(clearSearch())); new QShortcut(QKeySequence::Find, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut); new QShortcut(Qt::Key_Escape, m_ui->searchEdit, SLOT(clear()), nullptr, Qt::ApplicationShortcut); @@ -109,7 +109,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) return true; } } - } else if (event->type() == QEvent::FocusOut && !m_ui->searchEdit->text().isEmpty()) { + } else if (event->type() == QEvent::FocusOut) { if (config()->get(Config::Security_ClearSearch).toBool()) { int timeout = config()->get(Config::Security_ClearSearchTimeout).toInt(); if (timeout > 0) { @@ -117,6 +117,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) m_clearSearchTimer->start(timeout * 60000); // 60 sec * 1000 ms } } + emit lostFocus(); } else if (event->type() == QEvent::FocusIn) { // Never clear the search if we are using it m_clearSearchTimer->stop(); @@ -133,10 +134,10 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx) mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool))); mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword())); mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries())); - mx.connect(SIGNAL(clearSearch()), m_ui->searchEdit, SLOT(clear())); + mx.connect(SIGNAL(clearSearch()), this, SLOT(clearSearch())); mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer())); mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer())); - mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(searchFocus())); + mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(focusSearch())); mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit())); } @@ -149,7 +150,7 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget) emit caseSensitiveChanged(m_actionCaseSensitive->isChecked()); emit limitGroupChanged(m_actionLimitGroup->isChecked()); } else { - m_ui->searchEdit->clear(); + clearSearch(); } } @@ -201,12 +202,18 @@ void SearchWidget::setLimitGroup(bool state) updateLimitGroup(); } -void SearchWidget::searchFocus() +void SearchWidget::focusSearch() { m_ui->searchEdit->setFocus(); m_ui->searchEdit->selectAll(); } +void SearchWidget::clearSearch() +{ + m_ui->searchEdit->clear(); + emit searchCanceled(); +} + void SearchWidget::toggleHelp() { if (m_helpWidget->isVisible()) { diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index eefdff6ee..b2192f54d 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -52,16 +52,19 @@ protected: signals: void search(const QString& text); + void searchCanceled(); void caseSensitiveChanged(bool state); void limitGroupChanged(bool state); void escapePressed(); void copyPressed(); void downPressed(); void enterPressed(); + void lostFocus(); public slots: void databaseChanged(DatabaseWidget* dbWidget = nullptr); - void searchFocus(); + void focusSearch(); + void clearSearch(); private slots: void startSearchTimer(); diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index e78e9f94a..809fc6e53 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -208,8 +208,13 @@ void CsvImportWidget::writeDatabase() entry->setUrl(m_parserModel->data(m_parserModel->index(r, 4)).toString()); entry->setNotes(m_parserModel->data(m_parserModel->index(r, 5)).toString()); - if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) { - auto totp = Totp::parseSettings(m_parserModel->data(m_parserModel->index(r, 6)).toString()); + auto otpString = m_parserModel->data(m_parserModel->index(r, 6)); + if (otpString.isValid() && !otpString.toString().isEmpty()) { + auto totp = Totp::parseSettings(otpString.toString()); + if (totp->key.isEmpty()) { + // Bare secret, use default TOTP settings + totp = Totp::parseSettings({}, otpString.toString()); + } entry->setTotp(totp); } diff --git a/src/gui/databasekey/KeyFileEditWidget.cpp b/src/gui/databasekey/KeyFileEditWidget.cpp index 2fb0b3de2..534bfc8ab 100644 --- a/src/gui/databasekey/KeyFileEditWidget.cpp +++ b/src/gui/databasekey/KeyFileEditWidget.cpp @@ -42,7 +42,7 @@ KeyFileEditWidget::~KeyFileEditWidget() bool KeyFileEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> key) { auto fileKey = QSharedPointer<FileKey>::create(); - QString fileKeyName = m_compUi->keyFileCombo->currentText(); + QString fileKeyName = m_compUi->keyFileLineEdit->text(); if (!fileKey->load(fileKeyName, nullptr)) { return false; } @@ -64,7 +64,7 @@ bool KeyFileEditWidget::validate(QString& errorMessage) const { FileKey fileKey; QString fileKeyError; - QString fileKeyName = m_compUi->keyFileCombo->currentText(); + QString fileKeyName = m_compUi->keyFileLineEdit->text(); if (!fileKey.load(fileKeyName, &fileKeyError)) { errorMessage = tr("Error loading the key file '%1'\nMessage: %2").arg(fileKeyName, fileKeyError); return false; @@ -87,7 +87,7 @@ void KeyFileEditWidget::initComponentEditWidget(QWidget* widget) { Q_UNUSED(widget); Q_ASSERT(m_compEditWidget); - m_compUi->keyFileCombo->setFocus(); + m_compUi->keyFileLineEdit->setFocus(); } void KeyFileEditWidget::createKeyFile() @@ -108,7 +108,7 @@ void KeyFileEditWidget::createKeyFile() tr("Unable to create key file: %1").arg(errorMsg), QMessageBox::Button::Ok); } else { - m_compUi->keyFileCombo->setEditText(fileName); + m_compUi->keyFileLineEdit->setText(fileName); } } } @@ -143,6 +143,6 @@ void KeyFileEditWidget::browseKeyFile() } if (!fileName.isEmpty()) { - m_compUi->keyFileCombo->setEditText(fileName); + m_compUi->keyFileLineEdit->setText(fileName); } } diff --git a/src/gui/databasekey/KeyFileEditWidget.ui b/src/gui/databasekey/KeyFileEditWidget.ui index 088995dc8..3c83a55d3 100644 --- a/src/gui/databasekey/KeyFileEditWidget.ui +++ b/src/gui/databasekey/KeyFileEditWidget.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>370</width> - <height>76</height> + <width>566</width> + <height>94</height> </rect> </property> <layout class="QGridLayout" name="gridLayout"> @@ -23,67 +23,68 @@ <property name="bottomMargin"> <number>0</number> </property> - <item row="0" column="0"> - <widget class="QComboBox" name="keyFileCombo"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> + <item row="0" column="1"> + <widget class="QPushButton" name="createKeyFileButton"> <property name="accessibleName"> - <string>Key file selection</string> + <string>Generate a new key file</string> </property> - <property name="editable"> - <bool>true</bool> + <property name="text"> + <string>Generate</string> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QPushButton" name="browseKeyFileButton"> - <property name="accessibleName"> - <string>Browse for key file</string> - </property> + <item row="0" column="0"> + <widget class="QLabel" name="instructions"> <property name="text"> - <string>Browse...</string> + <string>Generate a new key file or choose an existing one to protect your database.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> </property> </widget> </item> - <item row="1" column="1"> - <widget class="QPushButton" name="createKeyFileButton"> - <property name="accessibleName"> - <string>Generate a new key file</string> + <item row="3" column="0"> + <widget class="QLabel" name="instructions_2"> + <property name="font"> + <font> + <italic>true</italic> + </font> </property> <property name="text"> - <string>Generate</string> + <string>Note: Do NOT use a file that may change as that will prevent you from unlocking your database.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> </property> </widget> </item> - <item row="2" column="0"> + <item row="4" column="0"> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> <property name="sizeHint" stdset="0"> <size> - <width>20</width> + <width>0</width> <height>0</height> </size> </property> </spacer> </item> <item row="1" column="0"> - <widget class="QLabel" name="label"> - <property name="font"> - <font> - <italic>true</italic> - </font> + <widget class="QLineEdit" name="keyFileLineEdit"> + <property name="clearButtonEnabled"> + <bool>true</bool> </property> - <property name="text"> - <string>Note: Do not use a file that may change as that will prevent you from unlocking your database!</string> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="browseKeyFileButton"> + <property name="accessibleName"> + <string>Browse for key file</string> </property> - <property name="wordWrap"> - <bool>true</bool> + <property name="text"> + <string>Browse…</string> </property> </widget> </item> diff --git a/src/gui/entry/AutoTypeMatchView.cpp b/src/gui/entry/AutoTypeMatchView.cpp index 72ee32fde..cdf1f9425 100644 --- a/src/gui/entry/AutoTypeMatchView.cpp +++ b/src/gui/entry/AutoTypeMatchView.cpp @@ -64,14 +64,20 @@ AutoTypeMatchView::AutoTypeMatchView(QWidget* parent) void AutoTypeMatchView::userNameCopied() { - clipboard()->setText(currentMatch().entry->username()); - emit matchTextCopied(); + auto entry = currentMatch().entry; + if (entry) { + clipboard()->setText(entry->resolvePlaceholder(entry->username())); + emit matchTextCopied(); + } } void AutoTypeMatchView::passwordCopied() { - clipboard()->setText(currentMatch().entry->password()); - emit matchTextCopied(); + auto entry = currentMatch().entry; + if (entry) { + clipboard()->setText(entry->resolvePlaceholder(entry->password())); + emit matchTextCopied(); + } } void AutoTypeMatchView::keyPressEvent(QKeyEvent* event) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index c4de82973..93b5943b4 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -279,6 +279,7 @@ void EditEntryWidget::setupBrowser() connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->onlyHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); + connect(m_browserUi->notHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified())); connect(m_browserUi->addURLButton, SIGNAL(clicked()), SLOT(insertURL())); connect(m_browserUi->removeURLButton, SIGNAL(clicked()), SLOT(removeCurrentURL())); connect(m_browserUi->editURLButton, SIGNAL(clicked()), SLOT(editCurrentURL())); @@ -306,9 +307,11 @@ void EditEntryWidget::updateBrowser() auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked(); auto hide = m_browserUi->hideEntryCheckbox->isChecked(); auto onlyHttpAuth = m_browserUi->onlyHttpAuthCheckbox->isChecked(); + auto notHttpAuth = m_browserUi->notHttpAuthCheckbox->isChecked(); m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? TRUE_STR : FALSE_STR)); m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? TRUE_STR : FALSE_STR)); m_customData->set(BrowserService::OPTION_ONLY_HTTP_AUTH, (onlyHttpAuth ? TRUE_STR : FALSE_STR)); + m_customData->set(BrowserService::OPTION_NOT_HTTP_AUTH, (notHttpAuth ? TRUE_STR : FALSE_STR)); } void EditEntryWidget::insertURL() @@ -473,6 +476,7 @@ void EditEntryWidget::setupEntryUpdate() connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->onlyHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); + connect(m_browserUi->notHttpAuthCheckbox, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->addURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->removeURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_browserUi->editURLButton, SIGNAL(toggled(bool)), SLOT(setModified())); @@ -958,6 +962,13 @@ void EditEntryWidget::setForms(Entry* entry, bool restore) m_browserUi->onlyHttpAuthCheckbox->setChecked(false); } + if (m_customData->contains(BrowserService::OPTION_NOT_HTTP_AUTH)) { + m_browserUi->notHttpAuthCheckbox->setChecked(m_customData->value(BrowserService::OPTION_NOT_HTTP_AUTH) + == TRUE_STR); + } else { + m_browserUi->notHttpAuthCheckbox->setChecked(false); + } + m_browserUi->addURLButton->setEnabled(!m_history); m_browserUi->removeURLButton->setEnabled(false); m_browserUi->editURLButton->setEnabled(false); diff --git a/src/gui/entry/EditEntryWidgetBrowser.ui b/src/gui/entry/EditEntryWidgetBrowser.ui index 9d1e0f511..5f117e987 100644 --- a/src/gui/entry/EditEntryWidgetBrowser.ui +++ b/src/gui/entry/EditEntryWidgetBrowser.ui @@ -60,6 +60,16 @@ </property> </widget> </item> + <item> + <widget class="QCheckBox" name="notHttpAuthCheckbox"> + <property name="toolTip"> + <string>Do not send this setting to the browser for HTTP Auth dialogs. If enabled, HTTP Auth dialogs will not show this entry for selection.</string> + </property> + <property name="text"> + <string>Do not use this entry with HTTP Basic Auth</string> + </property> + </widget> + </item> </layout> </widget> </item> diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp index 836c8ef27..f78b64417 100644 --- a/src/gui/entry/EntryAttachmentsWidget.cpp +++ b/src/gui/entry/EntryAttachmentsWidget.cpp @@ -238,7 +238,8 @@ void EntryAttachmentsWidget::saveSelectedAttachments() QFile file(attachmentPath); const QByteArray attachmentData = m_entryAttachments->value(filename); - const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size(); + const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser) + && file.write(attachmentData) == attachmentData.size(); if (!saveOk) { errors.append(QString("%1 - %2").arg(filename, file.errorString())); } diff --git a/src/gui/osutils/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm index faf061106..31362c8e6 100644 --- a/src/gui/osutils/macutils/AppKitImpl.mm +++ b/src/gui/osutils/macutils/AppKitImpl.mm @@ -203,11 +203,10 @@ - (void) toggleForegroundApp:(bool) foreground { - ProcessSerialNumber psn = {0, kCurrentProcess}; if (foreground) { - TransformProcessType(&psn, kProcessTransformToForegroundApplication); + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; } else { - TransformProcessType(&psn, kProcessTransformToUIElementApplication); + [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; } } diff --git a/src/gui/osutils/macutils/MacUtils.cpp b/src/gui/osutils/macutils/MacUtils.cpp index ebcc627e0..9c107df77 100644 --- a/src/gui/osutils/macutils/MacUtils.cpp +++ b/src/gui/osutils/macutils/MacUtils.cpp @@ -69,6 +69,7 @@ bool MacUtils::raiseWindow(WId pid) bool MacUtils::raiseOwnWindow() { + m_appkit->toggleForegroundApp(true); return m_appkit->activateProcess(m_appkit->ownProcessId()); } diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index eacb20060..9f147c59e 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -120,7 +120,8 @@ void NixUtils::setLaunchAtStartup(bool enable) << QStringLiteral("MimeType=application/x-keepass2;") << '\n' << QStringLiteral("X-GNOME-Autostart-enabled=true") << '\n' << QStringLiteral("X-GNOME-Autostart-Delay=2") << '\n' - << QStringLiteral("X-KDE-autostart-after=panel") << endl; + << QStringLiteral("X-KDE-autostart-after=panel") << '\n' + << QStringLiteral("X-LXQt-Need-Tray=true") << endl; desktopFile.close(); } else if (isLaunchAtStartupEnabled()) { QFile::remove(getAutostartDesktopFilename()); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index bc42b1e01..aea468059 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -150,8 +150,10 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) m_modelProxy->setSortLocaleAware(true); m_ui->healthcheckTableView->setModel(m_modelProxy.data()); m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); - m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + m_ui->healthcheckTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); m_ui->healthcheckTableView->setSortingEnabled(true); + m_ui->healthcheckTableView->setWordWrap(true); connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); @@ -281,7 +283,8 @@ void ReportsWidgetHealthcheck::calculateHealth() m_ui->healthcheckTableView->sortByColumn(0, Qt::AscendingOrder); } - m_ui->healthcheckTableView->resizeRowsToContents(); + m_ui->healthcheckTableView->resizeColumnsToContents(); + m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed); // Show the "show known bad entries" checkbox if there's any known // bad entry in the database. diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 406c465b9..f48e7448f 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -79,7 +79,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) m_modelProxy->setSortLocaleAware(true); m_ui->hibpTableView->setModel(m_modelProxy.data()); m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection); - m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + m_ui->hibpTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); m_ui->hibpTableView->setSortingEnabled(true); connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); @@ -219,7 +220,7 @@ void ReportsWidgetHibp::makeHibpTable() m_ui->showKnownBadCheckBox->hide(); } - m_ui->hibpTableView->resizeRowsToContents(); + m_ui->hibpTableView->resizeColumnsToContents(); m_ui->hibpTableView->sortByColumn(2, Qt::DescendingOrder); m_ui->stackedWidget->setCurrentIndex(1); diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss index ff5d915bb..e015efc25 100644 --- a/src/gui/styles/base/basestyle.qss +++ b/src/gui/styles/base/basestyle.qss @@ -69,3 +69,7 @@ QPlainTextEdit, QTextEdit { background-color: palette(base); padding-left: 4px; } + +QStatusBar { + background-color: palette(window); +} diff --git a/src/gui/styles/dark/DarkStyle.cpp b/src/gui/styles/dark/DarkStyle.cpp index 491f18d1f..74653bc98 100644 --- a/src/gui/styles/dark/DarkStyle.cpp +++ b/src/gui/styles/dark/DarkStyle.cpp @@ -21,6 +21,7 @@ #include <QDialog> #include <QMainWindow> #include <QMenuBar> +#include <QStatusBar> #include <QToolBar> DarkStyle::DarkStyle() @@ -110,7 +111,7 @@ QString DarkStyle::getAppStyleSheet() const void DarkStyle::polish(QWidget* widget) { if (qobject_cast<QMainWindow*>(widget) || qobject_cast<QDialog*>(widget) || qobject_cast<QMenuBar*>(widget) - || qobject_cast<QToolBar*>(widget)) { + || qobject_cast<QToolBar*>(widget) || qobject_cast<QStatusBar*>(widget)) { auto palette = widget->palette(); #if defined(Q_OS_MACOS) if (!osUtils->isDarkMode()) { diff --git a/src/gui/styles/light/LightStyle.cpp b/src/gui/styles/light/LightStyle.cpp index 483e1323b..537e483ba 100644 --- a/src/gui/styles/light/LightStyle.cpp +++ b/src/gui/styles/light/LightStyle.cpp @@ -22,6 +22,7 @@ #include <QDialog> #include <QMainWindow> #include <QMenuBar> +#include <QStatusBar> #include <QToolBar> LightStyle::LightStyle() @@ -111,7 +112,7 @@ QString LightStyle::getAppStyleSheet() const void LightStyle::polish(QWidget* widget) { if (qobject_cast<QMainWindow*>(widget) || qobject_cast<QDialog*>(widget) || qobject_cast<QMenuBar*>(widget) - || qobject_cast<QToolBar*>(widget)) { + || qobject_cast<QToolBar*>(widget) || qobject_cast<QStatusBar*>(widget)) { auto palette = widget->palette(); #if defined(Q_OS_MACOS) if (osUtils->isDarkMode()) { diff --git a/src/gui/widgets/KPToolBar.cpp b/src/gui/widgets/KPToolBar.cpp new file mode 100644 index 000000000..88b75b9b1 --- /dev/null +++ b/src/gui/widgets/KPToolBar.cpp @@ -0,0 +1,75 @@ +/*
+ * Copyright (C) 2021 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 "KPToolBar.h"
+
+#include <QAbstractButton>
+#include <QEvent>
+#include <QLayout>
+
+KPToolBar::KPToolBar(const QString& title, QWidget* parent)
+ : QToolBar(title, parent)
+{
+ init();
+}
+
+KPToolBar::KPToolBar(QWidget* parent)
+ : QToolBar(parent)
+{
+ init();
+}
+
+void KPToolBar::init()
+{
+ m_expandButton = findChild<QAbstractButton*>("qt_toolbar_ext_button");
+ m_expandTimer.setSingleShot(true);
+ connect(&m_expandTimer, &QTimer::timeout, this, [this] { setExpanded(false); });
+}
+
+bool KPToolBar::isExpanded()
+{
+ return !canExpand() || (canExpand() && m_expandButton->isChecked());
+}
+
+bool KPToolBar::canExpand()
+{
+ return m_expandButton && m_expandButton->isVisible();
+}
+
+void KPToolBar::setExpanded(bool state)
+{
+ if (canExpand() && !QMetaObject::invokeMethod(layout(), "setExpanded", Q_ARG(bool, state))) {
+ qWarning("Toolbar: Cannot invoke setExpanded!");
+ }
+}
+
+bool KPToolBar::event(QEvent* event)
+{
+ // Override events handled by the base class for better UX when using an expandable toolbar.
+ switch (event->type()) {
+ case QEvent::Leave:
+ // Hide the toolbar after 2 seconds of mouse exit
+ m_expandTimer.start(2000);
+ return true;
+ case QEvent::Enter:
+ // Mouse came back in, stop hiding timer
+ m_expandTimer.stop();
+ return true;
+ default:
+ return QToolBar::event(event);
+ }
+}
diff --git a/src/gui/widgets/KPToolBar.h b/src/gui/widgets/KPToolBar.h new file mode 100644 index 000000000..8883982bd --- /dev/null +++ b/src/gui/widgets/KPToolBar.h @@ -0,0 +1,52 @@ +/*
+ * Copyright (C) 2021 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_KPTOOLBAR_H
+#define KEEPASSXC_KPTOOLBAR_H
+
+#include <QPointer>
+#include <QTimer>
+#include <QToolBar>
+
+class QAbstractButton;
+
+class KPToolBar : public QToolBar
+{
+ Q_OBJECT
+
+public:
+ explicit KPToolBar(const QString& title, QWidget* parent = nullptr);
+ explicit KPToolBar(QWidget* parent = nullptr);
+ ~KPToolBar() override = default;
+
+ bool isExpanded();
+ bool canExpand();
+
+public slots:
+ void setExpanded(bool state);
+
+protected:
+ bool event(QEvent* event) override;
+
+private:
+ void init();
+
+ QTimer m_expandTimer;
+ QPointer<QAbstractButton> m_expandButton;
+};
+
+#endif // KEEPASSXC_KPTOOLBAR_H
diff --git a/src/keeshare/CMakeLists.txt b/src/keeshare/CMakeLists.txt index 333f273a3..96f9bda85 100644 --- a/src/keeshare/CMakeLists.txt +++ b/src/keeshare/CMakeLists.txt @@ -25,7 +25,7 @@ if(WITH_XC_KEESHARE) find_package(QuaZip) if(QUAZIP_FOUND) set(WITH_XC_KEESHARE_SECURE ON PARENT_SCOPE) - target_include_directories(keeshare SYSTEM PRIVATE ${QUAZIP_INCLUDE_DIR}) + target_include_directories(keeshare SYSTEM PRIVATE ${QUAZIP_INCLUDE_DIRS}) target_link_libraries(keeshare PRIVATE ${QUAZIP_LIBRARIES}) else() set(WITH_XC_KEESHARE_SECURE OFF PARENT_SCOPE) diff --git a/src/keeshare/ShareExport.cpp b/src/keeshare/ShareExport.cpp index c1797ac6d..aedbc04af 100644 --- a/src/keeshare/ShareExport.cpp +++ b/src/keeshare/ShareExport.cpp @@ -224,9 +224,5 @@ ShareObserver::Result ShareExport::intoContainer(const QString& resolvedPath, if (KeeShare::isContainerType(info, KeeShare::signedContainerFileType())) {
return intoSignedContainer(resolvedPath, reference, targetDb.data());
}
- if (KeeShare::isContainerType(info, KeeShare::unsignedContainerFileType())) {
- return intoUnsignedContainer(resolvedPath, reference, targetDb.data());
- }
- Q_ASSERT(false);
- return {reference.path, ShareObserver::Result::Error, tr("Unexpected export error occurred")};
+ return intoUnsignedContainer(resolvedPath, reference, targetDb.data());
}
diff --git a/src/keeshare/ShareImport.cpp b/src/keeshare/ShareImport.cpp index a767ab3aa..38a477aaf 100644 --- a/src/keeshare/ShareImport.cpp +++ b/src/keeshare/ShareImport.cpp @@ -344,8 +344,5 @@ ShareObserver::Result ShareImport::containerInto(const QString& resolvedPath, if (KeeShare::isContainerType(info, KeeShare::signedContainerFileType())) {
return signedContainerInto(resolvedPath, reference, targetGroup);
}
- if (KeeShare::isContainerType(info, KeeShare::unsignedContainerFileType())) {
- return unsignedContainerInto(resolvedPath, reference, targetGroup);
- }
- return {reference.path, ShareObserver::Result::Error, tr("Unknown share container type")};
+ return unsignedContainerInto(resolvedPath, reference, targetGroup);
}
diff --git a/src/main.cpp b/src/main.cpp index b88dc41e0..43f9d0992 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,6 +64,7 @@ int main(int argc, char** argv) QCommandLineOption configOption("config", QObject::tr("path to a custom config file"), "config"); QCommandLineOption localConfigOption( "localconfig", QObject::tr("path to a custom local config file"), "localconfig"); + QCommandLineOption lockOption("lock", QObject::tr("lock all open databases")); QCommandLineOption keyfileOption("keyfile", QObject::tr("key file of the database"), "keyfile"); QCommandLineOption pwstdinOption("pw-stdin", QObject::tr("read password of the database from stdin")); @@ -72,6 +73,7 @@ int main(int argc, char** argv) QCommandLineOption debugInfoOption(QStringList() << "debug-info", QObject::tr("Displays debugging information.")); parser.addOption(configOption); parser.addOption(localConfigOption); + parser.addOption(lockOption); parser.addOption(keyfileOption); parser.addOption(pwstdinOption); parser.addOption(debugInfoOption); @@ -96,12 +98,23 @@ int main(int argc, char** argv) } // Process single instance and early exit if already running + // FIXME: this is a *mess* and it is entirely my fault. --wundrweapon const QStringList fileNames = parser.positionalArguments(); if (app.isAlreadyRunning()) { - if (!fileNames.isEmpty()) { - app.sendFileNamesToRunningInstance(fileNames); + if (parser.isSet(lockOption)) { + if (app.sendLockToInstance()) { + qInfo() << QObject::tr("Locked databases.").toUtf8().constData(); + } else { + qWarning() << QObject::tr("Database failed to lock.").toUtf8().constData(); + return EXIT_FAILURE; + } + } else { + if (!fileNames.isEmpty()) { + app.sendFileNamesToRunningInstance(fileNames); + } + + qWarning() << QObject::tr("Another instance of KeePassXC is already running.").toUtf8().constData(); } - qWarning() << QObject::tr("Another instance of KeePassXC is already running.").toUtf8().constData(); return EXIT_SUCCESS; } |