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:"));