diff --git a/CMakeLists.txt b/CMakeLists.txt index 686db7362..3fb00a023 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,27 +73,49 @@ set(KEEPASSXC_VERSION_MINOR "3") set(KEEPASSXC_VERSION_PATCH "0") set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION_MAJOR}.${KEEPASSXC_VERSION_MINOR}.${KEEPASSXC_VERSION_PATCH}") -set(KEEPASSXC_RELEASE_BUILD OFF CACHE BOOLEAN "Remove stability warnings") +set(KEEPASSXC_BUILD_TYPE "Snapshot" CACHE STRING "Set KeePassXC build type to distinguish between stable releases and snapshots") +set_property(CACHE KEEPASSXC_BUILD_TYPE PROPERTY STRINGS Snapshot Release PreRelease) + # Check if on a tag, if so build as a release execute_process(COMMAND git tag --points-at HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_TAG) if(GIT_TAG) - set(KEEPASSXC_RELEASE_BUILD ON) -elseif(NOT KEEPASSXC_RELEASE_BUILD) + string(REGEX REPLACE "\r?\n$" "" GIT_TAG "${GIT_TAG}") + + if(GIT_TAG MATCHES "^[\\.0-9]+-(alpha|beta)[0-9]+$") + set(KEEPASSXC_BUILD_TYPE PreRelease) + set(KEEPASSXC_VERSION ${GIT_TAG}) + elseif(GIT_TAG MATCHES "^[\\.0-9]+$") + set(KEEPASSXC_BUILD_TYPE Release) + set(KEEPASSXC_VERSION ${GIT_TAG}) + endif() +endif() + +if(KEEPASSXC_BUILD_TYPE STREQUAL "PreRelease" AND NOT GIT_TAG) + set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-preview") +elseif(KEEPASSXC_BUILD_TYPE STREQUAL "Snapshot") set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-snapshot") endif() +if(KEEPASSXC_BUILD_TYPE STREQUAL "Release") + set(KEEPASSXC_BUILD_TYPE_RELEASE ON) +elseif(KEEPASSXC_BUILD_TYPE STREQUAL "PreRelease") + set(KEEPASSXC_BUILD_TYPE_PRE_RELEASE ON) +else() + set(KEEPASSXC_BUILD_TYPE_SNAPSHOT ON) +endif() + message(STATUS "Setting up build for KeePassXC v${KEEPASSXC_VERSION}\n") # Distribution info -set(KEEPASSXC_DIST True) -set(KEEPASSXC_DIST_TYPE "Other" CACHE STRING "KeePassXC Distribution type") +set(KEEPASSXC_DIST ON) +set(KEEPASSXC_DIST_TYPE "Other" CACHE STRING "KeePassXC Distribution Type") set_property(CACHE KEEPASSXC_DIST_TYPE PROPERTY STRINGS Snap AppImage Other) if(KEEPASSXC_DIST_TYPE STREQUAL "Snap") - set(KEEPASSXC_DIST_SNAP True) + set(KEEPASSXC_DIST_SNAP ON) elseif(KEEPASSXC_DIST_TYPE STREQUAL "AppImage") - set(KEEPASSXC_DIST_APPIMAGE True) + set(KEEPASSXC_DIST_APPIMAGE ON) elseif(KEEPASSXC_DIST_TYPE STREQUAL "Other") unset(KEEPASSXC_DIST) endif() diff --git a/release-tool b/release-tool index 10e25522c..08e788e13 100755 --- a/release-tool +++ b/release-tool @@ -246,7 +246,7 @@ checkVersionInCMake() { local app_name_upper="$(echo "$APP_NAME" | tr '[:lower:]' '[:upper:]')" local major_num="$(echo ${RELEASE_NAME} | cut -f1 -d.)" local minor_num="$(echo ${RELEASE_NAME} | cut -f2 -d.)" - local patch_num="$(echo ${RELEASE_NAME} | cut -f3 -d.)" + local patch_num="$(echo ${RELEASE_NAME} | cut -f3 -d. | cut -f1 -d-)" grep -q "${app_name_upper}_VERSION_MAJOR \"${major_num}\"" CMakeLists.txt if [ $? -ne 0 ]; then @@ -582,19 +582,26 @@ build() { done init - checkWorkingTreeClean OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" - if $BUILD_SNAPSHOT; then + if ${BUILD_SNAPSHOT}; then TAG_NAME="HEAD" local branch=`git rev-parse --abbrev-ref HEAD` logInfo "Using current branch ${branch} to build..." RELEASE_NAME="${RELEASE_NAME}-snapshot" + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot" else - logInfo "Checking out release tag '${TAG_NAME}'..." + checkWorkingTreeClean + + if $(echo "$TAG_NAME" | grep -qP "\-(alpha|beta)\\d+\$"); then + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=PreRelease" + logInfo "Checking out pre-release tag '${TAG_NAME}'..." + else + CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Release" + logInfo "Checking out release tag '${TAG_NAME}'..." + fi git checkout "$TAG_NAME" - CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_RELEASE_BUILD=ON" fi logInfo "Creating output directory..." @@ -604,20 +611,28 @@ build() { exitError "Failed to create output directory!" fi - if $BUILD_SOURCE_TARBALL; then + if ${BUILD_SOURCE_TARBALL}; then logInfo "Creating source tarball..." local app_name_lower="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]')" TARBALL_NAME="${app_name_lower}-${RELEASE_NAME}-src.tar.xz" git archive --format=tar "$TAG_NAME" --prefix="${app_name_lower}-${RELEASE_NAME}/" \ - | xz -6 > "${OUTPUT_DIR}/${TARBALL_NAME}" + | xz -6 > "${OUTPUT_DIR}/${TARBALL_NAME}" fi - + + if [ -e "${OUTPUT_DIR}/build-release" ]; then + logInfo "Cleaning existing build directory..." + rm -r "${OUTPUT_DIR}/build-release" 2> /dev/null + if [ $? -ne 0 ]; then + exitError "Failed to clean existing build directory, please do it manually." + fi + fi + logInfo "Creating build directory..." mkdir -p "${OUTPUT_DIR}/build-release" cd "${OUTPUT_DIR}/build-release" logInfo "Configuring sources..." - for p in $BUILD_PLUGINS; do + for p in ${BUILD_PLUGINS}; do CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_XC_$(echo $p | tr '[:lower:]' '[:upper:]')=On" done @@ -654,13 +669,13 @@ build() { -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" $CMAKE_OPTIONS "$SRC_DIR" logInfo "Compiling and packaging sources..." - make $MAKE_OPTIONS preinstall + mingw32-make $MAKE_OPTIONS preinstall # Call cpack directly instead of calling make package. # This is important because we want to build the MSI when making a # release. - cpack -G "NSIS;WIX;ZIP" + cpack -G "NSIS;ZIP;${CPACK_GENERATORS}" - mv "./${APP_NAME}-${RELEASE_NAME}-"*.{exe,msi,zip} ../ + mv "./${APP_NAME}-${RELEASE_NAME}-"*.* ../ else mkdir -p "${OUTPUT_DIR}/bin-release" diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index e1f14d5bc..92dab488a 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -269,8 +269,8 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow) } /** - * Global Autotype entry-point funcion - * Perform global autotype on the active window + * Global Autotype entry-point function + * Perform global Auto-Type on the active window */ void AutoType::performGlobalAutoType(const QList& dbList) { @@ -304,10 +304,19 @@ void AutoType::performGlobalAutoType(const QList& dbList) if (matchList.isEmpty()) { m_inAutoType.unlock(); - QString message = tr("Couldn't find an entry that matches the window title:"); - message.append("\n\n"); - message.append(windowTitle); - MessageBox::information(nullptr, tr("Auto-Type - KeePassXC"), message); + + if (qobject_cast(QCoreApplication::instance())) { + auto* msgBox = new QMessageBox(); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->setWindowTitle(tr("Auto-Type - KeePassXC")); + msgBox->setText(tr("Couldn't find an entry that matches the window title:").append("\n\n") + .append(windowTitle)); + msgBox->setIcon(QMessageBox::Information); + msgBox->setStandardButtons(QMessageBox::Ok); + msgBox->show(); + msgBox->raise(); + msgBox->activateWindow(); + } emit autotypeRejected(); } else if ((matchList.size() == 1) && !config()->get("security/autotypeask").toBool()) { @@ -315,7 +324,7 @@ void AutoType::performGlobalAutoType(const QList& dbList) m_inAutoType.unlock(); } else { m_windowFromGlobal = m_plugin->activeWindow(); - AutoTypeSelectDialog* selectDialog = new AutoTypeSelectDialog(); + auto* selectDialog = new AutoTypeSelectDialog(); connect(selectDialog, SIGNAL(matchActivated(AutoTypeMatch)), SLOT(performAutoTypeFromGlobal(AutoTypeMatch))); connect(selectDialog, SIGNAL(rejected()), SLOT(resetInAutoType())); diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index 805438b1a..9f3952b04 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -3,14 +3,14 @@ #ifndef KEEPASSX_CONFIG_KEEPASSX_H #define KEEPASSX_CONFIG_KEEPASSX_H -#define KEEPASSX_VERSION "${KEEPASSXC_VERSION}" +#define KEEPASSX_VERSION "@KEEPASSXC_VERSION@" -#define KEEPASSX_SOURCE_DIR "${CMAKE_SOURCE_DIR}" -#define KEEPASSX_BINARY_DIR "${CMAKE_BINARY_DIR}" +#define KEEPASSX_SOURCE_DIR "@CMAKE_SOURCE_DIR@" +#define KEEPASSX_BINARY_DIR "@CMAKE_BINARY_DIR@" -#define KEEPASSX_PREFIX_DIR "${CMAKE_INSTALL_PREFIX}" -#define KEEPASSX_PLUGIN_DIR "${PLUGIN_INSTALL_DIR}" -#define KEEPASSX_DATA_DIR "${DATA_INSTALL_DIR}" +#define KEEPASSX_PREFIX_DIR "@CMAKE_INSTALL_PREFIX@" +#define KEEPASSX_PLUGIN_DIR "@PLUGIN_INSTALL_DIR@" +#define KEEPASSX_DATA_DIR "@DATA_INSTALL_DIR@" #cmakedefine WITH_XC_AUTOTYPE #cmakedefine WITH_XC_NETWORKING @@ -19,13 +19,16 @@ #cmakedefine WITH_XC_YUBIKEY #cmakedefine WITH_XC_SSHAGENT +#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@" +#cmakedefine KEEPASSXC_BUILD_TYPE_RELEASE +#cmakedefine KEEPASSXC_BUILD_TYPE_PRE_RELEASE +#cmakedefine KEEPASSXC_BUILD_TYPE_SNAPSHOT + #cmakedefine KEEPASSXC_DIST #cmakedefine KEEPASSXC_DIST_TYPE "@KEEPASSXC_DIST_TYPE@" #cmakedefine KEEPASSXC_DIST_SNAP #cmakedefine KEEPASSXC_DIST_APPIMAGE -#cmakedefine KEEPASSXC_RELEASE_BUILD - #cmakedefine HAVE_PR_SET_DUMPABLE 1 #cmakedefine HAVE_RLIMIT_CORE 1 #cmakedefine HAVE_PT_DENY_ATTACH 1 diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 8db955c93..1dfc614e5 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -772,23 +772,41 @@ QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxD QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDepth) const { + if (maxDepth <= 0) { + qWarning("Maximum depth of replacement has been reached. Entry uuid: %s", qPrintable(uuid().toHex())); + return placeholder; + } + const PlaceholderType typeOfPlaceholder = placeholderType(placeholder); switch (typeOfPlaceholder) { case PlaceholderType::NotPlaceholder: case PlaceholderType::Unknown: return placeholder; case PlaceholderType::Title: - return title(); + if (placeholderType(title()) == PlaceholderType::Title) { + return title(); + } + return resolvePlaceholderRecursive(title(), maxDepth - 1); case PlaceholderType::UserName: - return username(); + if (placeholderType(username()) == PlaceholderType::UserName) { + return username(); + } + return resolvePlaceholderRecursive(username(), maxDepth - 1); case PlaceholderType::Password: - return password(); + if (placeholderType(password()) == PlaceholderType::Password) { + return password(); + } + return resolvePlaceholderRecursive(password(), maxDepth - 1); case PlaceholderType::Notes: - return notes(); - case PlaceholderType::Totp: - return totp(); + if (placeholderType(notes()) == PlaceholderType::Notes) { + return notes(); + } + return resolvePlaceholderRecursive(notes(), maxDepth - 1); case PlaceholderType::Url: - return url(); + if (placeholderType(url()) == PlaceholderType::Url) { + return url(); + } + return resolvePlaceholderRecursive(url(), maxDepth - 1); case PlaceholderType::UrlWithoutScheme: case PlaceholderType::UrlScheme: case PlaceholderType::UrlHost: @@ -802,6 +820,9 @@ QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDe const QString strUrl = resolveMultiplePlaceholdersRecursive(url(), maxDepth - 1); return resolveUrlPlaceholder(strUrl, typeOfPlaceholder); } + case PlaceholderType::Totp: + // totp can't have placeholder inside + return totp(); case PlaceholderType::CustomAttribute: { const QString key = placeholder.mid(3, placeholder.length() - 4); // {S:attr} => mid(3, len - 4) return attributes()->hasKey(key) ? attributes()->value(key) : QString(); @@ -815,6 +836,11 @@ QString Entry::resolvePlaceholderRecursive(const QString& placeholder, int maxDe QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const { + if (maxDepth <= 0) { + qWarning("Maximum depth of replacement has been reached. Entry uuid: %s", qPrintable(uuid().toHex())); + return placeholder; + } + // resolving references in format: {REF:@:} // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing @@ -828,6 +854,9 @@ QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder, const QString searchText = match.captured(EntryAttributes::SearchTextGroupName); const EntryReferenceType searchInType = Entry::referenceType(searchIn); + + Q_ASSERT(m_group); + Q_ASSERT(m_group->database()); const Entry* refEntry = m_group->database()->resolveEntry(searchText, searchInType); if (refEntry) { diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp index 3d08bb55e..f30dc64b4 100644 --- a/src/format/KdbxXmlReader.cpp +++ b/src/format/KdbxXmlReader.cpp @@ -700,7 +700,8 @@ Entry* KdbxXmlReader::parseEntry(bool history) entry->setIcon(uuid); } continue; - }if (m_xml.name() == "ForegroundColor") { + } + if (m_xml.name() == "ForegroundColor") { entry->setForegroundColor(readColor()); continue; } diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 4481dd82d..adfdea0a7 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -54,8 +54,8 @@ AboutDialog::AboutDialog(QWidget* parent) QString debugInfo = "KeePassXC - "; debugInfo.append(tr("Version %1\n").arg(KEEPASSX_VERSION)); -#ifndef KEEPASSXC_RELEASE_BUILD - debugInfo.append(tr("Build Type: Snapshot\n")); +#ifndef KEEPASSXC_BUILD_TYPE_RELEASE + debugInfo.append(tr("Build Type: %1\n").arg(KEEPASSXC_BUILD_TYPE)); #endif if (!commitHash.isEmpty()) { debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); diff --git a/src/gui/AboutDialog.ui b/src/gui/AboutDialog.ui index 43c3da3f9..39963c121 100644 --- a/src/gui/AboutDialog.ui +++ b/src/gui/AboutDialog.ui @@ -147,6 +147,13 @@ + + + + Project Maintainers: + + + @@ -156,15 +163,29 @@ - <p>Project Maintainers:</p> -<ul> + <ul> <li>droidmonkey</li> <li>phoerious</li> <li>TheZ3ro</li> <li>louib</li> <li>weslly</li> -</ul> -<p>Special thanks from the KeePassXC team go to debfx for creating the original KeePassX.</> +</ul> + + + + + + + + 0 + 0 + + + + Special thanks from the KeePassXC team go to debfx for creating the original KeePassX. + + + true @@ -198,8 +219,8 @@ 0 0 - 423 - 816 + 449 + 803 diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index cd57f6292..fda7586d7 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -776,21 +776,18 @@ void DatabaseWidget::switchToView(bool accepted) m_newGroup->setParent(m_newParent); m_groupView->setCurrentGroup(m_newGroup); m_groupView->expandGroup(m_newParent); - } - else { + } else { delete m_newGroup; } m_newGroup = nullptr; m_newParent = nullptr; - } - else if (m_newEntry) { + } else if (m_newEntry) { if (accepted) { m_newEntry->setGroup(m_newParent); m_entryView->setFocus(); m_entryView->setCurrentEntry(m_newEntry); - } - else { + } else { delete m_newEntry; } @@ -798,6 +795,10 @@ void DatabaseWidget::switchToView(bool accepted) m_newParent = nullptr; } + if (accepted) { + showMessage(tr("Entry updated successfully."), MessageWidget::Positive, false, 2000); + } + setCurrentWidget(m_mainWidget); } @@ -819,7 +820,16 @@ void DatabaseWidget::switchToEntryEdit(Entry* entry) void DatabaseWidget::switchToEntryEdit(Entry* entry, bool create) { - Group* group = currentGroup(); + // If creating an entry, it will be in `currentGroup()` so it's + // okay to use but when editing, the entry may not be in + // `currentGroup()` so we get the entry's group. + Group* group; + if (create) { + group = currentGroup(); + } else { + group = entry->group(); + } + Q_ASSERT(group); m_editEntryWidget->loadEntry(entry, create, false, group->name(), m_db); diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index daa2f7922..65c6306e1 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -119,7 +119,8 @@ bool EditWidget::readOnly() const void EditWidget::showMessage(const QString& text, MessageWidget::MessageType type) { - m_ui->messageWidget->showMessage(text, type); + m_ui->messageWidget->setCloseButtonVisible(false); + m_ui->messageWidget->showMessage(text, type, 2000); } void EditWidget::hideMessage() diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 6fad65859..76f1ffb60 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -425,7 +425,7 @@ MainWindow::MainWindow() } #endif -#ifndef KEEPASSXC_RELEASE_BUILD +#ifndef KEEPASSXC_BUILD_TYPE_RELEASE m_ui->globalMessageWidget->showMessage(tr("WARNING: You are using an unstable build of KeePassXC!\n" "There is a high risk of corruption, maintain a backup of your databases.\n" "This version is not meant for production use."), diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index e6cb0a92d..7a93f86bd 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -151,6 +151,11 @@ void PasswordGeneratorWidget::setStandaloneMode(bool standalone) } } +QString PasswordGeneratorWidget::getGeneratedPassword() +{ + return m_ui->editNewPassword->text(); +} + void PasswordGeneratorWidget::keyPressEvent(QKeyEvent* e) { if (e->key() == Qt::Key_Escape && m_standalone == true) { diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index 3d6d27a55..ed4414377 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -49,16 +49,18 @@ public: void saveSettings(); void reset(); void setStandaloneMode(bool standalone); -public Q_SLOTS: + QString getGeneratedPassword(); + +public slots: void regeneratePassword(); + void applyPassword(); + void copyPassword(); signals: void appliedPassword(const QString& password); void dialogTerminated(); private slots: - void applyPassword(); - void copyPassword(); void updateButtonsEnabled(const QString& password); void updatePasswordStrength(const QString& password); void togglePasswordShown(bool hidden); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index bab5a0728..c30557574 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "autotype/AutoType.h" #include "core/Config.h" @@ -97,7 +98,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) connect(this, SIGNAL(accepted()), SLOT(acceptEntry())); connect(this, SIGNAL(rejected()), SLOT(cancel())); - connect(this, SIGNAL(apply()), SLOT(saveEntry())); + connect(this, SIGNAL(apply()), SLOT(commitEntry())); connect(m_iconsWidget, SIGNAL(messageEditEntry(QString, MessageWidget::MessageType)), SLOT(showMessage(QString, MessageWidget::MessageType))); connect(m_iconsWidget, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage())); @@ -127,7 +128,7 @@ void EditEntryWidget::setupMain() QAction *action = new QAction(this); action->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(action, SIGNAL(triggered()), this, SLOT(saveEntry())); + connect(action, SIGNAL(triggered()), this, SLOT(commitEntry())); this->addAction(action); m_mainUi->passwordGenerator->hide(); @@ -156,6 +157,8 @@ void EditEntryWidget::setupAdvanced() connect(m_advancedUi->attributesView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(updateCurrentAttribute())); + connect(m_advancedUi->fgColorButton, SIGNAL(clicked()), SLOT(pickColor())); + connect(m_advancedUi->bgColorButton, SIGNAL(clicked()), SLOT(pickColor())); } void EditEntryWidget::setupIcon() @@ -188,6 +191,8 @@ void EditEntryWidget::setupAutoType() connect(m_autoTypeAssocModel, SIGNAL(modelReset()), SLOT(clearCurrentAssoc())); connect(m_autoTypeUi->windowTitleCombo, SIGNAL(editTextChanged(QString)), SLOT(applyCurrentAssoc())); + connect(m_autoTypeUi->customWindowSequenceButton, SIGNAL(toggled(bool)), + SLOT(applyCurrentAssoc())); connect(m_autoTypeUi->windowSequenceEdit, SIGNAL(textChanged(QString)), SLOT(applyCurrentAssoc())); } @@ -591,6 +596,8 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) editTriggers = QAbstractItemView::DoubleClicked; } m_advancedUi->attributesView->setEditTriggers(editTriggers); + setupColorButton(true, entry->foregroundColor()); + setupColorButton(false, entry->backgroundColor()); m_iconsWidget->setEnabled(!m_history); m_autoTypeUi->sequenceEdit->setReadOnly(m_history); m_autoTypeUi->windowTitleCombo->lineEdit()->setReadOnly(m_history); @@ -676,20 +683,39 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_mainUi->titleEdit->setFocus(); } -void EditEntryWidget::saveEntry() +/** + * Commit the form values to in-memory database representation + * + * @return true is commit successful, otherwise false + */ +bool EditEntryWidget::commitEntry() { if (m_history) { clear(); hideMessage(); emit editFinished(false); - return; + return true; } if (!passwordsEqual()) { showMessage(tr("Different passwords supplied."), MessageWidget::Error); - return; + return false; } + // Ask the user to apply the generator password, if open + if (m_mainUi->togglePasswordGeneratorButton->isChecked() && + m_mainUi->passwordGenerator->getGeneratedPassword() != m_mainUi->passwordEdit->text()) { + auto answer = MessageBox::question(this, tr("Apply generated password?"), + tr("Do you want to apply the generated password to this entry?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + if (answer == QMessageBox::Yes) { + m_mainUi->passwordGenerator->applyPassword(); + } + } + + // Hide the password generator + m_mainUi->togglePasswordGeneratorButton->setChecked(false); + if (m_advancedUi->attributesView->currentIndex().isValid() && m_advancedUi->attributesEdit->isEnabled()) { QString key = m_attributesModel->keyByIndex(m_advancedUi->attributesView->currentIndex()); m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), @@ -727,19 +753,18 @@ void EditEntryWidget::saveEntry() updateSSHAgent(); } #endif + + showMessage(tr("Entry updated successfully."), MessageWidget::Positive); + return true; } void EditEntryWidget::acceptEntry() { - // Check if passwords are mismatched first to prevent saving - if (!passwordsEqual()) { - showMessage(tr("Different passwords supplied."), MessageWidget::Error); - return; + if (commitEntry()) { + clear(); + hideMessage(); + emit editFinished(true); } - - saveEntry(); - clear(); - emit editFinished(true); } void EditEntryWidget::updateEntryData(Entry* entry) const @@ -756,26 +781,35 @@ 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())); + } else { + entry->setForegroundColor(QColor()); + } + + if (m_advancedUi->bgColorCheckBox->isChecked() && + m_advancedUi->bgColorButton->property("color").isValid()) { + entry->setBackgroundColor(QColor(m_advancedUi->bgColorButton->property("color").toString())); + } else { + entry->setBackgroundColor(QColor()); + } + IconStruct iconStruct = m_iconsWidget->state(); if (iconStruct.number < 0) { entry->setIcon(Entry::DefaultIconNumber); - } - else if (iconStruct.uuid.isNull()) { + } else if (iconStruct.uuid.isNull()) { entry->setIcon(iconStruct.number); - } - else { + } else { entry->setIcon(iconStruct.uuid); } entry->setAutoTypeEnabled(m_autoTypeUi->enableButton->isChecked()); if (m_autoTypeUi->inheritSequenceButton->isChecked()) { entry->setDefaultAutoTypeSequence(QString()); - } - else { - if (AutoType::verifyAutoTypeSyntax(m_autoTypeUi->sequenceEdit->text())) { - entry->setDefaultAutoTypeSequence(m_autoTypeUi->sequenceEdit->text()); - } + } else if (AutoType::verifyAutoTypeSyntax(m_autoTypeUi->sequenceEdit->text())) { + entry->setDefaultAutoTypeSequence(m_autoTypeUi->sequenceEdit->text()); } entry->autoTypeAssociations()->copyDataFrom(m_autoTypeAssoc); @@ -1120,3 +1154,38 @@ QMenu* EditEntryWidget::createPresetsMenu() expirePresetsMenu->addAction(tr("1 year"))->setData(QVariant::fromValue(TimeDelta::fromYears(1))); return expirePresetsMenu; } + +void EditEntryWidget::setupColorButton(bool foreground, const QColor& color) +{ + QWidget* button = m_advancedUi->fgColorButton; + QCheckBox* checkBox = m_advancedUi->fgColorCheckBox; + if (!foreground) { + button = m_advancedUi->bgColorButton; + checkBox = m_advancedUi->bgColorCheckBox; + } + + if (color.isValid()) { + button->setStyleSheet(QString("background-color:%1").arg(color.name())); + button->setProperty("color", color.name()); + checkBox->setChecked(true); + } else { + button->setStyleSheet(""); + button->setProperty("color", QVariant()); + checkBox->setChecked(false); + } +} + +void EditEntryWidget::pickColor() +{ + bool isForeground = (sender() == m_advancedUi->fgColorButton); + QColor oldColor = QColor(m_advancedUi->fgColorButton->property("color").toString()); + if (!isForeground) { + oldColor = QColor(m_advancedUi->bgColorButton->property("color").toString()); + } + + QColorDialog colorDialog(this); + QColor newColor = colorDialog.getColor(oldColor); + if (newColor.isValid()) { + setupColorButton(isForeground, newColor); + } +} diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index bd9f5cd0f..66d89dbfb 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -74,7 +74,7 @@ signals: private slots: void acceptEntry(); - void saveEntry(); + bool commitEntry(); void cancel(); void togglePasswordGeneratorButton(bool checked); void setGeneratedPassword(const QString& password); @@ -99,6 +99,7 @@ private slots: void updateHistoryButtons(const QModelIndex& current, const QModelIndex& previous); void useExpiryPreset(QAction* action); void toggleHideNotes(bool visible); + void pickColor(); #ifdef WITH_XC_SSHAGENT void updateSSHAgent(); void updateSSHAgentAttachment(); @@ -120,6 +121,7 @@ private: #endif void setupProperties(); void setupHistory(); + void setupColorButton(bool foreground, const QColor& color); bool passwordsEqual(); void setForms(const Entry* entry, bool restore = false); diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui index 8c729fd7c..9556eee19 100644 --- a/src/gui/entry/EditEntryWidgetAdvanced.ui +++ b/src/gui/entry/EditEntryWidgetAdvanced.ui @@ -2,19 +2,15 @@ EditEntryWidgetAdvanced + + + 0 + 0 + 532 + 364 + + - - 0 - - - 0 - - - 0 - - - 0 - @@ -157,6 +153,96 @@ + + + + + + + Foreground Color: + + + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 30 + 20 + + + + + + + + Background Color: + + + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 4a583b30e..71828c6ed 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -18,6 +18,7 @@ #include "EntryModel.h" #include +#include #include #include #include @@ -263,10 +264,26 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const font.setStrikeOut(true); } return font; - } else if (role == Qt::TextColorRole) { + } else if (role == Qt::ForegroundRole) { if (entry->hasReferences()) { QPalette p; return QVariant(p.color(QPalette::Active, QPalette::Mid)); + } else if (entry->foregroundColor().isValid()) { + return QVariant(entry->foregroundColor()); + } + } else if (role == Qt::BackgroundRole) { + if (entry->backgroundColor().isValid()) { + return QVariant(entry->backgroundColor()); + } + } else if (role == Qt::TextAlignmentRole) { + if (index.column() == Paperclip) { + return Qt::AlignCenter; + } + } else if (role == Qt::SizeHintRole) { + if (index.column() == Paperclip) { + QFont font; + QFontMetrics fm(font); + return fm.width(PaperClipDisplay) / 2; } } @@ -275,7 +292,9 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + Q_UNUSED(orientation); + + if (role == Qt::DisplayRole) { switch (section) { case ParentGroup: return tr("Group"); @@ -302,6 +321,11 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro case Attachments: return tr("Attachments"); } + } else if (role == Qt::TextAlignmentRole) { + switch (section) { + case Paperclip: + return Qt::AlignCenter; + } } return QVariant(); diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 264f202d1..94100c0a6 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -1,4 +1,5 @@ /* + * Copyright (C) 2018 KeePassXC Team * Copyright (C) 2013 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -15,6 +16,8 @@ * along with this program. If not, see . */ +#include + #include "TestEntry.h" #include "TestGlobal.h" #include "crypto/Crypto.h" @@ -28,7 +31,7 @@ void TestEntry::initTestCase() void TestEntry::testHistoryItemDeletion() { - Entry* entry = new Entry(); + QScopedPointer entry(new Entry()); QPointer historyEntry = new Entry(); entry->addHistoryItem(historyEntry); @@ -39,13 +42,11 @@ void TestEntry::testHistoryItemDeletion() entry->removeHistoryItems(historyEntriesToRemove); QCOMPARE(entry->historyItems().size(), 0); QVERIFY(historyEntry.isNull()); - - delete entry; } void TestEntry::testCopyDataFrom() { - Entry* entry = new Entry(); + QScopedPointer entry(new Entry()); entry->setTitle("testtitle"); entry->attributes()->set("attr1", "abc"); @@ -62,9 +63,8 @@ void TestEntry::testCopyDataFrom() assoc.sequence = "4"; entry->autoTypeAssociations()->add(assoc); - Entry* entry2 = new Entry(); - entry2->copyDataFrom(entry); - delete entry; + QScopedPointer entry2(new Entry()); + entry2->copyDataFrom(entry.data()); QCOMPARE(entry2->title(), QString("testtitle")); QCOMPARE(entry2->attributes()->value("attr1"), QString("abc")); @@ -77,13 +77,11 @@ void TestEntry::testCopyDataFrom() QCOMPARE(entry2->autoTypeAssociations()->size(), 2); QCOMPARE(entry2->autoTypeAssociations()->get(0).window, QString("1")); QCOMPARE(entry2->autoTypeAssociations()->get(1).window, QString("3")); - - delete entry2; } void TestEntry::testClone() { - Entry* entryOrg = new Entry(); + QScopedPointer entryOrg(new Entry()); entryOrg->setUuid(Uuid::random()); entryOrg->setTitle("Original Title"); entryOrg->beginUpdate(); @@ -96,42 +94,58 @@ void TestEntry::testClone() entryOrgTime.setCreationTime(dateTime); entryOrg->setTimeInfo(entryOrgTime); - Entry* entryCloneNone = entryOrg->clone(Entry::CloneNoFlags); + QScopedPointer entryCloneNone(entryOrg->clone(Entry::CloneNoFlags)); QCOMPARE(entryCloneNone->uuid(), entryOrg->uuid()); QCOMPARE(entryCloneNone->title(), QString("New Title")); QCOMPARE(entryCloneNone->historyItems().size(), 0); QCOMPARE(entryCloneNone->timeInfo().creationTime(), entryOrg->timeInfo().creationTime()); - delete entryCloneNone; - Entry* entryCloneNewUuid = entryOrg->clone(Entry::CloneNewUuid); + QScopedPointer entryCloneNewUuid(entryOrg->clone(Entry::CloneNewUuid)); QVERIFY(entryCloneNewUuid->uuid() != entryOrg->uuid()); QVERIFY(!entryCloneNewUuid->uuid().isNull()); QCOMPARE(entryCloneNewUuid->title(), QString("New Title")); QCOMPARE(entryCloneNewUuid->historyItems().size(), 0); QCOMPARE(entryCloneNewUuid->timeInfo().creationTime(), entryOrg->timeInfo().creationTime()); - delete entryCloneNewUuid; - Entry* entryCloneResetTime = entryOrg->clone(Entry::CloneResetTimeInfo); + QScopedPointer entryCloneResetTime(entryOrg->clone(Entry::CloneResetTimeInfo)); QCOMPARE(entryCloneResetTime->uuid(), entryOrg->uuid()); QCOMPARE(entryCloneResetTime->title(), QString("New Title")); QCOMPARE(entryCloneResetTime->historyItems().size(), 0); QVERIFY(entryCloneResetTime->timeInfo().creationTime() != entryOrg->timeInfo().creationTime()); - delete entryCloneResetTime; - Entry* entryCloneHistory = entryOrg->clone(Entry::CloneIncludeHistory); + QScopedPointer entryCloneHistory(entryOrg->clone(Entry::CloneIncludeHistory)); QCOMPARE(entryCloneHistory->uuid(), entryOrg->uuid()); QCOMPARE(entryCloneHistory->title(), QString("New Title")); QCOMPARE(entryCloneHistory->historyItems().size(), 1); QCOMPARE(entryCloneHistory->historyItems().at(0)->title(), QString("Original Title")); QCOMPARE(entryCloneHistory->timeInfo().creationTime(), entryOrg->timeInfo().creationTime()); - delete entryCloneHistory; - delete entryOrg; + Database db; + auto* entryOrgClone = entryOrg->clone(Entry::CloneNoFlags); + entryOrgClone->setGroup(db.rootGroup()); + + Entry* entryCloneUserRef = entryOrgClone->clone(Entry::CloneUserAsRef); + entryCloneUserRef->setGroup(db.rootGroup()); + QCOMPARE(entryCloneUserRef->uuid(), entryOrgClone->uuid()); + QCOMPARE(entryCloneUserRef->title(), QString("New Title")); + QCOMPARE(entryCloneUserRef->historyItems().size(), 0); + QCOMPARE(entryCloneUserRef->timeInfo().creationTime(), entryOrgClone->timeInfo().creationTime()); + QVERIFY(entryCloneUserRef->attributes()->isReference(EntryAttributes::UserNameKey)); + QCOMPARE(entryCloneUserRef->resolvePlaceholder(entryCloneUserRef->username()), entryOrgClone->username()); + + Entry* entryClonePassRef = entryOrgClone->clone(Entry::ClonePassAsRef); + entryClonePassRef->setGroup(db.rootGroup()); + QCOMPARE(entryClonePassRef->uuid(), entryOrgClone->uuid()); + QCOMPARE(entryClonePassRef->title(), QString("New Title")); + QCOMPARE(entryClonePassRef->historyItems().size(), 0); + QCOMPARE(entryClonePassRef->timeInfo().creationTime(), entryOrgClone->timeInfo().creationTime()); + QVERIFY(entryClonePassRef->attributes()->isReference(EntryAttributes::PasswordKey)); + QCOMPARE(entryClonePassRef->resolvePlaceholder(entryCloneUserRef->password()), entryOrg->password()); } void TestEntry::testResolveUrl() { - Entry* entry = new Entry(); + QScopedPointer entry(new Entry()); QString testUrl("www.google.com"); QString testCmd("cmd://firefox " + testUrl); QString testComplexCmd("cmd://firefox --start-now --url 'http://" + testUrl + "' --quit"); @@ -152,8 +166,6 @@ void TestEntry::testResolveUrl() QCOMPARE(entry->resolveUrl(nonHttpUrl), QString("")); // Test no URL QCOMPARE(entry->resolveUrl(noUrl), QString("")); - - delete entry; } void TestEntry::testResolveUrlPlaceholders() @@ -189,9 +201,9 @@ void TestEntry::testResolveUrlPlaceholders() void TestEntry::testResolveRecursivePlaceholders() { Database db; - Group* root = db.rootGroup(); + auto* root = db.rootGroup(); - Entry* entry1 = new Entry(); + auto* entry1 = new Entry(); entry1->setGroup(root); entry1->setUuid(Uuid::random()); entry1->setTitle("{USERNAME}"); @@ -201,7 +213,7 @@ void TestEntry::testResolveRecursivePlaceholders() entry1->attributes()->set("CustomTitle", "RecursiveValue"); QCOMPARE(entry1->resolveMultiplePlaceholders(entry1->title()), QString("RecursiveValue")); - Entry* entry2 = new Entry(); + auto* entry2 = new Entry(); entry2->setGroup(root); entry2->setUuid(Uuid::random()); entry2->setTitle("Entry2Title"); @@ -213,7 +225,7 @@ void TestEntry::testResolveRecursivePlaceholders() entry2->attributes()->set("Port", "1234"); entry2->attributes()->set("Uri", "uri/path"); - Entry* entry3 = new Entry(); + auto* entry3 = new Entry(); entry3->setGroup(root); entry3->setUuid(Uuid::random()); entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuid().toHex())); @@ -226,7 +238,7 @@ void TestEntry::testResolveRecursivePlaceholders() QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->password()), QString("RecursiveValue")); QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), QString("http://127.0.0.1:1234/uri/path")); - Entry* entry4 = new Entry(); + auto* entry4 = new Entry(); entry4->setGroup(root); entry4->setUuid(Uuid::random()); entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())); @@ -239,7 +251,7 @@ void TestEntry::testResolveRecursivePlaceholders() QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->password()), QString("RecursiveValue")); QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->url()), QString("http://127.0.0.1:1234/uri/path")); - Entry* entry5 = new Entry(); + auto* entry5 = new Entry(); entry5->setGroup(root); entry5->setUuid(Uuid::random()); entry5->attributes()->set("Scheme", "http"); @@ -256,14 +268,25 @@ void TestEntry::testResolveRecursivePlaceholders() const QString url("http://username:password@host.org:2017/some/path?q=e&t=s#fragment"); QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->url()), url); QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->title()), QString("title+/some/path+fragment+title")); + + auto* entry6 = new Entry(); + entry6->setGroup(root); + entry6->setUuid(Uuid::random()); + entry6->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())); + entry6->setUsername(QString("{TITLE}")); + entry6->setPassword(QString("{PASSWORD}")); + + QCOMPARE(entry6->resolvePlaceholder(entry6->title()), QString("Entry2Title")); + QCOMPARE(entry6->resolvePlaceholder(entry6->username()), QString("Entry2Title")); + QCOMPARE(entry6->resolvePlaceholder(entry6->password()), QString("{PASSWORD}")); } void TestEntry::testResolveReferencePlaceholders() { Database db; - Group* root = db.rootGroup(); + auto* root = db.rootGroup(); - Entry* entry1 = new Entry(); + auto* entry1 = new Entry(); entry1->setGroup(root); entry1->setUuid(Uuid::random()); entry1->setTitle("Title1"); @@ -273,9 +296,9 @@ void TestEntry::testResolveReferencePlaceholders() entry1->setNotes("Notes1"); entry1->attributes()->set("CustomAttribute1", "CustomAttributeValue1"); - Group* group = new Group(); + auto* group = new Group(); group->setParent(root); - Entry* entry2 = new Entry(); + auto* entry2 = new Entry(); entry2->setGroup(group); entry2->setUuid(Uuid::random()); entry2->setTitle("Title2"); @@ -285,7 +308,7 @@ void TestEntry::testResolveReferencePlaceholders() entry2->setNotes("Notes2"); entry2->attributes()->set("CustomAttribute2", "CustomAttributeValue2"); - Entry* entry3 = new Entry(); + auto* entry3 = new Entry(); entry3->setGroup(group); entry3->setUuid(Uuid::random()); entry3->setTitle("{S:AttributeTitle}"); @@ -299,7 +322,7 @@ void TestEntry::testResolveReferencePlaceholders() entry3->attributes()->set("AttributeUrl", "UrlValue"); entry3->attributes()->set("AttributeNotes", "NotesValue"); - Entry* tstEntry = new Entry(); + auto* tstEntry = new Entry(); tstEntry->setGroup(root); tstEntry->setUuid(Uuid::random()); @@ -356,67 +379,67 @@ void TestEntry::testResolveNonIdPlaceholdersToUuid() Database db; auto* root = db.rootGroup(); - Entry referencedEntryTitle; - referencedEntryTitle.setGroup(root); - referencedEntryTitle.setTitle("myTitle"); - referencedEntryTitle.setUuid(Uuid::random()); + auto* referencedEntryTitle = new Entry(); + referencedEntryTitle->setGroup(root); + referencedEntryTitle->setTitle("myTitle"); + referencedEntryTitle->setUuid(Uuid::random()); - Entry referencedEntryUsername; - referencedEntryUsername.setGroup(root); - referencedEntryUsername.setUsername("myUser"); - referencedEntryUsername.setUuid(Uuid::random()); + auto* referencedEntryUsername = new Entry(); + referencedEntryUsername->setGroup(root); + referencedEntryUsername->setUsername("myUser"); + referencedEntryUsername->setUuid(Uuid::random()); - Entry referencedEntryPassword; - referencedEntryPassword.setGroup(root); - referencedEntryPassword.setPassword("myPassword"); - referencedEntryPassword.setUuid(Uuid::random()); + auto* referencedEntryPassword = new Entry(); + referencedEntryPassword->setGroup(root); + referencedEntryPassword->setPassword("myPassword"); + referencedEntryPassword->setUuid(Uuid::random()); - Entry referencedEntryUrl; - referencedEntryUrl.setGroup(root); - referencedEntryUrl.setUrl("myUrl"); - referencedEntryUrl.setUuid(Uuid::random()); + auto* referencedEntryUrl = new Entry(); + referencedEntryUrl->setGroup(root); + referencedEntryUrl->setUrl("myUrl"); + referencedEntryUrl->setUuid(Uuid::random()); - Entry referencedEntryNotes; - referencedEntryNotes.setGroup(root); - referencedEntryNotes.setNotes("myNotes"); - referencedEntryNotes.setUuid(Uuid::random()); + auto* referencedEntryNotes = new Entry(); + referencedEntryNotes->setGroup(root); + referencedEntryNotes->setNotes("myNotes"); + referencedEntryNotes->setUuid(Uuid::random()); const QList placeholders{'T', 'U', 'P', 'A', 'N'}; - for (const QChar searchIn : placeholders) { + for (const QChar& searchIn : placeholders) { const Entry* referencedEntry = nullptr; QString newEntryNotesRaw("{REF:I@%1:%2}"); switch(searchIn.toLatin1()) { case 'T': - referencedEntry = &referencedEntryTitle; + referencedEntry = referencedEntryTitle; newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->title()); break; case 'U': - referencedEntry = &referencedEntryUsername; + referencedEntry = referencedEntryUsername; newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->username()); break; case 'P': - referencedEntry = &referencedEntryPassword; + referencedEntry = referencedEntryPassword; newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->password()); break; case 'A': - referencedEntry = &referencedEntryUrl; + referencedEntry = referencedEntryUrl; newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->url()); break; case 'N': - referencedEntry = &referencedEntryNotes; + referencedEntry = referencedEntryNotes; newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->notes()); break; default: break; } - Entry newEntry; - newEntry.setGroup(root); - newEntry.setNotes(newEntryNotesRaw); + auto* newEntry = new Entry(); + newEntry->setGroup(root); + newEntry->setNotes(newEntryNotesRaw); - const auto newEntryNotesResolved = - newEntry.resolveMultiplePlaceholders(newEntry.notes()); + const QString newEntryNotesResolved = + newEntry->resolveMultiplePlaceholders(newEntry->notes()); QCOMPARE(newEntryNotesResolved, QString(referencedEntry->uuid().toHex())); } } @@ -424,9 +447,9 @@ void TestEntry::testResolveNonIdPlaceholdersToUuid() void TestEntry::testResolveClonedEntry() { Database db; - Group* root = db.rootGroup(); + auto* root = db.rootGroup(); - Entry* original = new Entry(); + auto* original = new Entry(); original->setGroup(root); original->setUuid(Uuid::random()); original->setTitle("Title"); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index cf7b969e1..2e11fcf59 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include "gui/entry/EntryView.h" #include "gui/group/GroupModel.h" #include "gui/group/GroupView.h" +#include "gui/group/EditGroupWidget.h" #include "keys/PasswordKey.h" void TestGui::initTestCase() @@ -278,6 +280,7 @@ void TestGui::testTabs() void TestGui::testEditEntry() { QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + int editCount = 0; // Select the first entry in the database EntryView* entryView = m_dbWidget->findChild("entryView"); @@ -304,7 +307,24 @@ void TestGui::testEditEntry() QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); QCOMPARE(entry->title(), QString("Sample Entry_test")); - QCOMPARE(entry->historyItems().size(), 1); + QCOMPARE(entry->historyItems().size(), ++editCount); + + // Test entry colors (simulate choosing a color) + editEntryWidget->setCurrentPage(1); + auto fgColor = QColor(Qt::red); + auto bgColor = QColor(Qt::blue); + // Set foreground color + auto colorButton = editEntryWidget->findChild("fgColorButton"); + auto colorCheckBox = editEntryWidget->findChild("fgColorCheckBox"); + colorButton->setProperty("color", fgColor); + colorCheckBox->setChecked(true); + // Set background color + colorButton = editEntryWidget->findChild("bgColorButton"); + colorCheckBox = editEntryWidget->findChild("bgColorCheckBox"); + colorButton->setProperty("color", bgColor); + colorCheckBox->setChecked(true); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QCOMPARE(entry->historyItems().size(), ++editCount); // Test protected attributes editEntryWidget->setCurrentPage(1); @@ -336,12 +356,68 @@ void TestGui::testEditEntry() // Confirm edit was made QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode); QCOMPARE(entry->title(), QString("Sample Entry_test")); - QCOMPARE(entry->historyItems().size(), 2); + QCOMPARE(entry->foregroundColor(), fgColor); + QCOMPARE(entryItem.data(Qt::ForegroundRole), QVariant(fgColor)); + QCOMPARE(entry->backgroundColor(), bgColor); + QCOMPARE(entryItem.data(Qt::BackgroundRole), QVariant(bgColor)); + QCOMPARE(entry->historyItems().size(), ++editCount); // Confirm modified indicator is showing QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName)); } +void TestGui::testSearchEditEntry() +{ + // Regression test for Issue #1447 -- Uses example from issue description + + // Find buttons for group creation + EditGroupWidget* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QLineEdit* nameEdit = editGroupWidget->findChild("nameEdit"); + QDialogButtonBox* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + + // Add groups "Good" and "Bad" + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "Good"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); // Makes "Good" and "Bad" on the same level + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "Bad"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); + + // Find buttons for entry creation + QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); + EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + + // Create "Doggy" in "Good" + Group* goodGroup = m_dbWidget->currentGroup()->findChildByName(QString("Good")); + m_dbWidget->groupView()->setCurrentGroup(goodGroup); + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "Doggy"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + // Select "Bad" group in groupView + Group* badGroup = m_db->rootGroup()->findChildByName(QString("Bad")); + m_dbWidget->groupView()->setCurrentGroup(badGroup); + + // Search for "Doggy" entry + SearchWidget* searchWidget = toolBar->findChild("SearchWidget"); + QLineEdit* searchTextEdit = searchWidget->findChild("searchEdit"); + QTest::mouseClick(searchTextEdit, Qt::LeftButton); + QTest::keyClicks(searchTextEdit, "Doggy"); + QTRY_VERIFY(m_dbWidget->isInSearchMode()); + + // Goto "Doggy"'s edit view + QTest::keyClick(searchTextEdit, Qt::Key_Return); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode); + + // Check the path in header is "parent-group > entry" + QCOMPARE(m_dbWidget->findChild("editEntryWidget")->findChild("headerLabel")->text(), + QString("Good > Doggy > Edit entry")); +} + void TestGui::testAddEntry() { QToolBar* toolBar = m_mainWindow->findChild("toolBar"); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index b8d3ce1fc..0b403731d 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -46,6 +46,7 @@ private slots: void testAutoreloadDatabase(); void testTabs(); void testEditEntry(); + void testSearchEditEntry(); void testAddEntry(); void testPasswordEntryEntropy(); void testDicewareEntryEntropy();