Merge branch 'gtk-dyn-autocomplete'

Implement dynamic wxTextEntry autocompletion for wxGTK.

See https://github.com/wxWidgets/wxWidgets/pull/690

Closes #18061.
This commit is contained in:
Vadim Zeitlin
2018-01-27 19:27:09 +01:00
5 changed files with 339 additions and 31 deletions

View File

@@ -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.

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

View File

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

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,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<size_t>(res));
}
void WidgetsFrame::OnSetHint(wxCommandEvent& WXUNUSED(event))
{
wxTextEntryBase *entry = CurrentPage()->GetTextEntry();

View File

@@ -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 <gtk/gtk.h>
#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<GtkListStore> 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<GtkListStore> 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
// ----------------------------------------------------------------------------