diff --git a/docs/changes.txt b/docs/changes.txt index 37de4f616a..08de5a3c70 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -182,6 +182,7 @@ wxGTK: - Make wxUIActionSimulator work with GTK+ 3 (Scott Talbert). - Make wxBORDER_NONE work for wxTextCtrl with GTK+ 3 (Adrien Tétar). - Apply wxTextCtrl::SetDefaultStyle() to user-entered text (Andreas Falkenhahn). +- Implement dynamic auto-completion in wxTextEntry (AliKet). - Fix wxTextCtrl::GetStyle() with GTK+ 3. - Fix wxButton::SetBitmapPosition() with GTK+ 3 (Jake Nelson). - Support background colour in wxDataViewCtrl attributes. diff --git a/include/wx/gtk/textentry.h b/include/wx/gtk/textentry.h index 9f573d0bb4..e55fad8d4c 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,13 @@ private: // implement this to return the associated GtkEntry virtual GtkEntry *GetEntry() const = 0; + // Various auto-completion-related stuff, only used if any of AutoComplete() + // methods are called. + wxTextAutoCompleteData *m_autoCompleteData; + + // It needs to call our GetEntry() method. + friend class wxTextAutoCompleteData; + bool m_isUpperCase; }; diff --git a/interface/wx/textentry.h b/interface/wx/textentry.h index 618fde61d9..f580f86f1e 100644 --- a/interface/wx/textentry.h +++ b/interface/wx/textentry.h @@ -52,10 +52,6 @@ public: Call this function to enable auto-completion of the text typed in a single-line text control using the given @a choices. - Notice that currently this function is only implemented in wxGTK2, - wxMSW and wxOSX/Cocoa (for wxTextCtrl only, but not for wxComboBox) - ports and does nothing under the other platforms. - @since 2.9.0 @return @@ -82,9 +78,6 @@ public: Notice that you need to include @c wx/textcompleter.h in order to define your class inheriting from wxTextCompleter. - Currently this method is only implemented in wxMSW and wxOSX/Cocoa (for - wxTextCtrl only, but not for wxComboBox). - @since 2.9.2 @param completer diff --git a/samples/widgets/widgets.cpp b/samples/widgets/widgets.cpp index 9cc7de5cec..8fa5ebb5fb 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,9 @@ protected: void OnAutoCompleteFilenames(wxCommandEvent& event); void OnAutoCompleteDirectories(wxCommandEvent& event); void OnAutoCompleteCustom(wxCommandEvent& event); + void OnAutoCompleteKeyLength(wxCommandEvent& event); + + void DoUseCustomAutoComplete(size_t minLength = 1); void OnSetHint(wxCommandEvent& event); @@ -317,6 +322,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) @@ -443,6 +449,8 @@ WidgetsFrame::WidgetsFrame(const wxString& title) wxT("&Directories names auto-completion")); menuTextEntry->AppendRadioItem(TextEntry_AutoCompleteCustom, wxT("&Custom auto-completion")); + menuTextEntry->AppendRadioItem(TextEntry_AutoCompleteKeyLength, + wxT("Custom with &min length")); menuTextEntry->AppendSeparator(); menuTextEntry->Append(TextEntry_SetHint, "Set help &hint"); @@ -1025,6 +1033,11 @@ void WidgetsFrame::OnAutoCompleteDirectories(wxCommandEvent& WXUNUSED(event)) } void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) +{ + DoUseCustomAutoComplete(); +} + +void WidgetsFrame::DoUseCustomAutoComplete(size_t minLength) { wxTextEntryBase *entry = CurrentPage()->GetTextEntry(); wxCHECK_RET( entry, "menu item should be disabled" ); @@ -1038,6 +1051,11 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) class CustomTextCompleter : public wxTextCompleterSimple { public: + explicit CustomTextCompleter(size_t minLength) + : m_minLength(minLength) + { + } + virtual void GetCompletions(const wxString& prefix, wxArrayString& res) wxOVERRIDE { // This is used for illustrative purposes only and shows how many @@ -1064,9 +1082,10 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) } logCompletions(prefix, res); - // 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() ) + // Wait for enough text to be entered before proposing completions: + // this is done to avoid proposing too many of them when the + // control is empty, for example. + if ( prefix.length() < m_minLength ) return; // The only valid strings start with 3 digits so check for their @@ -1117,9 +1136,11 @@ void WidgetsFrame::OnAutoCompleteCustom(wxCommandEvent& WXUNUSED(event)) res.push_back(prefix + c); } } + + size_t m_minLength; }; - if ( entry->AutoComplete(new CustomTextCompleter) ) + if ( entry->AutoComplete(new CustomTextCompleter(minLength))) { wxLogMessage("Enabled custom auto completer for \"NNN XX\" items " "(where N is a digit and X is a letter)."); @@ -1130,6 +1151,23 @@ 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: 0 disables the length check completely."; + const wxString prompt = "Enter the minimum key length:"; + const wxString caption = "Minimum key length"; + + int res = wxGetNumberFromUser(message, prompt, caption, 1, 0, 100, this); + if ( res == -1 ) + return; + + wxLogMessage("The minimum key length for autocomplete is %d.", res); + + DoUseCustomAutoComplete(static_cast(res)); +} + void WidgetsFrame::OnSetHint(wxCommandEvent& WXUNUSED(event)) { wxTextEntryBase *entry = CurrentPage()->GetTextEntry(); diff --git a/src/gtk/textentry.cpp b/src/gtk/textentry.cpp index 966d9b9153..84144d2289 100644 --- a/src/gtk/textentry.cpp +++ b/src/gtk/textentry.cpp @@ -25,14 +25,18 @@ #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/window.h" #endif //WX_PRECOMP +#include "wx/textcompleter.h" + #include #include "wx/gtk/private.h" #include "wx/gtk/private/gtk2-compat.h" +#include "wx/gtk/private/object.h" #include "wx/gtk/private/string.h" //----------------------------------------------------------------------------- @@ -193,10 +197,239 @@ wx_gtk_paste_clipboard_callback( GtkWidget *widget, wxWindow *win ) } // extern "C" +// Base class for wxTextAutoCompleteFixed and wxTextAutoCompleteDynamic below. +class wxTextAutoCompleteData +{ +public: + // This method is only implemented by wxTextAutoCompleteFixed and will just + // return false for wxTextAutoCompleteDynamic. + virtual bool ChangeStrings(const wxArrayString& strings) = 0; + + // Conversely, this one is only implemented for wxTextAutoCompleteDynamic + // and will just return false (without taking ownership of the argument!) + // for wxTextAutoCompleteFixed. + virtual bool ChangeCompleter(wxTextCompleter* completer) = 0; + + void DisableCompletion() + { + gtk_entry_set_completion (GetGtkEntry(), NULL); + } + + virtual ~wxTextAutoCompleteData() + { + // Do not call DisableCompletion() here, it would result in problems + // when this object is destroyed from wxTextEntry dtor as the real + // control (e.g. wxTextCtrl) is already destroyed by then. + } + +protected: + // Check if completion can be used with this entry. + static bool CanComplete(wxTextEntry* entry) + { + // If this check fails, this is probably a multiline wxTextCtrl which + // doesn't have any associated GtkEntry. + return GTK_IS_ENTRY(entry->GetEntry()); + } + + explicit wxTextAutoCompleteData(wxTextEntry* entry) + : m_entry(entry) + { + GtkEntryCompletion* const completion = gtk_entry_completion_new(); + + gtk_entry_completion_set_text_column (completion, 0); + gtk_entry_set_completion (GetGtkEntry(), completion); + } + + // Provide access to wxTextEntry::GetEditableWindow() to the derived + // classes: we can call it because this class is a friend of wxTextEntry, + // but the derived classes can't do it directly. + static wxWindow* GetEditableWindow(wxTextEntry* entry) + { + return entry->GetEditableWindow(); + } + + GtkEntry* GetGtkEntry() const { return m_entry->GetEntry(); } + + // Helper function for appending a string to GtkListStore. + void AppendToStore(GtkListStore* store, const wxString& s) + { + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, (const gchar *)s.utf8_str(), -1); + } + + // Really change the completion model (which may be NULL). + void UseModel(GtkListStore* store) + { + GtkEntryCompletion* const c = gtk_entry_get_completion (GetGtkEntry()); + gtk_entry_completion_set_model (c, GTK_TREE_MODEL(store)); + gtk_entry_completion_complete (c); + } + + + // The text entry we're associated with. + wxTextEntry * const m_entry; + + wxDECLARE_NO_COPY_CLASS(wxTextAutoCompleteData); +}; + +// This class simply forwards to GtkListStore. +class wxTextAutoCompleteFixed : public wxTextAutoCompleteData +{ +public: + // Factory function, may return NULL if entry is invalid. + static wxTextAutoCompleteFixed* New(wxTextEntry *entry) + { + if ( !CanComplete(entry) ) + return NULL; + + return new wxTextAutoCompleteFixed(entry); + } + + virtual bool ChangeStrings(const wxArrayString& strings) wxOVERRIDE + { + wxGtkObject store(gtk_list_store_new (1, G_TYPE_STRING)); + + for ( wxArrayString::const_iterator i = strings.begin(); + i != strings.end(); + ++i ) + { + AppendToStore(store, *i); + } + + UseModel(store); + + return true; + } + + virtual bool ChangeCompleter(wxTextCompleter*) wxOVERRIDE + { + return false; + } + +private: + // Ctor is private, use New() to create objects of this type. + explicit wxTextAutoCompleteFixed(wxTextEntry *entry) + : wxTextAutoCompleteData(entry) + { + } + + wxDECLARE_NO_COPY_CLASS(wxTextAutoCompleteFixed); +}; + +// Dynamic completion using the provided custom wxTextCompleter. +class wxTextAutoCompleteDynamic : public wxTextAutoCompleteData +{ +public: + static wxTextAutoCompleteDynamic* New(wxTextEntry *entry) + { + if ( !CanComplete(entry) ) + return NULL; + + wxWindow * const win = GetEditableWindow(entry); + if ( !win ) + return NULL; + + return new wxTextAutoCompleteDynamic(entry, win); + } + + virtual ~wxTextAutoCompleteDynamic() + { + delete m_completer; + + m_win->Unbind(wxEVT_TEXT, &wxTextAutoCompleteDynamic::OnEntryChanged, this); + } + + virtual bool ChangeStrings(const wxArrayString&) wxOVERRIDE + { + return false; + } + + // Takes ownership of the pointer which must be non-NULL. + virtual bool ChangeCompleter(wxTextCompleter *completer) wxOVERRIDE + { + delete m_completer; + m_completer = completer; + + DoUpdateCompletionModel(); + + return true; + } + +private: + // Ctor is private, use New() to create objects of this type. + explicit wxTextAutoCompleteDynamic(wxTextEntry *entry, wxWindow *win) + : wxTextAutoCompleteData(entry), + m_win(win) + { + m_completer = NULL; + + win->Bind(wxEVT_TEXT, &wxTextAutoCompleteDynamic::OnEntryChanged, this); + } + + void OnEntryChanged(wxCommandEvent& event) + { + DoUpdateCompletionModel(); + + event.Skip(); + } + + // Recreate the model to contain all completions for the current prefix. + void DoUpdateCompletionModel() + { + const wxString& prefix = m_entry->GetValue(); + + if ( m_completer->Start(prefix) ) + { + wxGtkObject store(gtk_list_store_new (1, G_TYPE_STRING)); + + for (;;) + { + const wxString s = m_completer->GetNext(); + if ( s.empty() ) + break; + + AppendToStore(store, s); + } + + UseModel(store); + } + else + { + UseModel(NULL); + } + } + + + // Custom completer. + wxTextCompleter *m_completer; + + // The associated window, we need to store it to unbind our event handler. + wxWindow* const m_win; + + wxDECLARE_NO_COPY_CLASS(wxTextAutoCompleteDynamic); +}; + + // ============================================================================ // wxTextEntry implementation // ============================================================================ +// ---------------------------------------------------------------------------- +// initialization and destruction +// ---------------------------------------------------------------------------- + +wxTextEntry::wxTextEntry() +{ + m_autoCompleteData = NULL; + m_isUpperCase = false; +} + +wxTextEntry::~wxTextEntry() +{ + delete m_autoCompleteData; +} + // ---------------------------------------------------------------------------- // text operations // ---------------------------------------------------------------------------- @@ -411,30 +644,62 @@ void wxTextEntry::GetSelection(long *from, long *to) const bool wxTextEntry::DoAutoCompleteStrings(const wxArrayString& choices) { - 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 ) + // Try to update the existing data first. + if ( !m_autoCompleteData || !m_autoCompleteData->ChangeStrings(choices) ) { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - 0, (const gchar *)i->utf8_str(), - -1); + // If it failed, try creating a new object for fixed completion. + wxTextAutoCompleteFixed* const ac = wxTextAutoCompleteFixed::New(this); + if ( !ac ) + return false; + + ac->ChangeStrings(choices); + + delete m_autoCompleteData; + m_autoCompleteData = 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 true; } +bool wxTextEntry::DoAutoCompleteCustom(wxTextCompleter *completer) +{ + // First deal with the case when we just want to disable auto-completion. + if ( !completer ) + { + if ( m_autoCompleteData ) + { + // This is not done in dtor because it's unnecessary when replacing + // one completer with another one, and even dangerous, when the + // control is being destroyed anyhow, so we need to call it + // explicitly here to really disable completion. + m_autoCompleteData->DisableCompletion(); + + delete m_autoCompleteData; + m_autoCompleteData = NULL; + } + //else: Nothing to do, we hadn't used auto-completion even before. + } + else // Have a valid completer. + { + // As above, try to update the completer of the existing object first + // and fall back on creating a new one. + if ( !m_autoCompleteData || + !m_autoCompleteData->ChangeCompleter(completer) ) + { + wxTextAutoCompleteDynamic* const + ac = wxTextAutoCompleteDynamic::New(this); + if ( !ac ) + return false; + + ac->ChangeCompleter(completer); + + delete m_autoCompleteData; + m_autoCompleteData = ac; + } + } + + return true; +} // ---------------------------------------------------------------------------- // editable status // ----------------------------------------------------------------------------