From 574fed04fcbadf49c7f636e32934a34e30ecc838 Mon Sep 17 00:00:00 2001 From: Eugene Shalygin Date: Thu, 18 May 2017 15:13:08 +0200 Subject: [PATCH] Add visual feedback for wrong paths in FileSystemPathEdit --- src/gui/fspathedit.cpp | 9 ++ src/gui/fspathedit_p.cpp | 221 +++++++++++++++++++++++++++++++++++++++ src/gui/fspathedit_p.h | 67 +++++++++++- 3 files changed, 296 insertions(+), 1 deletion(-) diff --git a/src/gui/fspathedit.cpp b/src/gui/fspathedit.cpp index d5a3eda36..66d3ed314 100644 --- a/src/gui/fspathedit.cpp +++ b/src/gui/fspathedit.cpp @@ -80,6 +80,7 @@ class FileSystemPathEdit::FileSystemPathEditPrivate Mode m_mode; QString m_lastSignaledPath; QString m_dialogCaption; + Private::FileSystemPathValidator *m_validator; }; FileSystemPathEdit::FileSystemPathEditPrivate::FileSystemPathEditPrivate( @@ -89,6 +90,7 @@ FileSystemPathEdit::FileSystemPathEditPrivate::FileSystemPathEditPrivate( , m_browseAction {new QAction(q)} , m_browseBtn {new QToolButton(q)} , m_mode {FileSystemPathEdit::Mode::FileOpen} + , m_validator {new Private::FileSystemPathValidator(q)} { m_browseAction->setIconText(browseButtonBriefText.tr()); m_browseAction->setText(browseButtonFullText.tr()); @@ -97,6 +99,8 @@ FileSystemPathEdit::FileSystemPathEditPrivate::FileSystemPathEditPrivate( m_browseBtn->setDefaultAction(m_browseAction); m_fileNameFilter = tr("Any file") + QLatin1String(" (*)"); m_editor->setBrowseAction(m_browseAction); + m_validator->setStrictMode(false); + m_editor->setValidator(m_validator); modeChanged(); } @@ -163,6 +167,11 @@ void FileSystemPathEdit::FileSystemPathEditPrivate::modeChanged() } m_browseAction->setIcon(QApplication::style()->standardIcon(pixmap)); m_editor->completeDirectoriesOnly(showDirsOnly); + + m_validator->setExistingOnly(m_mode != FileSystemPathEdit::Mode::FileSave); + m_validator->setDirectoriesOnly((m_mode == FileSystemPathEdit::Mode::DirectoryOpen) || (m_mode == FileSystemPathEdit::Mode::DirectorySave)); + m_validator->setCheckReadPermission((m_mode == FileSystemPathEdit::Mode::FileOpen) || (m_mode == FileSystemPathEdit::Mode::DirectoryOpen)); + m_validator->setCheckWritePermission((m_mode == FileSystemPathEdit::Mode::FileSave) || (m_mode == FileSystemPathEdit::Mode::DirectorySave)); } FileSystemPathEdit::FileSystemPathEdit(Private::FileEditorWithCompletion *editor, QWidget *parent) diff --git a/src/gui/fspathedit_p.cpp b/src/gui/fspathedit_p.cpp index 8be1a87b7..e1762e5c0 100644 --- a/src/gui/fspathedit_p.cpp +++ b/src/gui/fspathedit_p.cpp @@ -29,13 +29,178 @@ #include "fspathedit_p.h" #include +#include #include +#include +#include + +// -------------------- FileSystemPathValidator ---------------------------------------- +Private::FileSystemPathValidator::FileSystemPathValidator(QObject *parent) + : QValidator(parent) + , m_strictMode(false) + , m_existingOnly(false) + , m_directoriesOnly(false) + , m_checkReadPermission(false) + , m_checkWritePermission(false) +{ +} + +bool Private::FileSystemPathValidator::strictMode() const +{ + return m_strictMode; +} + +void Private::FileSystemPathValidator::setStrictMode(bool v) +{ + m_strictMode = v; +} + +bool Private::FileSystemPathValidator::existingOnly() const +{ + return m_existingOnly; +} + +void Private::FileSystemPathValidator::setExistingOnly(bool v) +{ + m_existingOnly = v; +} + +bool Private::FileSystemPathValidator::directoriesOnly() const +{ + return m_directoriesOnly; +} + +void Private::FileSystemPathValidator::setDirectoriesOnly(bool v) +{ + m_directoriesOnly = v; +} + +bool Private::FileSystemPathValidator::checkReadPermission() const +{ + return m_checkReadPermission; +} + +void Private::FileSystemPathValidator::setCheckReadPermission(bool v) +{ + m_checkReadPermission = v; +} + +bool Private::FileSystemPathValidator::checkWritePermission() const +{ + return m_checkWritePermission; +} + +void Private::FileSystemPathValidator::setCheckWritePermission(bool v) +{ + m_checkWritePermission = v; +} + +QValidator::State Private::FileSystemPathValidator::validate(QString &input, int &pos) const +{ + if (input.isEmpty()) + return m_strictMode ? QValidator::Invalid : QValidator::Intermediate; + + // we test path components from beginning to the one with cursor location in strict mode + // and the one with cursor and beyond in non-strict mode + QVector components = input.splitRef(QDir::separator(), QString::KeepEmptyParts); + // find index of the component that contains pos + int componentWithCursorIndex = 0; + int pathLength = 0; + + // components.size() - 1 because when path ends with QDir::separator(), we will not see the last + // character in the components array, yet everything past the one before the last delimiter + // belongs to the last component + for (; (componentWithCursorIndex < components.size() - 1) && (pathLength < pos); ++componentWithCursorIndex) { + pathLength = components[componentWithCursorIndex].position() + components[componentWithCursorIndex].size(); + } + + Q_ASSERT(componentWithCursorIndex < components.size()); + + m_lastValidationState = QValidator::Acceptable; + if (componentWithCursorIndex > 0) + m_lastValidationState = validate(input, components, m_strictMode, 0, componentWithCursorIndex - 1); + if ((m_lastValidationState == QValidator::Acceptable) && (componentWithCursorIndex < components.size())) + m_lastValidationState = validate(input, components, false, componentWithCursorIndex, components.size() - 1); + return m_lastValidationState; +} + +QValidator::State Private::FileSystemPathValidator::validate(const QString &path, const QVector &pathComponents, bool strict, + int firstComponentToTest, int lastComponentToTest) const +{ + Q_ASSERT(firstComponentToTest >= 0); + Q_ASSERT(lastComponentToTest >= firstComponentToTest); + Q_ASSERT(lastComponentToTest < pathComponents.size()); + + m_lastTestResult = TestResult::DoesNotExist; + if (pathComponents.empty()) + return strict ? QValidator::Invalid : QValidator::Intermediate; + + for (int i = firstComponentToTest; i < lastComponentToTest; ++i) { + if (pathComponents[i].isEmpty()) continue; + + QStringRef componentPath(&path, 0, pathComponents[i].position() + pathComponents[i].size()); + m_lastTestResult = testPath(componentPath, false); + if (m_lastTestResult != TestResult::OK) { + m_lastTestedPath = componentPath.toString(); + return strict ? QValidator::Invalid : QValidator::Intermediate; + } + } + + const bool finalPath = (lastComponentToTest == (pathComponents.size() - 1)); + QStringRef componentPath(&path, 0, pathComponents[lastComponentToTest].position() + + pathComponents[lastComponentToTest].size()); + m_lastTestResult = testPath(componentPath, finalPath); + if (m_lastTestResult != TestResult::OK) { + m_lastTestedPath = componentPath.toString(); + return strict ? QValidator::Invalid : QValidator::Intermediate; + } + return QValidator::Acceptable; +} + +Private::FileSystemPathValidator::TestResult +Private::FileSystemPathValidator::testPath(const QStringRef &path, bool pathIsComplete) const +{ + QFileInfo fi(path.toString()); + if (m_existingOnly && !fi.exists()) + return TestResult::DoesNotExist; + + if ((!pathIsComplete || m_directoriesOnly) && !fi.isDir()) + return TestResult::NotADir; + + if (pathIsComplete) { + if (!m_directoriesOnly && fi.isDir()) + return TestResult::NotAFile; + + if (m_checkWritePermission && (fi.exists() && !fi.isWritable())) + return TestResult::CantWrite; + if (m_checkReadPermission && !fi.isReadable()) + return TestResult::CantRead; + } + + return TestResult::OK; +} + +Private::FileSystemPathValidator::TestResult Private::FileSystemPathValidator::lastTestResult() const +{ + return m_lastTestResult; +} + +QValidator::State Private::FileSystemPathValidator::lastValidationState() const +{ + return m_lastValidationState; +} + +QString Private::FileSystemPathValidator::lastTestedPath() const +{ + return m_lastTestedPath; +} Private::FileLineEdit::FileLineEdit(QWidget *parent) : QLineEdit {parent} , m_completerModel {new QFileSystemModel(this)} , m_completer {new QCompleter(this)} , m_browseAction {nullptr} + , m_warningAction {nullptr} { m_completerModel->setRootPath(""); m_completerModel->setIconProvider(&m_iconProvider); @@ -68,6 +233,11 @@ void Private::FileLineEdit::setBrowseAction(QAction *action) m_browseAction = action; } +void Private::FileLineEdit::setValidator(QValidator *validator) +{ + QLineEdit::setValidator(validator); +} + QWidget *Private::FileLineEdit::widget() { return this; @@ -80,6 +250,33 @@ void Private::FileLineEdit::keyPressEvent(QKeyEvent *e) m_completerModel->setRootPath(QFileInfo(text()).absoluteDir().absolutePath()); showCompletionPopup(); } + + const FileSystemPathValidator *validator = + qobject_cast(this->validator()); + if (validator) { + FileSystemPathValidator::TestResult lastTestResult = validator->lastTestResult(); + QValidator::State lastState = validator->lastValidationState(); + if (lastTestResult == FileSystemPathValidator::TestResult::OK) { + if (m_warningAction) { + delete m_warningAction; + m_warningAction = nullptr; + } + } + else { + if (!m_warningAction) { + m_warningAction = new QAction(this); + addAction(m_warningAction, QLineEdit::TrailingPosition); + } + } + + if (m_warningAction) { + if (lastState == QValidator::Invalid) + m_warningAction->setIcon(style()->standardIcon(QStyle::SP_MessageBoxCritical)); + else if (lastState == QValidator::Intermediate) + m_warningAction->setIcon(style()->standardIcon(QStyle::SP_MessageBoxWarning)); + m_warningAction->setToolTip(warningText(lastTestResult).arg(validator->lastTestedPath())); + } + } } void Private::FileLineEdit::contextMenuEvent(QContextMenuEvent *event) @@ -100,6 +297,25 @@ void Private::FileLineEdit::showCompletionPopup() m_completer->complete(); } +QString Private::FileLineEdit::warningText(FileSystemPathValidator::TestResult r) +{ + using TestResult = FileSystemPathValidator::TestResult; + switch (r) { + case TestResult::DoesNotExist: + return tr("'%1' does not exist"); + case TestResult::NotADir: + return tr("'%1' does not point to a directory"); + case TestResult::NotAFile: + return tr("'%1' does not point to a file"); + case TestResult::CantRead: + return tr("Does not have read permission in '%1'"); + case TestResult::CantWrite: + return tr("Does not have write permission in '%1'"); + default: + return QString(); + } +} + Private::FileComboEdit::FileComboEdit(QWidget *parent) : QComboBox {parent} { @@ -117,6 +333,11 @@ void Private::FileComboEdit::setBrowseAction(QAction *action) static_cast(lineEdit())->setBrowseAction(action); } +void Private::FileComboEdit::setValidator(QValidator *validator) +{ + lineEdit()->setValidator(validator); +} + void Private::FileComboEdit::setFilenameFilters(const QStringList &filters) { static_cast(lineEdit())->setFilenameFilters(filters); diff --git a/src/gui/fspathedit_p.h b/src/gui/fspathedit_p.h index 450a3ea52..618aef0b3 100644 --- a/src/gui/fspathedit_p.h +++ b/src/gui/fspathedit_p.h @@ -39,9 +39,69 @@ #include #include #include +#include +#include +#include + +class QStringList; namespace Private { + class FileSystemPathValidator: public QValidator + { + Q_OBJECT + + public: + FileSystemPathValidator(QObject *parent = nullptr); + + bool strictMode() const; + void setStrictMode(bool v); + + bool existingOnly() const; + void setExistingOnly(bool v); + + bool directoriesOnly() const; + void setDirectoriesOnly(bool v); + + bool checkReadPermission() const; + void setCheckReadPermission(bool v); + + bool checkWritePermission() const; + void setCheckWritePermission(bool v); + + QValidator::State validate(QString &input, int &pos) const override; + + enum class TestResult + { + OK, + DoesNotExist, + NotADir, + NotAFile, + CantRead, + CantWrite + }; + + TestResult lastTestResult() const; + QValidator::State lastValidationState() const; + QString lastTestedPath() const; + + private: + QValidator::State validate(const QString &path, const QVector &pathComponents, bool strict, + int firstComponentToTest, int lastComponentToTest) const; + + TestResult testPath(const QStringRef &path, bool pathIsComplete) const; + + bool m_strictMode; + bool m_existingOnly; + bool m_directoriesOnly; + bool m_checkReadPermission; + bool m_checkWritePermission; + + mutable TestResult m_lastTestResult; + mutable QValidator::State m_lastValidationState; + mutable QString m_lastTestedPath; + }; + class FileEditorWithCompletion { public: @@ -49,6 +109,7 @@ namespace Private virtual void completeDirectoriesOnly(bool completeDirsOnly) = 0; virtual void setFilenameFilters(const QStringList &filters) = 0; virtual void setBrowseAction(QAction *action) = 0; + virtual void setValidator(QValidator *validator) = 0; virtual QWidget *widget() = 0; }; @@ -64,6 +125,7 @@ namespace Private void completeDirectoriesOnly(bool completeDirsOnly) override; void setFilenameFilters(const QStringList &filters) override; void setBrowseAction(QAction *action) override; + void setValidator(QValidator *validator) override; QWidget *widget() override; protected: @@ -74,13 +136,15 @@ namespace Private void showCompletionPopup(); private: + static QString warningText(FileSystemPathValidator::TestResult r); + QFileSystemModel *m_completerModel; QCompleter *m_completer; QAction *m_browseAction; QFileIconProvider m_iconProvider; + QAction *m_warningAction; }; - class FileComboEdit: public QComboBox, public FileEditorWithCompletion { Q_OBJECT @@ -91,6 +155,7 @@ namespace Private void completeDirectoriesOnly(bool completeDirsOnly) override; void setFilenameFilters(const QStringList &filters) override; void setBrowseAction(QAction *action) override; + void setValidator(QValidator *validator) override; QWidget *widget() override; protected: