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 <eike.ziller@qt.io>
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-06-29 08:23:45 +02:00
parent 569e73c5ed
commit 83111bb3f6
15 changed files with 350 additions and 213 deletions

View File

@@ -1,5 +1,5 @@
add_qtc_library(Utils add_qtc_library(Utils
DEPENDS Tasking Qt::Qml Qt::Xml DEPENDS Tasking Qt::Qml Qt::Xml Spinner
PUBLIC_DEPENDS PUBLIC_DEPENDS
Qt::Concurrent Qt::Core Qt::Network Qt::Gui Qt::Widgets Qt::Concurrent Qt::Core Qt::Network Qt::Gui Qt::Widgets
Qt::Core5Compat Qt::Core5Compat

View File

@@ -662,7 +662,7 @@ public:
MacroExpanderProvider m_expanderProvider; MacroExpanderProvider m_expanderProvider;
FilePath m_baseFileName; FilePath m_baseFileName;
StringAspect::ValueAcceptor m_valueAcceptor; StringAspect::ValueAcceptor m_valueAcceptor;
FancyLineEdit::ValidationFunction m_validator; std::optional<FancyLineEdit::ValidationFunction> m_validator;
std::function<void()> m_openTerminal; std::function<void()> m_openTerminal;
bool m_undoRedoEnabled = true; bool m_undoRedoEnabled = true;
@@ -1066,9 +1066,9 @@ void StringAspect::setValidationFunction(const FancyLineEdit::ValidationFunction
{ {
d->m_validator = validator; d->m_validator = validator;
if (d->m_lineEditDisplay) if (d->m_lineEditDisplay)
d->m_lineEditDisplay->setValidationFunction(d->m_validator); d->m_lineEditDisplay->setValidationFunction(*d->m_validator);
else if (d->m_pathChooserDisplay) 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<void ()> &openTerminal) void StringAspect::setOpenTerminalHandler(const std::function<void ()> &openTerminal)
@@ -1119,8 +1119,9 @@ void StringAspect::addToLayout(LayoutItem &parent)
d->m_pathChooserDisplay->setExpectedKind(d->m_expectedKind); d->m_pathChooserDisplay->setExpectedKind(d->m_expectedKind);
if (!d->m_historyCompleterKey.isEmpty()) if (!d->m_historyCompleterKey.isEmpty())
d->m_pathChooserDisplay->setHistoryCompleter(d->m_historyCompleterKey); d->m_pathChooserDisplay->setHistoryCompleter(d->m_historyCompleterKey);
if (d->m_validator) 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->setEnvironment(d->m_environment);
d->m_pathChooserDisplay->setBaseDirectory(d->m_baseFileName); d->m_pathChooserDisplay->setBaseDirectory(d->m_baseFileName);
d->m_pathChooserDisplay->setOpenTerminalHandler(d->m_openTerminal); d->m_pathChooserDisplay->setOpenTerminalHandler(d->m_openTerminal);
@@ -1163,8 +1164,9 @@ void StringAspect::addToLayout(LayoutItem &parent)
d->m_lineEditDisplay->setPlaceholderText(d->m_placeHolderText); d->m_lineEditDisplay->setPlaceholderText(d->m_placeHolderText);
if (!d->m_historyCompleterKey.isEmpty()) if (!d->m_historyCompleterKey.isEmpty())
d->m_lineEditDisplay->setHistoryCompleter(d->m_historyCompleterKey); d->m_lineEditDisplay->setHistoryCompleter(d->m_historyCompleterKey);
if (d->m_validator) 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->setTextKeepingActiveCursor(displayedString);
d->m_lineEditDisplay->setReadOnly(isReadOnly()); d->m_lineEditDisplay->setReadOnly(isReadOnly());
d->m_lineEditDisplay->setValidatePlaceHolder(d->m_validatePlaceHolder); d->m_lineEditDisplay->setValidatePlaceHolder(d->m_validatePlaceHolder);

View File

@@ -11,15 +11,19 @@
#include "utilsicons.h" #include "utilsicons.h"
#include "utilstr.h" #include "utilstr.h"
#include <solutions/spinner/spinner.h>
#include <QApplication> #include <QApplication>
#include <QFutureWatcher>
#include <QKeyEvent> #include <QKeyEvent>
#include <QKeySequence> #include <QKeySequence>
#include <QMenu> #include <QMenu>
#include <QShortcut>
#include <QStylePainter>
#include <QPropertyAnimation> #include <QPropertyAnimation>
#include <QShortcut>
#include <QStyle> #include <QStyle>
#include <QStyleOptionFocusRect> #include <QStyleOptionFocusRect>
#include <QStylePainter>
#include <QTimer>
#include <QValidator> #include <QValidator>
#include <QWindow> #include <QWindow>
@@ -45,8 +49,7 @@
\li A history completer. \li A history completer.
\li The ability to validate the contents of the text field by overriding \li The ability to validate the contents of the text field by setting the \a validationFunction.
virtual \c validate() function in derived clases.
\endlist \endlist
When invalid, the text color will turn red and a tooltip will When invalid, the text color will turn red and a tooltip will
@@ -120,17 +123,29 @@ public:
const QColor m_errorTextColor; const QColor m_errorTextColor;
const QColor m_placeholderTextColor; const QColor m_placeholderTextColor;
QString m_errorMessage; QString m_errorMessage;
SpinnerSolution::Spinner *m_spinner = nullptr;
QTimer m_spinnerDelayTimer;
std::unique_ptr<QFutureWatcher<FancyLineEdit::AsyncValidationResult>> m_validatorWatcher;
}; };
FancyLineEditPrivate::FancyLineEditPrivate(FancyLineEdit *parent) : FancyLineEditPrivate::FancyLineEditPrivate(FancyLineEdit *parent)
QObject(parent), : QObject(parent)
m_lineEdit(parent), , m_lineEdit(parent)
m_completionShortcut(completionShortcut()->key(), parent), , m_completionShortcut(completionShortcut()->key(), parent)
m_okTextColor(creatorTheme()->color(Theme::TextColorNormal)), , m_okTextColor(creatorTheme()->color(Theme::TextColorNormal))
m_errorTextColor(creatorTheme()->color(Theme::TextColorError)), , m_errorTextColor(creatorTheme()->color(Theme::TextColorError))
m_placeholderTextColor(QApplication::palette().color(QPalette::PlaceholderText)) , 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); m_completionShortcut.setContext(Qt::WidgetShortcut);
connect(completionShortcut(), &CompletionShortcut::keyChanged, connect(completionShortcut(), &CompletionShortcut::keyChanged,
&m_completionShortcut, &QShortcut::setKey); &m_completionShortcut, &QShortcut::setKey);
@@ -475,21 +490,20 @@ void FancyLineEdit::setValidatePlaceHolder(bool on)
d->m_validatePlaceHolder = 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){ const QString newText = result ? *result : oldText;
if (t != d->m_lastFilterText) { if (!result)
d->m_lastFilterText = t; d->m_errorMessage = result.error();
emit filterChanged(t); else
} d->m_errorMessage.clear();
}
d->m_errorMessage.clear();
// Are we displaying the placeholder text? // Are we displaying the placeholder text?
const bool isDisplayingPlaceholderText = !placeholderText().isEmpty() && t.isEmpty(); const bool isDisplayingPlaceholderText = !placeholderText().isEmpty() && newText.isEmpty();
const bool validates = d->m_validationFunction(this, &d->m_errorMessage); const bool validates = result.has_value();
const State newState = isDisplayingPlaceholderText ? DisplayingPlaceholderText const State newState = isDisplayingPlaceholderText ? DisplayingPlaceholderText
: (validates ? Valid : Invalid); : (validates ? Valid : Invalid);
if (!validates || d->m_toolTipSet) { if (!validates || d->m_toolTipSet) {
@@ -504,18 +518,20 @@ void FancyLineEdit::validate()
d->m_firstChange = false; d->m_firstChange = false;
QPalette p = palette(); QPalette p = palette();
p.setColor(QPalette::Active, QPalette::Text, p.setColor(QPalette::Active,
newState == Invalid ? d->m_errorTextColor : d->m_okTextColor); QPalette::Text,
p.setColor(QPalette::Active, QPalette::PlaceholderText, newState == Invalid ? d->m_errorTextColor : d->m_okTextColor);
validates || !d->m_validatePlaceHolder p.setColor(QPalette::Active,
? d->m_placeholderTextColor : d->m_errorTextColor); QPalette::PlaceholderText,
validates || !d->m_validatePlaceHolder ? d->m_placeholderTextColor
: d->m_errorTextColor);
setPalette(p); setPalette(p);
if (validHasChanged) if (validHasChanged)
emit validChanged(newState == Valid); emit validChanged(newState == Valid);
} }
const QString fixedString = fixInputString(t); const QString fixedString = fixInputString(newText);
if (t != fixedString) { if (newText != fixedString) {
const int cursorPos = cursorPosition(); const int cursorPos = cursorPosition();
QSignalBlocker blocker(this); QSignalBlocker blocker(this);
setText(fixedString); setText(fixedString);
@@ -523,15 +539,80 @@ void FancyLineEdit::validate()
} }
// Check buttons. // 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)) { for (auto &button : std::as_const(d->m_iconbutton)) {
if (button->hasAutoHide()) 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<QFutureWatcher<AsyncValidationResult>>();
connect(d->m_validatorWatcher.get(),
&QFutureWatcher<AsyncValidationResult>::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<QString> result;
if (validates)
result = t;
else
result = make_unexpected(error);
handleValidationResult(result, t);
}
} }
QString FancyLineEdit::fixInputString(const QString &string) QString FancyLineEdit::fixInputString(const QString &string)

