From 602f4f3334d6d934af22097fc1b70c0912eeb4b9 Mon Sep 17 00:00:00 2001 From: AliKet Date: Fri, 31 Jan 2025 20:16:21 +0100 Subject: [PATCH] wxQt: Add auto-completion support to wxTextEntry Closes #25133. --- include/wx/qt/textentry.h | 13 ++ src/qt/textentry.cpp | 272 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) diff --git a/include/wx/qt/textentry.h b/include/wx/qt/textentry.h index 997c716d9e..23b8a54e92 100644 --- a/include/wx/qt/textentry.h +++ b/include/wx/qt/textentry.h @@ -8,6 +8,8 @@ #ifndef _WX_QT_TEXTENTRY_H_ #define _WX_QT_TEXTENTRY_H_ +class wxTextAutoCompleteData; // private class used only by wxTextEntry itself + class WXDLLIMPEXP_CORE wxTextEntry : public wxTextEntryBase { public: @@ -40,10 +42,21 @@ protected: virtual wxString DoGetValue() const override; virtual void DoSetValue(const wxString& value, int flags=0) override; + virtual bool DoAutoCompleteStrings(const wxArrayString& choices) override; + virtual bool DoAutoCompleteFileNames(int flags) override; + virtual bool DoAutoCompleteCustom(wxTextCompleter* completer) override; + virtual wxWindow *GetEditableWindow() override; // Block/unblock the corresponding Qt signal. virtual void EnableTextChangedEvents(bool enable) override; + + // Various auto-completion-related stuff, only used if any of AutoComplete() + // methods are called. Use the function above to access it. + wxTextAutoCompleteData* m_autoCompleteData = nullptr; + + // It needs to call our GetEditableWindow() method. + friend class wxTextAutoCompleteData; }; #endif // _WX_QT_TEXTENTRY_H_ diff --git a/src/qt/textentry.cpp b/src/qt/textentry.cpp index 7f25b40110..ae3752b714 100644 --- a/src/qt/textentry.cpp +++ b/src/qt/textentry.cpp @@ -9,8 +9,16 @@ #include "wx/wxprec.h" #include "wx/textentry.h" +#include "wx/textcompleter.h" #include "wx/window.h" +#include "wx/qt/private/converter.h" +#include +#include +#include + +#include +#include #include wxTextEntry::wxTextEntry() @@ -122,3 +130,267 @@ void wxTextEntry::EnableTextChangedEvents(bool enable) if ( win ) win->GetHandle()->blockSignals(!enable); } + +// ---------------------------------------------------------------------------- +// auto-completion +// ---------------------------------------------------------------------------- +namespace +{ +// This class is taken from Qt documentation "as is" to see "C:\Program Files" +// instead of just "Program Files" as QFileSystemModel does by default. +class FileSystemModel : public QFileSystemModel +{ +public: + explicit FileSystemModel(QObject* parent = nullptr) : QFileSystemModel(parent) + { + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if ( role == Qt::DisplayRole && index.column() == 0 ) + { + QString path = QDir::toNativeSeparators(filePath(index)); + + if ( path.endsWith(QDir::separator()) ) + path.chop(1); + + return path; + } + + return QFileSystemModel::data(index, role); + } +}; +} + +// This class gathers the all auto-complete-related stuff we use. It is +// allocated on demand by wxTextEntry when AutoComplete() is called. +class wxTextAutoCompleteData +{ +public: + enum class CompleterType + { + StringCompleter, + FileSystemCompleter + }; + + // The constructor associates us with the given text entry. + explicit wxTextAutoCompleteData(wxTextEntry* entry, + CompleterType type = CompleterType::StringCompleter) + : m_entry(entry), + m_win(entry->GetEditableWindow()) + { + if ( m_win ) + { + auto qtWidget = m_win->GetHandle(); + m_qtCompleter = new QCompleter(qtWidget->parentWidget()); + + if ( type == CompleterType::FileSystemCompleter ) + { + auto fsModel = new FileSystemModel(m_qtCompleter); + fsModel->setRootPath(QDir::currentPath()); + m_qtCompleter->setModel(fsModel); + } + else + { + m_qtCompleter->setModel( new QStringListModel(m_qtCompleter) ); + m_qtCompleter->setCaseSensitivity(Qt::CaseInsensitive); + } + + if ( auto lineEdit = qobject_cast(qtWidget) ) + { + lineEdit->setCompleter(m_qtCompleter); + } + else if ( auto comboBox = qobject_cast(qtWidget) ) + { + comboBox->setCompleter(m_qtCompleter); + } + else + { + wxDELETE(m_qtCompleter); + } + } + } + + ~wxTextAutoCompleteData() + { + ChangeCustomCompleter(nullptr); // disable custom completer if any. + } + + bool IsOk() const { return m_qtCompleter != nullptr; } + + void ChangeStrings(const wxArrayString& strings) + { + ChangeCustomCompleter(nullptr); // disable custom completer if any. + + auto listModel = qobject_cast(m_qtCompleter->model()); + + wxASSERT_MSG(listModel, "A QStringListModel object is expected here"); + + QStringList list; + + for ( auto str : strings ) + { + list << wxQtConvertString( str ); + } + + listModel->setStringList(list); + } + + // Takes ownership of the pointer if it is non-null. + bool ChangeCustomCompleter(wxTextCompleter* completer) + { + if ( m_completer ) + m_win->Unbind(wxEVT_TEXT, &wxTextAutoCompleteData::OnEntryChanged, this); + + delete m_completer; + m_completer = completer; + + if ( m_completer ) + m_win->Bind(wxEVT_TEXT, &wxTextAutoCompleteData::OnEntryChanged, this); + + return true; + } + + void DisableCompletion() + { + if ( m_qtCompleter ) + { + auto qtWidget = m_qtCompleter->widget(); + + if ( auto lineEdit = qobject_cast(qtWidget) ) + { + lineEdit->setCompleter(nullptr); + } + else if ( auto comboBox = qobject_cast(qtWidget) ) + { + comboBox->setCompleter(nullptr); + } + + m_qtCompleter = nullptr; + } + } + +private: + // Update the strings returned by QCompleter to correspond to + // the currently valid choices according to the custom completer. + void UpdateStringsFromCustomCompleter() + { + if ( !m_completer ) + return; + + auto listModel = qobject_cast(m_qtCompleter->model()); + + wxASSERT_MSG(listModel, "A QStringListModel object is expected here"); + + const wxString prefix = wxQtConvertString(m_qtCompleter->completionPrefix()); + + if ( m_prefix == prefix ) + return; + + QStringList list; + + if ( m_completer->Start(prefix) ) + { + m_prefix = prefix; + + for (;;) + { + const wxString s = m_completer->GetNext(); + + if ( s.empty() ) + break; + + list << wxQtConvertString(s); + } + } + + listModel->setStringList(list); + } + + void OnEntryChanged(wxCommandEvent& event) + { + UpdateStringsFromCustomCompleter(); + + event.Skip(); + } + + // The text entry we're associated with. + wxTextEntry* const m_entry; + + // The window of this text entry. + wxWindow* const m_win; + + // Custom completer or nullptr if none. + wxTextCompleter* m_completer = nullptr; + + // This pointer is owned by the underlying QWidget (QLineEdit, QComboBox, ...) + QCompleter* m_qtCompleter = nullptr; + + wxString m_prefix; + + wxDECLARE_NO_COPY_CLASS(wxTextAutoCompleteData); +}; + +bool wxTextEntry::DoAutoCompleteFileNames(int WXUNUSED(flags)) +{ + if ( m_autoCompleteData ) + { + m_autoCompleteData->DisableCompletion(); + + wxDELETE(m_autoCompleteData); + } + + auto fsCompleter = wxTextAutoCompleteData::CompleterType::FileSystemCompleter; + + std::unique_ptr + autoCompleteData( new wxTextAutoCompleteData(this, fsCompleter) ); + + if ( autoCompleteData->IsOk() ) + { + m_autoCompleteData = autoCompleteData.release(); + } + + return m_autoCompleteData != nullptr; +} + +bool wxTextEntry::DoAutoCompleteStrings(const wxArrayString& choices) +{ + if ( m_autoCompleteData ) + { + m_autoCompleteData->DisableCompletion(); + + wxDELETE(m_autoCompleteData); + } + + std::unique_ptr + autoCompleteData( new wxTextAutoCompleteData(this) ); + + if ( autoCompleteData->IsOk() ) + { + m_autoCompleteData = autoCompleteData.release(); + m_autoCompleteData->ChangeStrings(choices); + } + + return m_autoCompleteData != nullptr; +} + +bool wxTextEntry::DoAutoCompleteCustom(wxTextCompleter* completer) +{ + if ( m_autoCompleteData ) + { + m_autoCompleteData->DisableCompletion(); + + wxDELETE(m_autoCompleteData); + } + + std::unique_ptr + autoCompleteData( new wxTextAutoCompleteData(this) ); + + if ( autoCompleteData->IsOk() ) + { + m_autoCompleteData = autoCompleteData.release(); + m_autoCompleteData->ChangeCustomCompleter(completer); + } + + return m_autoCompleteData != nullptr; +}