Implement dynamic auto-completion for wxGTK

Make completion using custom wxTextCompleter work in wxGTK too.

Closes #18061.
This commit is contained in:
AliKet
2018-01-26 23:43:33 +01:00
committed by Vadim Zeitlin
parent 9b51ef82af
commit 18983cf538
3 changed files with 356 additions and 23 deletions

View File

@@ -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;
};

View File

@@ -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<size_t>(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();

View File

@@ -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 <gtk/gtk.h>
@@ -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<wxTextCompleterFixed*>(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
// ----------------------------------------------------------------------------