View File

@@ -6,8 +6,10 @@
#include "utils_global.h" #include "utils_global.h"
#include "completinglineedit.h" #include "completinglineedit.h"
#include "expected.h"
#include <QAbstractButton> #include <QAbstractButton>
#include <QFuture>
#include <functional> #include <functional>
@@ -98,8 +100,13 @@ public:
// Validation // Validation
// line edit, (out)errorMessage -> valid? // line edit, (out)errorMessage -> valid?
using ValidationFunction = std::function<bool(FancyLineEdit *, QString *)>; using AsyncValidationResult = Utils::expected_str<QString>;
enum State { Invalid, DisplayingPlaceholderText, Valid }; using AsyncValidationFuture = QFuture<AsyncValidationResult>;
using AsyncValidationFunction = std::function<AsyncValidationFuture(QString)>;
using SynchronousValidationFunction = std::function<bool(FancyLineEdit *, QString *)>;
using ValidationFunction = std::variant<AsyncValidationFunction, SynchronousValidationFunction>;
enum State { Invalid, DisplayingPlaceholderText, Valid, Validating };
State state() const; State state() const;
bool isValid() const; bool isValid() const;
@@ -138,6 +145,8 @@ protected:
private: private:
void iconClicked(FancyLineEdit::Side); void iconClicked(FancyLineEdit::Side);
void handleValidationResult(AsyncValidationResult result, const QString &oldText);
static bool validateWithValidator(FancyLineEdit *edit, QString *errorMessage); static bool validateWithValidator(FancyLineEdit *edit, QString *errorMessage);
// Unimplemented, to force the user to make a decision on // Unimplemented, to force the user to make a decision on
// whether to use setHistoryCompleter() or setSpecialCompleter(). // whether to use setHistoryCompleter() or setSpecialCompleter().

