From 83111bb3f6a8bd41830a78ee7371a39d5ddf8d5b Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Thu, 29 Jun 2023 08:23:45 +0200 Subject: [PATCH] Utils: Make validation async Changes FancyLineEdit to accept two types of validation functions: AsyncValidationFunction, which returns a QFuture and takes the new text, or SynchronousValidationFunction. Especially PathChooser is changed to use async validation function to improve snappyness of settings pages that do heavy validation, for instance the Debugger page. Change-Id: I1677e7d8acc29e36c69a867850304b7913e6ae7e Reviewed-by: Eike Ziller Reviewed-by: hjk --- src/libs/utils/CMakeLists.txt | 2 +- src/libs/utils/aspects.cpp | 12 +- src/libs/utils/fancylineedit.cpp | 149 ++++++++++++++---- src/libs/utils/fancylineedit.h | 13 +- src/libs/utils/layoutbuilder.cpp | 12 ++ src/libs/utils/layoutbuilder.h | 3 + src/libs/utils/pathchooser.cpp | 118 +++++++------- src/libs/utils/pathchooser.h | 3 +- src/libs/utils/projectintropage.cpp | 2 + src/plugins/debugger/debuggeritemmanager.cpp | 128 ++++++++------- .../modeleditor/extpropertiesmview.cpp | 3 - src/plugins/projectexplorer/buildaspects.cpp | 19 +-- .../qmakeprojectmanager/addlibrarywizard.cpp | 30 ++-- src/plugins/qtsupport/qtoptionspage.cpp | 41 ++--- src/plugins/squish/squishsettings.cpp | 28 ++-- 15 files changed, 350 insertions(+), 213 deletions(-) diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 78e530a6d58..883727de72d 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -1,5 +1,5 @@ add_qtc_library(Utils - DEPENDS Tasking Qt::Qml Qt::Xml + DEPENDS Tasking Qt::Qml Qt::Xml Spinner PUBLIC_DEPENDS Qt::Concurrent Qt::Core Qt::Network Qt::Gui Qt::Widgets Qt::Core5Compat diff --git a/src/libs/utils/aspects.cpp b/src/libs/utils/aspects.cpp index 5c1fd5cdb26..a97424a6e91 100644 --- a/src/libs/utils/aspects.cpp +++ b/src/libs/utils/aspects.cpp @@ -662,7 +662,7 @@ public: MacroExpanderProvider m_expanderProvider; FilePath m_baseFileName; StringAspect::ValueAcceptor m_valueAcceptor; - FancyLineEdit::ValidationFunction m_validator; + std::optional m_validator; std::function m_openTerminal; bool m_undoRedoEnabled = true; @@ -1066,9 +1066,9 @@ void StringAspect::setValidationFunction(const FancyLineEdit::ValidationFunction { d->m_validator = validator; if (d->m_lineEditDisplay) - d->m_lineEditDisplay->setValidationFunction(d->m_validator); + d->m_lineEditDisplay->setValidationFunction(*d->m_validator); else if (d->m_pathChooserDisplay) - d->m_pathChooserDisplay->setValidationFunction(d->m_validator); + d->m_pathChooserDisplay->setValidationFunction(*d->m_validator); } void StringAspect::setOpenTerminalHandler(const std::function &openTerminal) @@ -1119,8 +1119,9 @@ void StringAspect::addToLayout(LayoutItem &parent) d->m_pathChooserDisplay->setExpectedKind(d->m_expectedKind); if (!d->m_historyCompleterKey.isEmpty()) d->m_pathChooserDisplay->setHistoryCompleter(d->m_historyCompleterKey); + if (d->m_validator) - d->m_pathChooserDisplay->setValidationFunction(d->m_validator); + d->m_pathChooserDisplay->setValidationFunction(*d->m_validator); d->m_pathChooserDisplay->setEnvironment(d->m_environment); d->m_pathChooserDisplay->setBaseDirectory(d->m_baseFileName); d->m_pathChooserDisplay->setOpenTerminalHandler(d->m_openTerminal); @@ -1163,8 +1164,9 @@ void StringAspect::addToLayout(LayoutItem &parent) d->m_lineEditDisplay->setPlaceholderText(d->m_placeHolderText); if (!d->m_historyCompleterKey.isEmpty()) d->m_lineEditDisplay->setHistoryCompleter(d->m_historyCompleterKey); + if (d->m_validator) - d->m_lineEditDisplay->setValidationFunction(d->m_validator); + d->m_lineEditDisplay->setValidationFunction(*d->m_validator); d->m_lineEditDisplay->setTextKeepingActiveCursor(displayedString); d->m_lineEditDisplay->setReadOnly(isReadOnly()); d->m_lineEditDisplay->setValidatePlaceHolder(d->m_validatePlaceHolder); diff --git a/src/libs/utils/fancylineedit.cpp b/src/libs/utils/fancylineedit.cpp index 2bf50a70fa1..e46ca6b8460 100644 --- a/src/libs/utils/fancylineedit.cpp +++ b/src/libs/utils/fancylineedit.cpp @@ -11,15 +11,19 @@ #include "utilsicons.h" #include "utilstr.h" +#include + #include +#include #include #include #include -#include -#include #include +#include #include #include +#include +#include #include #include @@ -45,8 +49,7 @@ \li A history completer. - \li The ability to validate the contents of the text field by overriding - virtual \c validate() function in derived clases. + \li The ability to validate the contents of the text field by setting the \a validationFunction. \endlist When invalid, the text color will turn red and a tooltip will @@ -120,17 +123,29 @@ public: const QColor m_errorTextColor; const QColor m_placeholderTextColor; QString m_errorMessage; + + SpinnerSolution::Spinner *m_spinner = nullptr; + QTimer m_spinnerDelayTimer; + + std::unique_ptr> m_validatorWatcher; }; -FancyLineEditPrivate::FancyLineEditPrivate(FancyLineEdit *parent) : - QObject(parent), - m_lineEdit(parent), - m_completionShortcut(completionShortcut()->key(), parent), - m_okTextColor(creatorTheme()->color(Theme::TextColorNormal)), - m_errorTextColor(creatorTheme()->color(Theme::TextColorError)), - m_placeholderTextColor(QApplication::palette().color(QPalette::PlaceholderText)) - +FancyLineEditPrivate::FancyLineEditPrivate(FancyLineEdit *parent) + : QObject(parent) + , m_lineEdit(parent) + , m_completionShortcut(completionShortcut()->key(), parent) + , m_okTextColor(creatorTheme()->color(Theme::TextColorNormal)) + , m_errorTextColor(creatorTheme()->color(Theme::TextColorError)) + , m_placeholderTextColor(QApplication::palette().color(QPalette::PlaceholderText)) + , m_spinner(new SpinnerSolution::Spinner(SpinnerSolution::SpinnerSize::Small, m_lineEdit)) { + m_spinner->setVisible(false); + m_spinnerDelayTimer.setInterval(200); + m_spinnerDelayTimer.setSingleShot(true); + connect(&m_spinnerDelayTimer, &QTimer::timeout, m_spinner, [spinner = m_spinner] { + spinner->setVisible(true); + }); + m_completionShortcut.setContext(Qt::WidgetShortcut); connect(completionShortcut(), &CompletionShortcut::keyChanged, &m_completionShortcut, &QShortcut::setKey); @@ -475,21 +490,20 @@ void FancyLineEdit::setValidatePlaceHolder(bool on) d->m_validatePlaceHolder = on; } -void FancyLineEdit::validate() +void FancyLineEdit::handleValidationResult(AsyncValidationResult result, const QString &oldText) { - const QString t = text(); + d->m_spinner->setVisible(false); + d->m_spinnerDelayTimer.stop(); - if (d->m_isFiltering){ - if (t != d->m_lastFilterText) { - d->m_lastFilterText = t; - emit filterChanged(t); - } - } + const QString newText = result ? *result : oldText; + if (!result) + d->m_errorMessage = result.error(); + else + d->m_errorMessage.clear(); - d->m_errorMessage.clear(); // Are we displaying the placeholder text? - const bool isDisplayingPlaceholderText = !placeholderText().isEmpty() && t.isEmpty(); - const bool validates = d->m_validationFunction(this, &d->m_errorMessage); + const bool isDisplayingPlaceholderText = !placeholderText().isEmpty() && newText.isEmpty(); + const bool validates = result.has_value(); const State newState = isDisplayingPlaceholderText ? DisplayingPlaceholderText : (validates ? Valid : Invalid); if (!validates || d->m_toolTipSet) { @@ -504,18 +518,20 @@ void FancyLineEdit::validate() d->m_firstChange = false; QPalette p = palette(); - p.setColor(QPalette::Active, QPalette::Text, - newState == Invalid ? d->m_errorTextColor : d->m_okTextColor); - p.setColor(QPalette::Active, QPalette::PlaceholderText, - validates || !d->m_validatePlaceHolder - ? d->m_placeholderTextColor : d->m_errorTextColor); + p.setColor(QPalette::Active, + QPalette::Text, + newState == Invalid ? d->m_errorTextColor : d->m_okTextColor); + p.setColor(QPalette::Active, + QPalette::PlaceholderText, + validates || !d->m_validatePlaceHolder ? d->m_placeholderTextColor + : d->m_errorTextColor); setPalette(p); if (validHasChanged) emit validChanged(newState == Valid); } - const QString fixedString = fixInputString(t); - if (t != fixedString) { + const QString fixedString = fixInputString(newText); + if (newText != fixedString) { const int cursorPos = cursorPosition(); QSignalBlocker blocker(this); setText(fixedString); @@ -523,15 +539,80 @@ void FancyLineEdit::validate() } // Check buttons. - if (d->m_oldText.isEmpty() || t.isEmpty()) { + if (d->m_oldText.isEmpty() || newText.isEmpty()) { for (auto &button : std::as_const(d->m_iconbutton)) { if (button->hasAutoHide()) - button->animateShow(!t.isEmpty()); + button->animateShow(!newText.isEmpty()); } - d->m_oldText = t; + d->m_oldText = newText; } - handleChanged(t); + handleChanged(newText); +} + +void FancyLineEdit::validate() +{ + if (d->m_validationFunction.index() == 0) { + AsyncValidationFunction &validationFunction = std::get<0>(d->m_validationFunction); + if (!validationFunction) + return; + + if (d->m_validatorWatcher) + d->m_validatorWatcher->cancel(); + + const QString oldText = text(); + + if (d->m_isFiltering) { + if (oldText != d->m_lastFilterText) { + d->m_lastFilterText = oldText; + emit filterChanged(oldText); + } + } + + d->m_validatorWatcher = std::make_unique>(); + connect(d->m_validatorWatcher.get(), + &QFutureWatcher::finished, + this, + [this, oldText]() { + FancyLineEdit::AsyncValidationResult result = d->m_validatorWatcher->result(); + + handleValidationResult(result, oldText); + }); + + d->m_state = Validating; + d->m_spinnerDelayTimer.start(); + + AsyncValidationFuture future = validationFunction(text()); + d->m_validatorWatcher->setFuture(future); + + return; + } + + if (d->m_validationFunction.index() == 1) { + auto &validationFunction = std::get<1>(d->m_validationFunction); + if (!validationFunction) + return; + + const QString t = text(); + + if (d->m_isFiltering) { + if (t != d->m_lastFilterText) { + d->m_lastFilterText = t; + emit filterChanged(t); + } + } + + QString error; + const bool validates = validationFunction(this, &error); + expected_str result; + + if (validates) + result = t; + else + result = make_unexpected(error); + + handleValidationResult(result, t); + } } QString FancyLineEdit::fixInputString(const QString &string) diff --git a/src/libs/utils/fancylineedit.h b/src/libs/utils/fancylineedit.h index eb54bc8d866..27a36974e44 100644 --- a/src/libs/utils/fancylineedit.h +++ b/src/libs/utils/fancylineedit.h @@ -6,8 +6,10 @@ #include "utils_global.h" #include "completinglineedit.h" +#include "expected.h" #include +#include #include @@ -98,8 +100,13 @@ public: // Validation // line edit, (out)errorMessage -> valid? - using ValidationFunction = std::function; - enum State { Invalid, DisplayingPlaceholderText, Valid }; + using AsyncValidationResult = Utils::expected_str; + using AsyncValidationFuture = QFuture; + using AsyncValidationFunction = std::function; + using SynchronousValidationFunction = std::function; + using ValidationFunction = std::variant; + + enum State { Invalid, DisplayingPlaceholderText, Valid, Validating }; State state() const; bool isValid() const; @@ -138,6 +145,8 @@ protected: private: void iconClicked(FancyLineEdit::Side); + void handleValidationResult(AsyncValidationResult result, const QString &oldText); + static bool validateWithValidator(FancyLineEdit *edit, QString *errorMessage); // Unimplemented, to force the user to make a decision on // whether to use setHistoryCompleter() or setSpecialCompleter(). diff --git a/src/libs/utils/layoutbuilder.cpp b/src/libs/utils/layoutbuilder.cpp index 881dc613587..1fceaf11b87 100644 --- a/src/libs/utils/layoutbuilder.cpp +++ b/src/libs/utils/layoutbuilder.cpp @@ -888,6 +888,18 @@ LayoutItem columnStretch(int column, int stretch) }; } +LayoutItem fieldGrowthPolicy(QFormLayout::FieldGrowthPolicy policy) +{ + return [policy](QObject *target) { + if (auto form = qobject_cast(target)) { + form->setFieldGrowthPolicy(policy); + } else { + QTC_CHECK(false); + } + }; +} + + // Id based setters LayoutItem id(ID &out) diff --git a/src/libs/utils/layoutbuilder.h b/src/libs/utils/layoutbuilder.h index 4a756139897..72fefc1b781 100644 --- a/src/libs/utils/layoutbuilder.h +++ b/src/libs/utils/layoutbuilder.h @@ -6,6 +6,7 @@ #include #include #include +#include #include @@ -215,6 +216,8 @@ QTCREATOR_UTILS_EXPORT LayoutItem resize(int, int); QTCREATOR_UTILS_EXPORT LayoutItem columnStretch(int column, int stretch); QTCREATOR_UTILS_EXPORT LayoutItem spacing(int); QTCREATOR_UTILS_EXPORT LayoutItem windowTitle(const QString &windowTitle); +QTCREATOR_UTILS_EXPORT LayoutItem fieldGrowthPolicy(QFormLayout::FieldGrowthPolicy policy); + // "Getters" diff --git a/src/libs/utils/pathchooser.cpp b/src/libs/utils/pathchooser.cpp index 1fe1e962560..a13471ef07f 100644 --- a/src/libs/utils/pathchooser.cpp +++ b/src/libs/utils/pathchooser.cpp @@ -3,6 +3,7 @@ #include "pathchooser.h" +#include "async.h" #include "commandline.h" #include "environment.h" #include "fileutils.h" @@ -15,6 +16,7 @@ #include "utilstr.h" #include +#include #include #include #include @@ -531,100 +533,73 @@ void PathChooser::setToolTip(const QString &toolTip) d->m_lineEdit->setToolTip(toolTip); } -FancyLineEdit::ValidationFunction PathChooser::defaultValidationFunction() const +static FancyLineEdit::AsyncValidationResult validatePath(FilePath filePath, + const QString &defaultValue, + PathChooser::Kind kind) { - return std::bind(&PathChooser::validatePath, this, std::placeholders::_1, std::placeholders::_2); -} - -bool PathChooser::validatePath(FancyLineEdit *edit, QString *errorMessage) const -{ - QString input = edit->text(); - - if (input.isEmpty()) { - if (!d->m_defaultValue.isEmpty()) { - input = d->m_defaultValue; - } else { - if (errorMessage) - *errorMessage = Tr::tr("The path must not be empty."); - return false; - } - } - - const FilePath filePath = d->expandedPath(FilePath::fromUserInput(input)); if (filePath.isEmpty()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" expanded to an empty string.").arg(input); - return false; + if (!defaultValue.isEmpty()) { + filePath = FilePath::fromUserInput(defaultValue); + } else { + return make_unexpected(Tr::tr("The path must not be empty.")); + } } // Check if existing - switch (d->m_acceptingKind) { + switch (kind) { case PathChooser::ExistingDirectory: if (!filePath.exists()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput())); } if (!filePath.isDir()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput())); } break; case PathChooser::File: if (!filePath.exists()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput())); } if (!filePath.isFile()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput())); } break; case PathChooser::SaveFile: if (!filePath.parentDir().exists()) { - if (errorMessage) - *errorMessage = Tr::tr("The directory \"%1\" does not exist.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The directory \"%1\" does not exist.").arg(filePath.toUserOutput())); } if (filePath.exists() && filePath.isDir()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput())); } break; case PathChooser::ExistingCommand: if (!filePath.exists()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput())); } if (!filePath.isExecutableFile()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" is not an executable file.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" is not an executable file.").arg(filePath.toUserOutput())); } break; case PathChooser::Directory: if (filePath.exists() && !filePath.isDir()) { - if (errorMessage) - *errorMessage = Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()); - return false; + return make_unexpected( + Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput())); } if (filePath.osType() == OsTypeWindows && !filePath.startsWithDriveLetter() && !filePath.startsWith("\\\\") && !filePath.startsWith("//")) { - if (errorMessage) - *errorMessage = Tr::tr("Invalid path \"%1\".").arg(filePath.toUserOutput()); - return false; + return make_unexpected(Tr::tr("Invalid path \"%1\".").arg(filePath.toUserOutput())); } break; case PathChooser::Command: if (filePath.exists() && !filePath.isExecutableFile()) { - if (errorMessage) - *errorMessage = Tr::tr("Cannot execute \"%1\".").arg(filePath.toUserOutput()); - return false; + return make_unexpected(Tr::tr("Cannot execute \"%1\".").arg(filePath.toUserOutput())); } break; @@ -632,9 +607,34 @@ bool PathChooser::validatePath(FancyLineEdit *edit, QString *errorMessage) const ; } - if (errorMessage) - *errorMessage = Tr::tr("Full path: \"%1\"").arg(filePath.toUserOutput()); - return true; + return filePath.toUserOutput(); +} + +FancyLineEdit::AsyncValidationFunction PathChooser::defaultValidationFunction() const +{ + return [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture { + if (text.isEmpty()) { + return QtFuture::makeReadyFuture((Utils::expected_str( + make_unexpected(Tr::tr("The path must not be empty."))))); + } + + const FilePath expanded = d->expandedPath(FilePath::fromUserInput(text)); + + if (expanded.isEmpty()) { + return QtFuture::makeReadyFuture((Utils::expected_str( + make_unexpected(Tr::tr("The path \"%1\" expanded to an empty string.") + .arg(expanded.toUserOutput()))))); + } + + return Utils::asyncRun( + [expanded, + defVal = d->m_defaultValue, + kind = d->m_acceptingKind, + env = d->m_environment, + baseDirectory = d->m_baseDirectory]() -> FancyLineEdit::AsyncValidationResult { + return validatePath(expanded, defVal, kind); + }); + }; } void PathChooser::setValidationFunction(const FancyLineEdit::ValidationFunction &fn) diff --git a/src/libs/utils/pathchooser.h b/src/libs/utils/pathchooser.h index 62a370185da..45930a61270 100644 --- a/src/libs/utils/pathchooser.h +++ b/src/libs/utils/pathchooser.h @@ -80,7 +80,7 @@ public: /** Returns the suggested label title when used in a form layout. */ static QString label(); - FancyLineEdit::ValidationFunction defaultValidationFunction() const; + FancyLineEdit::AsyncValidationFunction defaultValidationFunction() const; void setValidationFunction(const FancyLineEdit::ValidationFunction &fn); /** Return the home directory, which needs some fixing under Windows. */ @@ -153,7 +153,6 @@ private: // Use filePath().toString() or better suitable conversions. QString path() const { return filePath().toString(); } - bool validatePath(FancyLineEdit *edit, QString *errorMessage) const; // Returns overridden title or the one from QString makeDialogTitle(const QString &title); void slotBrowse(bool remote); diff --git a/src/libs/utils/projectintropage.cpp b/src/libs/utils/projectintropage.cpp index 0b1a0a63557..8452f48c50b 100644 --- a/src/libs/utils/projectintropage.cpp +++ b/src/libs/utils/projectintropage.cpp @@ -212,6 +212,8 @@ bool ProjectIntroPage::validate() return false; case FancyLineEdit::Valid: break; + case FancyLineEdit::Validating: + break; } // Check existence of the directory diff --git a/src/plugins/debugger/debuggeritemmanager.cpp b/src/plugins/debugger/debuggeritemmanager.cpp index 2c97bdc8456..c672cf4b94d 100644 --- a/src/plugins/debugger/debuggeritemmanager.cpp +++ b/src/plugins/debugger/debuggeritemmanager.cpp @@ -14,10 +14,12 @@ #include <projectexplorer/projectexplorericons.h> #include <utils/algorithm.h> +#include <utils/async.h> #include <utils/detailswidget.h> #include <utils/environment.h> #include <utils/fileutils.h> #include <utils/hostosinfo.h> +#include <utils/layoutbuilder.h> #include <utils/pathchooser.h> #include <utils/persistentsettings.h> #include <utils/process.h> @@ -29,6 +31,7 @@ #include <QDir> #include <QFileInfo> #include <QFormLayout> +#include <QFutureWatcher> #include <QHeaderView> #include <QLabel> #include <QLineEdit> @@ -101,7 +104,6 @@ private: void setAbis(const QStringList &abiNames); QLineEdit *m_displayNameLineEdit; - QLineEdit *m_typeLineEdit; QLabel *m_cdbLabel; PathChooser *m_binaryChooser; bool m_autodetected = false; @@ -109,12 +111,12 @@ private: DebuggerEngineType m_engineType = NoEngineType; QVariant m_id; - QLabel *m_abisLabel; - QLineEdit *m_abis; - QLabel *m_versionLabel; - QLineEdit *m_version; - QLabel *m_workingDirectoryLabel; + QLabel *m_abis; + QLabel *m_version; + QLabel *m_type; + PathChooser *m_workingDirectoryChooser; + QFutureWatcher<DebuggerItem> m_updateWatcher; }; // -------------------------------------------------------------------------- @@ -311,52 +313,47 @@ DebuggerItemConfigWidget::DebuggerItemConfigWidget() { m_displayNameLineEdit = new QLineEdit(this); - m_typeLineEdit = new QLineEdit(this); - m_typeLineEdit->setEnabled(false); - m_binaryChooser = new PathChooser(this); m_binaryChooser->setExpectedKind(PathChooser::ExistingCommand); m_binaryChooser->setMinimumWidth(400); m_binaryChooser->setHistoryCompleter("DebuggerPaths"); - m_binaryChooser->setValidationFunction([this](FancyLineEdit *edit, QString *errorMessage) { - if (!m_binaryChooser->defaultValidationFunction()(edit, errorMessage)) - return false; - DebuggerItem item; - item.setCommand(m_binaryChooser->filePath()); - errorMessage->clear(); - item.reinitializeFromFile(errorMessage); - return errorMessage->isEmpty(); - }); + m_binaryChooser->setValidationFunction( + [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture { + return m_binaryChooser->defaultValidationFunction()(text).then( + [](const FancyLineEdit::AsyncValidationResult &result) + -> FancyLineEdit::AsyncValidationResult { + if (!result) + return result; + + DebuggerItem item; + item.setCommand(FilePath::fromUserInput(result.value())); + QString errorMessage; + item.reinitializeFromFile(&errorMessage); + + if (!errorMessage.isEmpty()) + return make_unexpected(errorMessage); + + return result.value(); + }); + }); m_binaryChooser->setAllowPathFromDevice(true); - m_workingDirectoryLabel = new QLabel(Tr::tr("ABIs:")); m_workingDirectoryChooser = new PathChooser(this); m_workingDirectoryChooser->setExpectedKind(PathChooser::Directory); m_workingDirectoryChooser->setMinimumWidth(400); m_workingDirectoryChooser->setHistoryCompleter("DebuggerPaths"); - m_cdbLabel = new QLabel(this); - m_cdbLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); - m_cdbLabel->setOpenExternalLinks(true); + auto makeInteractiveLabel = []() { + auto label = new QLabel; + label->setTextInteractionFlags(Qt::TextEditorInteraction | Qt::TextBrowserInteraction); + label->setOpenExternalLinks(true); + return label; + }; - m_versionLabel = new QLabel(Tr::tr("Version:")); - m_version = new QLineEdit(this); - m_version->setPlaceholderText(Tr::tr("Unknown")); - m_version->setEnabled(false); - - m_abisLabel = new QLabel(Tr::tr("Working directory:")); - m_abis = new QLineEdit(this); - m_abis->setEnabled(false); - - auto formLayout = new QFormLayout(this); - formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - formLayout->addRow(new QLabel(Tr::tr("Name:")), m_displayNameLineEdit); - formLayout->addRow(m_cdbLabel); - formLayout->addRow(new QLabel(Tr::tr("Path:")), m_binaryChooser); - formLayout->addRow(new QLabel(Tr::tr("Type:")), m_typeLineEdit); - formLayout->addRow(m_abisLabel, m_abis); - formLayout->addRow(m_versionLabel, m_version); - formLayout->addRow(m_workingDirectoryLabel, m_workingDirectoryChooser); + m_cdbLabel = makeInteractiveLabel(); + m_version = makeInteractiveLabel(); + m_abis = makeInteractiveLabel(); + m_type = makeInteractiveLabel(); connect(m_binaryChooser, &PathChooser::textChanged, this, &DebuggerItemConfigWidget::binaryPathHasChanged); @@ -364,6 +361,30 @@ DebuggerItemConfigWidget::DebuggerItemConfigWidget() this, &DebuggerItemConfigWidget::store); connect(m_displayNameLineEdit, &QLineEdit::textChanged, this, &DebuggerItemConfigWidget::store); + + connect(&m_updateWatcher, &QFutureWatcher<DebuggerItem>::finished, this, [this] { + if (m_updateWatcher.future().resultCount() > 0) { + DebuggerItem tmp = m_updateWatcher.result(); + setAbis(tmp.abiNames()); + m_version->setText(tmp.version()); + m_engineType = tmp.engineType(); + m_type->setText(tmp.engineTypeName()); + } + }); + + // clang-format off + using namespace Layouting; + Form { + fieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow), + Tr::tr("Name:"), m_displayNameLineEdit, br, + Tr::tr("Path:"), m_binaryChooser, br, + m_cdbLabel, br, + Tr::tr("Type:"), m_type, br, + Tr::tr("ABIs:"), m_abis, br, + Tr::tr("Version:"), m_version, br, + Tr::tr("Working directory:"), m_workingDirectoryChooser, br, + }.attachTo(this); + // clang-format on } DebuggerItem DebuggerItemConfigWidget::item() const @@ -413,20 +434,12 @@ void DebuggerItemConfigWidget::load(const DebuggerItem *item) m_displayNameLineEdit->setEnabled(!item->isAutoDetected()); m_displayNameLineEdit->setText(item->unexpandedDisplayName()); - m_typeLineEdit->setText(item->engineTypeName()); + m_type->setText(item->engineTypeName()); m_binaryChooser->setReadOnly(item->isAutoDetected()); m_binaryChooser->setFilePath(item->command()); m_binaryChooser->setExpectedKind(m_generic ? PathChooser::Any : PathChooser::ExistingCommand); - m_abisLabel->setVisible(!m_generic); - m_abis->setVisible(!m_generic); - m_versionLabel->setVisible(!m_generic); - m_version->setVisible(!m_generic); - m_workingDirectoryLabel->setVisible(!m_generic); - m_workingDirectoryChooser->setVisible(!m_generic); - - m_workingDirectoryChooser->setReadOnly(item->isAutoDetected()); m_workingDirectoryChooser->setFilePath(item->workingDirectory()); @@ -462,16 +475,21 @@ void DebuggerItemConfigWidget::binaryPathHasChanged() return; if (!m_generic) { + m_updateWatcher.cancel(); + DebuggerItem tmp; if (m_binaryChooser->filePath().isExecutableFile()) { tmp = item(); - tmp.reinitializeFromFile(); + m_updateWatcher.setFuture(Utils::asyncRun([tmp]() mutable { + tmp.reinitializeFromFile(); + return tmp; + })); + } else { + setAbis(tmp.abiNames()); + m_version->setText(tmp.version()); + m_engineType = tmp.engineType(); + m_type->setText(tmp.engineTypeName()); } - - setAbis(tmp.abiNames()); - m_version->setText(tmp.version()); - m_engineType = tmp.engineType(); - m_typeLineEdit->setText(tmp.engineTypeName()); } store(); diff --git a/src/plugins/modeleditor/extpropertiesmview.cpp b/src/plugins/modeleditor/extpropertiesmview.cpp index 4977d816b18..b57d1bc2a96 100644 --- a/src/plugins/modeleditor/extpropertiesmview.cpp +++ b/src/plugins/modeleditor/extpropertiesmview.cpp @@ -41,9 +41,6 @@ void ExtPropertiesMView::visitMPackage(const qmt::MPackage *package) m_configPath = new Utils::PathChooser(m_topWidget); m_configPath->setPromptDialogTitle(Tr::tr("Select Custom Configuration Folder")); m_configPath->setExpectedKind(Utils::PathChooser::ExistingDirectory); - m_configPath->setValidationFunction([=](Utils::FancyLineEdit *edit, QString *errorMessage) { - return edit->text().isEmpty() || m_configPath->defaultValidationFunction()(edit, errorMessage); - }); m_configPath->setInitialBrowsePathBackup( Utils::FilePath::fromString(project->fileName()).absolutePath()); addRow(Tr::tr("Config path:"), m_configPath, "configpath"); diff --git a/src/plugins/projectexplorer/buildaspects.cpp b/src/plugins/projectexplorer/buildaspects.cpp index 606911654fd..071e8b5f586 100644 --- a/src/plugins/projectexplorer/buildaspects.cpp +++ b/src/plugins/projectexplorer/buildaspects.cpp @@ -42,22 +42,23 @@ BuildDirectoryAspect::BuildDirectoryAspect(const BuildConfiguration *bc) setSettingsKey("ProjectExplorer.BuildConfiguration.BuildDirectory"); setLabelText(Tr::tr("Build directory:")); setExpectedKind(Utils::PathChooser::Directory); - setValidationFunction([this](FancyLineEdit *edit, QString *error) { - const FilePath fixedDir = fixupDir(FilePath::fromUserInput(edit->text())); - if (!fixedDir.isEmpty()) - edit->setText(fixedDir.toUserOutput()); - const FilePath newPath = FilePath::fromUserInput(edit->text()); + setValidationFunction([this](QString text) -> FancyLineEdit::AsyncValidationFuture { + const FilePath fixedDir = fixupDir(FilePath::fromUserInput(text)); + if (!fixedDir.isEmpty()) + text = fixedDir.toUserOutput(); + + const FilePath newPath = FilePath::fromUserInput(text); const auto buildDevice = BuildDeviceKitAspect::device(d->target->kit()); if (buildDevice && buildDevice->type() != ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE && !buildDevice->rootPath().ensureReachable(newPath)) { - *error = Tr::tr("The build directory is not reachable from the build device."); - return false; + return QtFuture::makeReadyFuture((Utils::expected_str<QString>(make_unexpected( + Tr::tr("The build directory is not reachable from the build device."))))); } - - return pathChooser() ? pathChooser()->defaultValidationFunction()(edit, error) : true; + return pathChooser()->defaultValidationFunction()(text); }); + setOpenTerminalHandler([this, bc] { Core::FileUtils::openTerminal(FilePath::fromString(value()), bc->environment()); }); diff --git a/src/plugins/qmakeprojectmanager/addlibrarywizard.cpp b/src/plugins/qmakeprojectmanager/addlibrarywizard.cpp index ff74701cf71..856c5fdd1d4 100644 --- a/src/plugins/qmakeprojectmanager/addlibrarywizard.cpp +++ b/src/plugins/qmakeprojectmanager/addlibrarywizard.cpp @@ -40,13 +40,12 @@ static QStringList qt_clean_filter_list(const QString &filter) return f.split(QLatin1Char(' '), Qt::SkipEmptyParts); } -static bool validateLibraryPath(const FilePath &filePath, - const PathChooser *pathChooser, - QString *errorMessage) +static FancyLineEdit::AsyncValidationResult validateLibraryPath(const QString &input, + const QString &promptDialogFilter) { - Q_UNUSED(errorMessage) + const FilePath filePath = FilePath::fromUserInput(input); if (!filePath.exists()) - return false; + return make_unexpected(Tr::tr("File does not exist.")); const QString fileName = filePath.fileName(); @@ -55,14 +54,14 @@ static bool validateLibraryPath(const FilePath &filePath, ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption; - const QStringList filters = qt_clean_filter_list(pathChooser->promptDialogFilter()); + const QStringList filters = qt_clean_filter_list(promptDialogFilter); for (const QString &filter : filters) { QString pattern = QRegularExpression::wildcardToRegularExpression(filter); QRegularExpression regExp(pattern, option); if (regExp.match(fileName).hasMatch()) - return true; - } - return false; + return input; + } + return make_unexpected(Tr::tr("File does not match filter.")); } AddLibraryWizard::AddLibraryWizard(const FilePath &proFile, QWidget *parent) @@ -182,10 +181,15 @@ DetailsPage::DetailsPage(AddLibraryWizard *parent) PathChooser * const libPathChooser = m_libraryDetailsWidget->libraryPathChooser; libPathChooser->setHistoryCompleter("Qmake.LibDir.History"); - const auto pathValidator = [libPathChooser](FancyLineEdit *edit, QString *errorMessage) { - return libPathChooser->defaultValidationFunction()(edit, errorMessage) - && validateLibraryPath(libPathChooser->filePath(), - libPathChooser, errorMessage); + const auto pathValidator = + [libPathChooser](const QString &text) -> FancyLineEdit::AsyncValidationFuture { + return libPathChooser->defaultValidationFunction()(text).then( + [pDialogFilter = libPathChooser->promptDialogFilter()]( + const FancyLineEdit::AsyncValidationResult &result) { + if (!result) + return result; + return validateLibraryPath(result.value(), pDialogFilter); + }); }; libPathChooser->setValidationFunction(pathValidator); setProperty(SHORT_TITLE_PROPERTY, Tr::tr("Details")); diff --git a/src/plugins/qtsupport/qtoptionspage.cpp b/src/plugins/qtsupport/qtoptionspage.cpp index 60e58d78d6b..4b4442ff13a 100644 --- a/src/plugins/qtsupport/qtoptionspage.cpp +++ b/src/plugins/qtsupport/qtoptionspage.cpp @@ -934,21 +934,19 @@ static std::optional<FilePath> settingsDirForQtDir(const FilePath &baseDirectory return {}; } -static bool validateQtInstallDir(PathChooser *input, QString *errorString) +static FancyLineEdit::AsyncValidationResult validateQtInstallDir(const QString &input, + const FilePath &baseDirectory) { - const FilePath qtDir = input->rawFilePath(); - if (!settingsDirForQtDir(input->baseDirectory(), qtDir)) { - if (errorString) { - const QStringList filesToCheck = settingsFilesToCheck() + qtversionFilesToCheck(); - *errorString = "<html><body>" - + Tr::tr("Qt installation information was not found in \"%1\". " - "Choose a directory that contains one of the files %2") - .arg(qtDir.toUserOutput(), - "<pre>" + filesToCheck.join('\n') + "</pre>"); - } - return false; + const FilePath qtDir = FilePath::fromUserInput(input); + if (!settingsDirForQtDir(baseDirectory, qtDir)) { + const QStringList filesToCheck = settingsFilesToCheck() + qtversionFilesToCheck(); + return make_unexpected( + "<html><body>" + + Tr::tr("Qt installation information was not found in \"%1\". " + "Choose a directory that contains one of the files %2") + .arg(qtDir.toUserOutput(), "<pre>" + filesToCheck.join('\n') + "</pre>")); } - return true; + return input; } static FilePath defaultQtInstallationPath() @@ -976,12 +974,17 @@ void QtOptionsPageWidget::linkWithQt() pathInput->setBaseDirectory(FilePath::fromString(QCoreApplication::applicationDirPath())); pathInput->setPromptDialogTitle(title); pathInput->setMacroExpander(nullptr); - pathInput->setValidationFunction([pathInput](FancyLineEdit *input, QString *errorString) { - if (pathInput->defaultValidationFunction() - && !pathInput->defaultValidationFunction()(input, errorString)) - return false; - return validateQtInstallDir(pathInput, errorString); - }); + pathInput->setValidationFunction( + [pathInput](const QString &input) -> FancyLineEdit::AsyncValidationFuture { + return pathInput->defaultValidationFunction()(input).then( + [baseDir = pathInput->baseDirectory()]( + const FancyLineEdit::AsyncValidationResult &result) + -> FancyLineEdit::AsyncValidationResult { + if (!result) + return result; + return validateQtInstallDir(result.value(), baseDir); + }); + }); const std::optional<FilePath> currentLink = currentlyLinkedQtDir(nullptr); pathInput->setFilePath(currentLink ? *currentLink : defaultQtInstallationPath()); pathInput->setAllowPathFromDevice(true); diff --git a/src/plugins/squish/squishsettings.cpp b/src/plugins/squish/squishsettings.cpp index 5b069f35fcb..597b292a901 100644 --- a/src/plugins/squish/squishsettings.cpp +++ b/src/plugins/squish/squishsettings.cpp @@ -46,17 +46,23 @@ SquishSettings::SquishSettings() squishPath.setLabelText(Tr::tr("Squish path:")); squishPath.setExpectedKind(PathChooser::ExistingDirectory); squishPath.setPlaceHolderText(Tr::tr("Path to Squish installation")); - squishPath.setValidationFunction([this](FancyLineEdit *edit, QString *error) { - QTC_ASSERT(edit, return false); - if (!squishPath.pathChooser()->defaultValidationFunction()(edit, error)) - return false; - const FilePath squishServer = FilePath::fromUserInput(edit->text()) - .pathAppended(HostOsInfo::withExecutableSuffix("bin/squishserver")); - const bool valid = squishServer.isExecutableFile(); - if (!valid && error) - *error = Tr::tr("Path does not contain server executable at its default location."); - return valid; - }); + squishPath.setValidationFunction( + [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture { + return squishPath.pathChooser()->defaultValidationFunction()(text).then( + [](const FancyLineEdit::AsyncValidationResult &result) + -> FancyLineEdit::AsyncValidationResult { + if (!result) + return result; + + const FilePath squishServer + = FilePath::fromUserInput(result.value()) + .pathAppended(HostOsInfo::withExecutableSuffix("bin/squishserver")); + if (!squishServer.isExecutableFile()) + return make_unexpected(Tr::tr( + "Path does not contain server executable at its default location.")); + return result.value(); + }); + }); licensePath.setSettingsKey("LicensePath"); licensePath.setLabelText(Tr::tr("License path:"));