From 18983cf538275077b3e918b1a9a67a8ff0e7731b Mon Sep 17 00:00:00 2001 From: AliKet Date: Fri, 26 Jan 2018 23:43:33 +0100 Subject: [PATCH] Implement dynamic auto-completion for wxGTK Make completion using custom wxTextCompleter work in wxGTK too. Closes #18061. --- include/wx/gtk/textentry.h | 15 +- samples/widgets/widgets.cpp | 38 ++++- src/gtk/textentry.cpp | 326 +++++++++++++++++++++++++++++++++--- 3 files changed, 356 insertions(+), 23 deletions(-) diff --git a/include/wx/gtk/textentry.h b/include/wx/gtk/textentry.h index 9f573d0bb4..674eab2ebd 100644 --- a/include/wx/gtk/textentry.h +++ b/include/wx/gtk/textentry.h @@ -14,6 +14,8 @@ typedef struct _GdkEventKey GdkEventKey; typedef struct _GtkEditable GtkEditable; typedef struct _GtkEntry GtkEntry; +class wxTextAutoCompleteData; // private class used only by wxTextEntry itself + // ---------------------------------------------------------------------------- // wxTextEntry: roughly corresponds to GtkEditable // ---------------------------------------------------------------------------- @@ -21,7 +23,8 @@ typedef struct _GtkEntry GtkEntry; class WXDLLIMPEXP_CORE wxTextEntry : public wxTextEntryBase { public: - wxTextEntry() { m_isUpperCase = false; } + wxTextEntry(); + virtual ~wxTextEntry(); // implement wxTextEntryBase pure virtual methods virtual void WriteText(const wxString& text) wxOVERRIDE; @@ -76,6 +79,7 @@ protected: virtual wxPoint DoGetMargins() const wxOVERRIDE; virtual bool DoAutoCompleteStrings(const wxArrayString& choices) wxOVERRIDE; + virtual bool DoAutoCompleteCustom(wxTextCompleter *completer) wxOVERRIDE; // Override the base class method to use GtkEntry IM context. virtual int GTKIMFilterKeypress(GdkEventKey* event) const; @@ -90,6 +94,15 @@ private: // implement this to return the associated GtkEntry virtual GtkEntry *GetEntry() const = 0; + wxTextAutoCompleteData *GetOrCreateCompleter(); + + // Various auto-completion-related stuff, only used if any of AutoComplete() + // methods are called. Use the function above to access it. + wxTextAutoCompleteData *m_autoCompleteData; + + // It needs to call our GetEditable() method. + friend class wxTextAutoCompleteData; + bool m_isUpperCase; }; diff --git a/samples/widgets/widgets.cpp b/samples/widgets/widgets.cpp index 9cc7de5cec..ef66b725a6 100644 --- a/samples/widgets/widgets.cpp +++ b/samples/widgets/widgets.cpp @@ -46,6 +46,7 @@ #include "wx/sizer.h" #include "wx/colordlg.h" #include "wx/fontdlg.h" +#include "wx/numdlg.h" #include "wx/textdlg.h" #include "wx/imaglist.h" #include "wx/wupdlock.h" @@ -108,6 +109,7 @@ enum TextEntry_AutoCompleteFilenames, TextEntry_AutoCompleteDirectories, TextEntry_AutoCompleteCustom, + TextEntry_AutoCompleteKeyLength, TextEntry_SetHint, TextEntry_End @@ -188,6 +190,7 @@ protected: void OnAutoCompleteFilenames(wxCommandEvent& event); void OnAutoCompleteDirectories(wxCommandEvent& event); void OnAutoCompleteCustom(wxCommandEvent& event); + void OnAutoCompleteKeyLength(wxCommandEvent& event); void OnSetHint(wxCommandEvent& event); @@ -218,6 +221,9 @@ private: // the book containing the test pages WidgetsBookCtrl *m_book; + // + int m_prefixMinLength; + // any class wishing to process wxWidgets events must use this macro wxDECLARE_EVENT_TABLE(); }; @@ -317,6 +323,7 @@ wxBEGIN_EVENT_TABLE(WidgetsFrame, wxFrame) EVT_MENU(TextEntry_AutoCompleteFilenames, WidgetsFrame::OnAutoCompleteFilenames) EVT_MENU(TextEntry_AutoCompleteDirectories, WidgetsFrame::OnAutoCompleteDirectories) EVT_MENU(TextEntry_AutoCompleteCustom, WidgetsFrame::OnAutoCompleteCustom) + EVT_MENU(TextEntry_AutoCompleteKeyLength, WidgetsFrame::OnAutoCompleteKeyLength) EVT_MENU(TextEntry_SetHint, WidgetsFrame::OnSetHint) @@ -386,6 +393,8 @@ WidgetsFrame::WidgetsFrame(const wxString& title) #endif // USE_LOG m_book = NULL; + m_prefixMinLength = 1; + #if wxUSE_MENUS // create the menubar wxMenuBar *mbar = new wxMenuBar; @@ -444,6 +453,8 @@ WidgetsFrame::WidgetsFrame(const wxString& title) menuTextEntry->AppendRadioItem(TextEntry_AutoCompleteCustom, wxT("&Custom auto-completion")); menuTextEntry->AppendSeparator(); + menuTextEntry->Append(TextEntry_AutoCompleteKeyLength, + wxT("&Minimum key length for auto-completion")); menuTextEntry->Append(TextEntry_SetHint, "Set help &hint"); mbar->Append(menuTextEntry, wxT("&Text")); @@ -1038,6 +1049,8 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) class CustomTextCompleter : public wxTextCompleterSimple { public: + CustomTextCompleter( int length ) : m_minLength( length ) {} + virtual void GetCompletions(const wxString& prefix, wxArrayString& res) wxOVERRIDE { // This is used for illustrative purposes only and shows how many @@ -1066,7 +1079,10 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) // Normally it doesn't make sense to complete empty control, there // are too many choices and listing them all wouldn't be helpful. - if ( prefix.empty() ) + // Or if we know in advance that a prefix with one or two characters + // would still results in too many choices too. + if ( prefix.empty() || + prefix.length() < static_cast(m_minLength) ) return; // The only valid strings start with 3 digits so check for their @@ -1117,9 +1133,11 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) res.push_back(prefix + c); } } + + int m_minLength; }; - if ( entry->AutoComplete(new CustomTextCompleter) ) + if ( entry->AutoComplete( new CustomTextCompleter( m_prefixMinLength ) ) ) { wxLogMessage("Enabled custom auto completer for \"NNN XX\" items " "(where N is a digit and X is a letter)."); @@ -1130,6 +1148,22 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) } } +void WidgetsFrame::OnAutoCompleteKeyLength(wxCommandEvent& WXUNUSED(event)) +{ + const wxString message = "The auto-completion is triggered if and only if\n" + "the length of the search key (prefix) is at least [LENGTH].\n" + "Hint: negative values disable auto-completion."; + const wxString prompt = "Enter the minimum key length:"; + const wxString caption = "Minimum key length"; + + m_prefixMinLength = wxGetNumberFromUser(message, prompt, caption, 1, -1, 100, this); + + wxCommandEvent theEvent(wxEVT_MENU, TextEntry_AutoCompleteCustom); + ProcessEventLocally(theEvent); + + wxLogMessage("The minimum key length for autocomplete is : %d.", m_prefixMinLength); +} + void WidgetsFrame::OnSetHint(wxCommandEvent& WXUNUSED(event)) { wxTextEntryBase *entry = CurrentPage()->GetTextEntry(); diff --git a/src/gtk/textentry.cpp b/src/gtk/textentry.cpp index 966d9b9153..a25c312529 100644 --- a/src/gtk/textentry.cpp +++ b/src/gtk/textentry.cpp @@ -25,9 +25,11 @@ #if wxUSE_TEXTCTRL || wxUSE_COMBOBOX #ifndef WX_PRECOMP + #include "wx/event.h" #include "wx/textentry.h" - #include "wx/window.h" #include "wx/textctrl.h" + #include "wx/textcompleter.h" + #include "wx/window.h" #endif //WX_PRECOMP #include @@ -193,10 +195,268 @@ wx_gtk_paste_clipboard_callback( GtkWidget *widget, wxWindow *win ) } // extern "C" +// This class gathers the all auto-complete-related stuff we use. It is +// allocated on demand by wxTextEntry when AutoComplete() is called. +// +// GTK already has completion functionality support for a GtkEntry via +// GtkEntryCompletion. This class simply forwards to GtkListStore +// in case we used ChangeStrings() overload. or to wxTextCompleter +// associated with it otherwise. +class wxTextAutoCompleteData +{ +public: + // The constructor associates us with the given text entry. + wxEXPLICIT wxTextAutoCompleteData(wxTextEntry *entry) + : m_entry(entry) + { + m_completer = NULL; + + m_isDynamicCompleter = false; + + m_newCompletionsNeeded = m_entry->IsEmpty(); + + // this asserts if entry is multiline. + // because multiline is actually a GtkTextView not a GtkEntry. + // even GetEditable() will return NULL if entry is multiline. + + wxCHECK_RET( GTK_IS_ENTRY(GetGtkEntry()), + "auto completion doesn't work with this control" ); + } + + ~wxTextAutoCompleteData() + { + delete m_completer; + } + + // Must be called after creating this object to verify if initializing it + // succeeded. + bool IsOk() const + { + return GTK_IS_ENTRY( GetGtkEntry() ); + } + + void ChangeStrings(const wxArrayString& strings) + { + wxDELETE( m_completer ); + + DoUpdateCompleterType(); + + DoEnableCompletion(); + + GtkListStore * const store = gtk_list_store_new (1, G_TYPE_STRING); + GtkTreeIter iter; + + for ( wxArrayString::const_iterator i = strings.begin(); + i != strings.end(); + ++i ) + { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, (const gchar *)i->utf8_str(), -1); + } + + gtk_entry_completion_set_model (GetEntryCompletion(), GTK_TREE_MODEL(store)); + g_object_unref (store); + + DoRefresh(); + } + + // Takes ownership of the pointer if it is non-NULL. + bool ChangeCustomCompleter(wxTextCompleter *completer) + { + delete m_completer; + m_completer = completer; + + if ( m_completer ) + { + wxTextCompleterFixed* fixedCompl = + dynamic_cast(m_completer); + + if ( fixedCompl ) + { + wxArrayString completions; + fixedCompl->GetCompletions(wxEmptyString, completions); + + ChangeStrings(completions); + + wxDELETE(m_completer); + return true; + } + + DoEnableCompletion(); + + DoUpdateCompletionModel(); + } + else + { + DisableCompletion(); + } + + DoUpdateCompleterType(); + + return true; + } + + void DisableCompletion() + { + gtk_entry_set_completion (GetGtkEntry(), NULL); + + wxDELETE(m_completer); + DoUpdateCompleterType(); + } + +private: + + void DoEnableCompletion() + { + if ( !GetEntryCompletion() ) + { + GtkEntryCompletion * const completion = gtk_entry_completion_new(); + + gtk_entry_completion_set_text_column (completion, 0); + gtk_entry_set_completion (GetGtkEntry(), completion); + } + } + + // for a given prefix, if DoUpdateCompletionModel() succeeds, + // we won't do any further update of the model as long as we + // do not clear the textentry. but then we have to start over again. + + void OnEntryChanged( wxCommandEvent& event ) + { + if ( event.GetString().empty() ) + { + m_newCompletionsNeeded = true; + } + else + { + if ( m_newCompletionsNeeded ) + DoUpdateCompletionModel(); + } + + event.Skip(); + } + + void DoUpdateCompleterType() + { + const bool isDynamic = (m_completer != NULL); + + if ( m_isDynamicCompleter == isDynamic ) + // we already connected/disconnected to/from + // the event handler. + return; + + m_isDynamicCompleter = isDynamic; + + wxWindow * const win = m_entry->GetEditableWindow(); + + // Disconnect from the event handler if we request + // a non-dynamic behaviour of our completion methode + // (e.g. completions are supplied via + // ChangeStrings() or wxTextCompleterFixed ) + // Connect otherwise. + // + // The event handler role is to request from the m_completer + // to generate dynamically new completions (e.g from database) + // if a certain condition is met (e.g. textentry is cleared + // and/or typed in text length >= *MIN_PREFIX_LENGTH* ) + // + + if ( !m_completer ) + { + win->Unbind(wxEVT_TEXT, &wxTextAutoCompleteData::OnEntryChanged, this); + } + else + { + win->Bind(wxEVT_TEXT, &wxTextAutoCompleteData::OnEntryChanged, this); + } + } + + void DoRefresh() + { + gtk_entry_completion_complete (GetEntryCompletion()); + } + + void DoUpdateCompletionModel() + { + wxASSERT_MSG( m_completer, "m_completer should not be null." ); + + const wxString prefix = m_entry->GetValue(); + + if ( m_completer->Start(prefix) ) + { + GtkListStore * const store = gtk_list_store_new (1, G_TYPE_STRING); + GtkTreeIter iter; + + for (;;) + { + const wxString s = m_completer->GetNext(); + if ( s.empty() ) + break; + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, (const gchar *)s.utf8_str(), -1); + } + + gtk_entry_completion_set_model (GetEntryCompletion(), GTK_TREE_MODEL(store)); + g_object_unref (store); + + m_newCompletionsNeeded = false; + } + else + { + gtk_entry_completion_set_model (GetEntryCompletion(), NULL); + } + + DoRefresh(); + } + + + GtkEntry* GetGtkEntry() const { return m_entry->GetEntry(); } + + GtkEntryCompletion* GetEntryCompletion() const + { + return gtk_entry_get_completion (GetGtkEntry()); + } + + // The text entry we're associated with. + wxTextEntry * const m_entry; + + // Custom completer or NULL if none. + wxTextCompleter *m_completer; + + // helps to decide if we should connect/disconnect + // to/from the event handler. + bool m_isDynamicCompleter; + + // Each time we entered a new prefix, GtkEntryCompletion needs to be fed + // with new completions. And this flag lets as try to DoUpdateCompletionModel() + // and if it succeeds, it'll set the flag to false and OnEntryChanged() + // will not try to call it again unless we entered a new prefix. + bool m_newCompletionsNeeded; + + wxDECLARE_NO_COPY_CLASS(wxTextAutoCompleteData); +}; + + // ============================================================================ // wxTextEntry implementation // ============================================================================ +// ---------------------------------------------------------------------------- +// initialization and destruction +// ---------------------------------------------------------------------------- + +wxTextEntry::wxTextEntry() +{ + m_autoCompleteData = NULL; + m_isUpperCase = false; +} + +wxTextEntry::~wxTextEntry() +{ + delete m_autoCompleteData; +} + // ---------------------------------------------------------------------------- // text operations // ---------------------------------------------------------------------------- @@ -409,32 +669,58 @@ void wxTextEntry::GetSelection(long *from, long *to) const // auto completion // ---------------------------------------------------------------------------- -bool wxTextEntry::DoAutoCompleteStrings(const wxArrayString& choices) +wxTextAutoCompleteData *wxTextEntry::GetOrCreateCompleter() { - GtkEntry* const entry = (GtkEntry*)GetEditable(); - wxCHECK_MSG(GTK_IS_ENTRY(entry), false, "auto completion doesn't work with this control"); - - GtkListStore * const store = gtk_list_store_new(1, G_TYPE_STRING); - GtkTreeIter iter; - - for ( wxArrayString::const_iterator i = choices.begin(); - i != choices.end(); - ++i ) + if ( !m_autoCompleteData ) { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - 0, (const gchar *)i->utf8_str(), - -1); + wxTextAutoCompleteData * const ac = new wxTextAutoCompleteData(this); + if ( ac->IsOk() ) + m_autoCompleteData = ac; + else + delete ac; } - GtkEntryCompletion * const completion = gtk_entry_completion_new(); - gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(store)); - gtk_entry_completion_set_text_column(completion, 0); - gtk_entry_set_completion(entry, completion); - g_object_unref(completion); + return m_autoCompleteData; +} + +bool wxTextEntry::DoAutoCompleteStrings(const wxArrayString& choices) +{ + wxTextAutoCompleteData * const ac = GetOrCreateCompleter(); + if ( !ac ) + return false; + + ac->ChangeStrings(choices); + return true; } +bool wxTextEntry::DoAutoCompleteCustom(wxTextCompleter *completer) +{ + // First deal with the case when we just want to disable auto-completion. + if ( !completer ) + { + if ( m_autoCompleteData ) + m_autoCompleteData->DisableCompletion(); + //else: Nothing to do, we hadn't used auto-completion even before. + } + else // Have a valid completer. + { + wxTextAutoCompleteData * const ac = GetOrCreateCompleter(); + if ( !ac ) + { + // Delete the custom completer for consistency with the case when + // we succeed to avoid memory leaks in user code. + delete completer; + return false; + } + + // This gives ownership of the custom completer to m_autoCompleteData. + if ( !ac->ChangeCustomCompleter(completer) ) + return false; + } + + return true; +} // ---------------------------------------------------------------------------- // editable status // ----------------------------------------------------------------------------