View File

@@ -888,6 +888,18 @@ LayoutItem columnStretch(int column, int stretch)
}; };
} }
LayoutItem fieldGrowthPolicy(QFormLayout::FieldGrowthPolicy policy)
{
return [policy](QObject *target) {
if (auto form = qobject_cast<QFormLayout *>(target)) {
form->setFieldGrowthPolicy(policy);
} else {
QTC_CHECK(false);
}
};
}
// Id based setters // Id based setters
LayoutItem id(ID &out) LayoutItem id(ID &out)

View File

@@ -6,6 +6,7 @@
#include <QList> #include <QList>
#include <QString> #include <QString>
#include <QtGlobal> #include <QtGlobal>
#include <QFormLayout>
#include <optional> #include <optional>
@@ -215,6 +216,8 @@ QTCREATOR_UTILS_EXPORT LayoutItem resize(int, int);
QTCREATOR_UTILS_EXPORT LayoutItem columnStretch(int column, int stretch); QTCREATOR_UTILS_EXPORT LayoutItem columnStretch(int column, int stretch);
QTCREATOR_UTILS_EXPORT LayoutItem spacing(int); QTCREATOR_UTILS_EXPORT LayoutItem spacing(int);
QTCREATOR_UTILS_EXPORT LayoutItem windowTitle(const QString &windowTitle); QTCREATOR_UTILS_EXPORT LayoutItem windowTitle(const QString &windowTitle);
QTCREATOR_UTILS_EXPORT LayoutItem fieldGrowthPolicy(QFormLayout::FieldGrowthPolicy policy);
// "Getters" // "Getters"

View File

@@ -3,6 +3,7 @@
#include "pathchooser.h" #include "pathchooser.h"
#include "async.h"
#include "commandline.h" #include "commandline.h"
#include "environment.h" #include "environment.h"
#include "fileutils.h" #include "fileutils.h"
@@ -15,6 +16,7 @@
#include "utilstr.h" #include "utilstr.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFuture>
#include <QGuiApplication> #include <QGuiApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMenu> #include <QMenu>
@@ -531,100 +533,73 @@ void PathChooser::setToolTip(const QString &toolTip)
d->m_lineEdit->setToolTip(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 (filePath.isEmpty()) {
if (errorMessage) if (!defaultValue.isEmpty()) {
*errorMessage = Tr::tr("The path \"%1\" expanded to an empty string.").arg(input); filePath = FilePath::fromUserInput(defaultValue);
return false; } else {
return make_unexpected(Tr::tr("The path must not be empty."));
}
} }
// Check if existing // Check if existing
switch (d->m_acceptingKind) { switch (kind) {
case PathChooser::ExistingDirectory: case PathChooser::ExistingDirectory:
if (!filePath.exists()) { if (!filePath.exists()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()));
return false;
} }
if (!filePath.isDir()) { if (!filePath.isDir()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()));
return false;
} }
break; break;
case PathChooser::File: case PathChooser::File:
if (!filePath.exists()) { if (!filePath.exists()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()));
return false;
} }
if (!filePath.isFile()) { if (!filePath.isFile()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()));
return false;
} }
break; break;
case PathChooser::SaveFile: case PathChooser::SaveFile:
if (!filePath.parentDir().exists()) { if (!filePath.parentDir().exists()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The directory \"%1\" does not exist.").arg(filePath.toUserOutput()); Tr::tr("The directory \"%1\" does not exist.").arg(filePath.toUserOutput()));
return false;
} }
if (filePath.exists() && filePath.isDir()) { if (filePath.exists() && filePath.isDir()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" is not a file.").arg(filePath.toUserOutput()));
return false;
} }
break; break;
case PathChooser::ExistingCommand: case PathChooser::ExistingCommand:
if (!filePath.exists()) { if (!filePath.exists()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" does not exist.").arg(filePath.toUserOutput()));
return false;
} }
if (!filePath.isExecutableFile()) { if (!filePath.isExecutableFile()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" is not an executable file.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" is not an executable file.").arg(filePath.toUserOutput()));
return false;
} }
break; break;
case PathChooser::Directory: case PathChooser::Directory:
if (filePath.exists() && !filePath.isDir()) { if (filePath.exists() && !filePath.isDir()) {
if (errorMessage) return make_unexpected(
*errorMessage = Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()); Tr::tr("The path \"%1\" is not a directory.").arg(filePath.toUserOutput()));
return false;
} }
if (filePath.osType() == OsTypeWindows && !filePath.startsWithDriveLetter() if (filePath.osType() == OsTypeWindows && !filePath.startsWithDriveLetter()
&& !filePath.startsWith("\\\\") && !filePath.startsWith("//")) { && !filePath.startsWith("\\\\") && !filePath.startsWith("//")) {
if (errorMessage) return make_unexpected(Tr::tr("Invalid path \"%1\".").arg(filePath.toUserOutput()));
*errorMessage = Tr::tr("Invalid path \"%1\".").arg(filePath.toUserOutput());
return false;
} }
break; break;
case PathChooser::Command: case PathChooser::Command:
if (filePath.exists() && !filePath.isExecutableFile()) { if (filePath.exists() && !filePath.isExecutableFile()) {
if (errorMessage) return make_unexpected(Tr::tr("Cannot execute \"%1\".").arg(filePath.toUserOutput()));
*errorMessage = Tr::tr("Cannot execute \"%1\".").arg(filePath.toUserOutput());
return false;
} }
break; break;
@@ -632,9 +607,34 @@ bool PathChooser::validatePath(FancyLineEdit *edit, QString *errorMessage) const
; ;
} }
if (errorMessage) return filePath.toUserOutput();
*errorMessage = Tr::tr("Full path: \"%1\"").arg(filePath.toUserOutput()); }
return true;
FancyLineEdit::AsyncValidationFunction PathChooser::defaultValidationFunction() const
{
return [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture {
if (text.isEmpty()) {
return QtFuture::makeReadyFuture((Utils::expected_str<QString>(
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<QString>(
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) void PathChooser::setValidationFunction(const FancyLineEdit::ValidationFunction &fn)

View File

@@ -80,7 +80,7 @@ public:
/** Returns the suggested label title when used in a form layout. */ /** Returns the suggested label title when used in a form layout. */
static QString label(); static QString label();
FancyLineEdit::ValidationFunction defaultValidationFunction() const; FancyLineEdit::AsyncValidationFunction defaultValidationFunction() const;
void setValidationFunction(const FancyLineEdit::ValidationFunction &fn); void setValidationFunction(const FancyLineEdit::ValidationFunction &fn);
/** Return the home directory, which needs some fixing under Windows. */ /** Return the home directory, which needs some fixing under Windows. */
@@ -153,7 +153,6 @@ private:
// Use filePath().toString() or better suitable conversions. // Use filePath().toString() or better suitable conversions.
QString path() const { return filePath().toString(); } QString path() const { return filePath().toString(); }
bool validatePath(FancyLineEdit *edit, QString *errorMessage) const;
// Returns overridden title or the one from <title> // Returns overridden title or the one from <title>
QString makeDialogTitle(const QString &title); QString makeDialogTitle(const QString &title);
void slotBrowse(bool remote); void slotBrowse(bool remote);

View File

@@ -212,6 +212,8 @@ bool ProjectIntroPage::validate()
return false; return false;
case FancyLineEdit::Valid: case FancyLineEdit::Valid:
break; break;
case FancyLineEdit::Validating:
break;
} }
// Check existence of the directory // Check existence of the directory

View File

@@ -14,10 +14,12 @@
#include <projectexplorer/projectexplorericons.h> #include <projectexplorer/projectexplorericons.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/async.h>
#include <utils/detailswidget.h> #include <utils/detailswidget.h>
#include <utils/environment.h> #include <utils/environment.h>
#include <utils/fileutils.h> #include <utils/fileutils.h>
#include <utils/hostosinfo.h> #include <utils/hostosinfo.h>
#include <utils/layoutbuilder.h>
#include <utils/pathchooser.h> #include <utils/pathchooser.h>
#include <utils/persistentsettings.h> #include <utils/persistentsettings.h>
#include <utils/process.h> #include <utils/process.h>
@@ -29,6 +31,7 @@
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QFormLayout> #include <QFormLayout>
#include <QFutureWatcher>
#include <QHeaderView> #include <QHeaderView>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
@@ -101,7 +104,6 @@ private:
void setAbis(const QStringList &abiNames); void setAbis(const QStringList &abiNames);
QLineEdit *m_displayNameLineEdit; QLineEdit *m_displayNameLineEdit;
QLineEdit *m_typeLineEdit;
QLabel *m_cdbLabel; QLabel *m_cdbLabel;
PathChooser *m_binaryChooser; PathChooser *m_binaryChooser;
bool m_autodetected = false; bool m_autodetected = false;
@@ -109,12 +111,12 @@ private:
DebuggerEngineType m_engineType = NoEngineType; DebuggerEngineType m_engineType = NoEngineType;
QVariant m_id; QVariant m_id;
QLabel *m_abisLabel; QLabel *m_abis;
QLineEdit *m_abis; QLabel *m_version;
QLabel *m_versionLabel; QLabel *m_type;
QLineEdit *m_version;
QLabel *m_workingDirectoryLabel;
PathChooser *m_workingDirectoryChooser; PathChooser *m_workingDirectoryChooser;
QFutureWatcher<DebuggerItem> m_updateWatcher;
}; };
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@@ -311,52 +313,47 @@ DebuggerItemConfigWidget::DebuggerItemConfigWidget()
{ {
m_displayNameLineEdit = new QLineEdit(this); m_displayNameLineEdit = new QLineEdit(this);
m_typeLineEdit = new QLineEdit(this);
m_typeLineEdit->setEnabled(false);
m_binaryChooser = new PathChooser(this); m_binaryChooser = new PathChooser(this);
m_binaryChooser->setExpectedKind(PathChooser::ExistingCommand); m_binaryChooser->setExpectedKind(PathChooser::ExistingCommand);
m_binaryChooser->setMinimumWidth(400); m_binaryChooser->setMinimumWidth(400);
m_binaryChooser->setHistoryCompleter("DebuggerPaths"); m_binaryChooser->setHistoryCompleter("DebuggerPaths");
m_binaryChooser->setValidationFunction([this](FancyLineEdit *edit, QString *errorMessage) { m_binaryChooser->setValidationFunction(
if (!m_binaryChooser->defaultValidationFunction()(edit, errorMessage)) [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture {
return false; return m_binaryChooser->defaultValidationFunction()(text).then(
DebuggerItem item; [](const FancyLineEdit::AsyncValidationResult &result)
item.setCommand(m_binaryChooser->filePath()); -> FancyLineEdit::AsyncValidationResult {
errorMessage->clear(); if (!result)
item.reinitializeFromFile(errorMessage); return result;
return errorMessage->isEmpty();
}); 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_binaryChooser->setAllowPathFromDevice(true);
m_workingDirectoryLabel = new QLabel(Tr::tr("ABIs:"));
m_workingDirectoryChooser = new PathChooser(this); m_workingDirectoryChooser = new PathChooser(this);
m_workingDirectoryChooser->setExpectedKind(PathChooser::Directory); m_workingDirectoryChooser->setExpectedKind(PathChooser::Directory);
m_workingDirectoryChooser->setMinimumWidth(400); m_workingDirectoryChooser->setMinimumWidth(400);
m_workingDirectoryChooser->setHistoryCompleter("DebuggerPaths"); m_workingDirectoryChooser->setHistoryCompleter("DebuggerPaths");
m_cdbLabel = new QLabel(this); auto makeInteractiveLabel = []() {
m_cdbLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); auto label = new QLabel;
m_cdbLabel->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextEditorInteraction | Qt::TextBrowserInteraction);
label->setOpenExternalLinks(true);
return label;
};
m_versionLabel = new QLabel(Tr::tr("Version:")); m_cdbLabel = makeInteractiveLabel();
m_version = new QLineEdit(this); m_version = makeInteractiveLabel();
m_version->setPlaceholderText(Tr::tr("Unknown")); m_abis = makeInteractiveLabel();
m_version->setEnabled(false); m_type = makeInteractiveLabel();
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);
connect(m_binaryChooser, &PathChooser::textChanged, connect(m_binaryChooser, &PathChooser::textChanged,
this, &DebuggerItemConfigWidget::binaryPathHasChanged); this, &DebuggerItemConfigWidget::binaryPathHasChanged);
@@ -364,6 +361,30 @@ DebuggerItemConfigWidget::DebuggerItemConfigWidget()
this, &DebuggerItemConfigWidget::store); this, &DebuggerItemConfigWidget::store);
connect(m_displayNameLineEdit, &QLineEdit::textChanged, connect(m_displayNameLineEdit, &QLineEdit::textChanged,
this, &DebuggerItemConfigWidget::store); 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 DebuggerItem DebuggerItemConfigWidget::item() const
@@ -413,20 +434,12 @@ void DebuggerItemConfigWidget::load(const DebuggerItem *item)
m_displayNameLineEdit->setEnabled(!item->isAutoDetected()); m_displayNameLineEdit->setEnabled(!item->isAutoDetected());
m_displayNameLineEdit->setText(item->unexpandedDisplayName()); m_displayNameLineEdit->setText(item->unexpandedDisplayName());
m_typeLineEdit->setText(item->engineTypeName()); m_type->setText(item->engineTypeName());
m_binaryChooser->setReadOnly(item->isAutoDetected()); m_binaryChooser->setReadOnly(item->isAutoDetected());
m_binaryChooser->setFilePath(item->command()); m_binaryChooser->setFilePath(item->command());
m_binaryChooser->setExpectedKind(m_generic ? PathChooser::Any : PathChooser::ExistingCommand); 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->setReadOnly(item->isAutoDetected());
m_workingDirectoryChooser->setFilePath(item->workingDirectory()); m_workingDirectoryChooser->setFilePath(item->workingDirectory());
@@ -462,16 +475,21 @@ void DebuggerItemConfigWidget::binaryPathHasChanged()
return; return;
if (!m_generic) { if (!m_generic) {
m_updateWatcher.cancel();
DebuggerItem tmp; DebuggerItem tmp;
if (m_binaryChooser->filePath().isExecutableFile()) { if (m_binaryChooser->filePath().isExecutableFile()) {
tmp = item(); 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(); store();

View File

@@ -41,9 +41,6 @@ void ExtPropertiesMView::visitMPackage(const qmt::MPackage *package)
m_configPath = new Utils::PathChooser(m_topWidget); m_configPath = new Utils::PathChooser(m_topWidget);
m_configPath->setPromptDialogTitle(Tr::tr("Select Custom Configuration Folder")); m_configPath->setPromptDialogTitle(Tr::tr("Select Custom Configuration Folder"));
m_configPath->setExpectedKind(Utils::PathChooser::ExistingDirectory); 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( m_configPath->setInitialBrowsePathBackup(
Utils::FilePath::fromString(project->fileName()).absolutePath()); Utils::FilePath::fromString(project->fileName()).absolutePath());
addRow(Tr::tr("Config path:"), m_configPath, "configpath"); addRow(Tr::tr("Config path:"), m_configPath, "configpath");

View File

@@ -42,22 +42,23 @@ BuildDirectoryAspect::BuildDirectoryAspect(const BuildConfiguration *bc)
setSettingsKey("ProjectExplorer.BuildConfiguration.BuildDirectory"); setSettingsKey("ProjectExplorer.BuildConfiguration.BuildDirectory");
setLabelText(Tr::tr("Build directory:")); setLabelText(Tr::tr("Build directory:"));
setExpectedKind(Utils::PathChooser::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()); const auto buildDevice = BuildDeviceKitAspect::device(d->target->kit());
if (buildDevice && buildDevice->type() != ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE if (buildDevice && buildDevice->type() != ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE
&& !buildDevice->rootPath().ensureReachable(newPath)) { && !buildDevice->rootPath().ensureReachable(newPath)) {
*error = Tr::tr("The build directory is not reachable from the build device."); return QtFuture::makeReadyFuture((Utils::expected_str<QString>(make_unexpected(
return false; Tr::tr("The build directory is not reachable from the build device.")))));
} }
return pathChooser()->defaultValidationFunction()(text);
return pathChooser() ? pathChooser()->defaultValidationFunction()(edit, error) : true;
}); });
setOpenTerminalHandler([this, bc] { setOpenTerminalHandler([this, bc] {
Core::FileUtils::openTerminal(FilePath::fromString(value()), bc->environment()); Core::FileUtils::openTerminal(FilePath::fromString(value()), bc->environment());
}); });

View File

@@ -40,13 +40,12 @@ static QStringList qt_clean_filter_list(const QString &filter)
return f.split(QLatin1Char(' '), Qt::SkipEmptyParts); return f.split(QLatin1Char(' '), Qt::SkipEmptyParts);
} }
static bool validateLibraryPath(const FilePath &filePath, static FancyLineEdit::AsyncValidationResult validateLibraryPath(const QString &input,
const PathChooser *pathChooser, const QString &promptDialogFilter)
QString *errorMessage)
{ {
Q_UNUSED(errorMessage) const FilePath filePath = FilePath::fromUserInput(input);
if (!filePath.exists()) if (!filePath.exists())
return false; return make_unexpected(Tr::tr("File does not exist."));
const QString fileName = filePath.fileName(); const QString fileName = filePath.fileName();
@@ -55,14 +54,14 @@ static bool validateLibraryPath(const FilePath &filePath,
? QRegularExpression::CaseInsensitiveOption ? QRegularExpression::CaseInsensitiveOption
: QRegularExpression::NoPatternOption; : QRegularExpression::NoPatternOption;
const QStringList filters = qt_clean_filter_list(pathChooser->promptDialogFilter()); const QStringList filters = qt_clean_filter_list(promptDialogFilter);
for (const QString &filter : filters) { for (const QString &filter : filters) {
QString pattern = QRegularExpression::wildcardToRegularExpression(filter); QString pattern = QRegularExpression::wildcardToRegularExpression(filter);
QRegularExpression regExp(pattern, option); QRegularExpression regExp(pattern, option);
if (regExp.match(fileName).hasMatch()) if (regExp.match(fileName).hasMatch())
return true; return input;
} }
return false; return make_unexpected(Tr::tr("File does not match filter."));
} }
AddLibraryWizard::AddLibraryWizard(const FilePath &proFile, QWidget *parent) AddLibraryWizard::AddLibraryWizard(const FilePath &proFile, QWidget *parent)
@@ -182,10 +181,15 @@ DetailsPage::DetailsPage(AddLibraryWizard *parent)
PathChooser * const libPathChooser = m_libraryDetailsWidget->libraryPathChooser; PathChooser * const libPathChooser = m_libraryDetailsWidget->libraryPathChooser;
libPathChooser->setHistoryCompleter("Qmake.LibDir.History"); libPathChooser->setHistoryCompleter("Qmake.LibDir.History");
const auto pathValidator = [libPathChooser](FancyLineEdit *edit, QString *errorMessage) { const auto pathValidator =
return libPathChooser->defaultValidationFunction()(edit, errorMessage) [libPathChooser](const QString &text) -> FancyLineEdit::AsyncValidationFuture {
&& validateLibraryPath(libPathChooser->filePath(), return libPathChooser->defaultValidationFunction()(text).then(
libPathChooser, errorMessage); [pDialogFilter = libPathChooser->promptDialogFilter()](
const FancyLineEdit::AsyncValidationResult &result) {
if (!result)
return result;
return validateLibraryPath(result.value(), pDialogFilter);
});
}; };
libPathChooser->setValidationFunction(pathValidator); libPathChooser->setValidationFunction(pathValidator);
setProperty(SHORT_TITLE_PROPERTY, Tr::tr("Details")); setProperty(SHORT_TITLE_PROPERTY, Tr::tr("Details"));

View File

@@ -934,21 +934,19 @@ static std::optional<FilePath> settingsDirForQtDir(const FilePath &baseDirectory
return {}; return {};
} }
static bool validateQtInstallDir(PathChooser *input, QString *errorString) static FancyLineEdit::AsyncValidationResult validateQtInstallDir(const QString &input,
const FilePath &baseDirectory)
{ {
const FilePath qtDir = input->rawFilePath(); const FilePath qtDir = FilePath::fromUserInput(input);
if (!settingsDirForQtDir(input->baseDirectory(), qtDir)) { if (!settingsDirForQtDir(baseDirectory, qtDir)) {
if (errorString) { const QStringList filesToCheck = settingsFilesToCheck() + qtversionFilesToCheck();
const QStringList filesToCheck = settingsFilesToCheck() + qtversionFilesToCheck(); return make_unexpected(
*errorString = "<html><body>" "<html><body>"
+ Tr::tr("Qt installation information was not found in \"%1\". " + Tr::tr("Qt installation information was not found in \"%1\". "
"Choose a directory that contains one of the files %2") "Choose a directory that contains one of the files %2")
.arg(qtDir.toUserOutput(), .arg(qtDir.toUserOutput(), "<pre>" + filesToCheck.join('\n') + "</pre>"));
"<pre>" + filesToCheck.join('\n') + "</pre>");
}
return false;
} }
return true; return input;
} }
static FilePath defaultQtInstallationPath() static FilePath defaultQtInstallationPath()
@@ -976,12 +974,17 @@ void QtOptionsPageWidget::linkWithQt()
pathInput->setBaseDirectory(FilePath::fromString(QCoreApplication::applicationDirPath())); pathInput->setBaseDirectory(FilePath::fromString(QCoreApplication::applicationDirPath()));
pathInput->setPromptDialogTitle(title); pathInput->setPromptDialogTitle(title);
pathInput->setMacroExpander(nullptr); pathInput->setMacroExpander(nullptr);
pathInput->setValidationFunction([pathInput](FancyLineEdit *input, QString *errorString) { pathInput->setValidationFunction(
if (pathInput->defaultValidationFunction() [pathInput](const QString &input) -> FancyLineEdit::AsyncValidationFuture {
&& !pathInput->defaultValidationFunction()(input, errorString)) return pathInput->defaultValidationFunction()(input).then(
return false; [baseDir = pathInput->baseDirectory()](
return validateQtInstallDir(pathInput, errorString); const FancyLineEdit::AsyncValidationResult &result)
}); -> FancyLineEdit::AsyncValidationResult {
if (!result)
return result;
return validateQtInstallDir(result.value(), baseDir);
});
});
const std::optional<FilePath> currentLink = currentlyLinkedQtDir(nullptr); const std::optional<FilePath> currentLink = currentlyLinkedQtDir(nullptr);
pathInput->setFilePath(currentLink ? *currentLink : defaultQtInstallationPath()); pathInput->setFilePath(currentLink ? *currentLink : defaultQtInstallationPath());
pathInput->setAllowPathFromDevice(true); pathInput->setAllowPathFromDevice(true);

View File

@@ -46,17 +46,23 @@ SquishSettings::SquishSettings()
squishPath.setLabelText(Tr::tr("Squish path:")); squishPath.setLabelText(Tr::tr("Squish path:"));
squishPath.setExpectedKind(PathChooser::ExistingDirectory); squishPath.setExpectedKind(PathChooser::ExistingDirectory);
squishPath.setPlaceHolderText(Tr::tr("Path to Squish installation")); squishPath.setPlaceHolderText(Tr::tr("Path to Squish installation"));
squishPath.setValidationFunction([this](FancyLineEdit *edit, QString *error) { squishPath.setValidationFunction(
QTC_ASSERT(edit, return false); [this](const QString &text) -> FancyLineEdit::AsyncValidationFuture {
if (!squishPath.pathChooser()->defaultValidationFunction()(edit, error)) return squishPath.pathChooser()->defaultValidationFunction()(text).then(
return false; [](const FancyLineEdit::AsyncValidationResult &result)
const FilePath squishServer = FilePath::fromUserInput(edit->text()) -> FancyLineEdit::AsyncValidationResult {
.pathAppended(HostOsInfo::withExecutableSuffix("bin/squishserver")); if (!result)
const bool valid = squishServer.isExecutableFile(); return result;
if (!valid && error)
*error = Tr::tr("Path does not contain server executable at its default location."); const FilePath squishServer
return valid; = 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.setSettingsKey("LicensePath");
licensePath.setLabelText(Tr::tr("License path:")); licensePath.setLabelText(Tr::tr("License path:"));