Coalesce wxEVT_TEXT events in wxGTK wxTextCtrl and wxComboBox

For consistency with the other platforms, coalesce multiple wxEVT_TEXT
events resulting from a single user action into a single one in wxGTK
too. For example, when pressing a key in a control with some text
selected, wxGTK previously generated 2 wxEVT_TEXT events: one
corresponding to the removal of the selection and another one to the
addition of the new text. Now only a single event with the new text is
generated, as in the other ports.

Doing this requires delaying sending wxEVT_TEXT until GTK itself ends
handling the key press, however we delay it as little as possible, so
hopefully this shouldn't have any visible effects at wx API level.

Closes #10050.
This commit is contained in:
Vadim Zeitlin 2019-07-10 02:14:21 +02:00
parent 958df5fb74
commit 2c6dcc2e51
3 changed files with 141 additions and 2 deletions

View File

@ -15,6 +15,7 @@ typedef struct _GtkEditable GtkEditable;
typedef struct _GtkEntry GtkEntry;
class wxTextAutoCompleteData; // private class used only by wxTextEntry itself
class wxTextCoalesceData; // another private class
// ----------------------------------------------------------------------------
// wxTextEntry: roughly corresponds to GtkEditable
@ -62,12 +63,16 @@ public:
bool GTKEntryOnInsertText(const char* text);
bool GTKIsUpperCase() const { return m_isUpperCase; }
// Called from "changed" signal handler for GtkEntry.
// Called from "changed" signal handler (or, possibly, slightly later, when
// coalescing several "changed" signals into a single event) for GtkEntry.
//
// By default just generates a wxEVT_TEXT, but overridden to do more things
// in wxTextCtrl.
virtual void GTKOnTextChanged() { SendTextUpdatedEvent(); }
// Helper functions only used internally.
wxTextCoalesceData* GTKGetCoalesceData() const { return m_coalesceData; }
protected:
// This method must be called from the derived class Create() to connect
// the handlers for the clipboard (cut/copy/paste) events.
@ -95,6 +100,12 @@ protected:
// GtkEntry IM context.
int GTKEntryIMFilterKeypress(GdkEventKey* event) const;
// If GTKEntryIMFilterKeypress() is not called (as multiline wxTextCtrl
// uses its own IM), call this method instead to still notify wxTextEntry
// about the key press events in the given widget.
void GTKEntryOnKeypress(GtkWidget* widget) const;
static int GTKGetEntryTextLength(GtkEntry* entry);
// Block/unblock the corresponding GTK signal.
@ -124,6 +135,10 @@ private:
// It needs to call our GetEntry() method.
friend class wxTextAutoCompleteData;
// Data used for coalescing "changed" events resulting from a single user
// action.
mutable wxTextCoalesceData* m_coalesceData;
bool m_isUpperCase;
};

View File

@ -858,7 +858,11 @@ GtkEntry *wxTextCtrl::GetEntry() const
int wxTextCtrl::GTKIMFilterKeypress(GdkEventKey* event) const
{
if (IsSingleLine())
return wxTextEntry::GTKIMFilterKeypress(event);
return GTKEntryIMFilterKeypress(event);
// When not calling GTKEntryIMFilterKeypress(), we need to notify the code
// in wxTextEntry about the key presses explicitly.
GTKEntryOnKeypress(m_text);
int result = false;
#if GTK_CHECK_VERSION(2, 22, 0)

View File

@ -37,6 +37,65 @@
#include "wx/gtk/private/object.h"
#include "wx/gtk/private/string.h"
// ----------------------------------------------------------------------------
// wxTextCoalesceData
// ----------------------------------------------------------------------------
class wxTextCoalesceData
{
public:
wxTextCoalesceData(GtkWidget* widget, gulong handlerAfterKeyPress)
: m_handlerAfterKeyPress(handlerAfterKeyPress)
{
m_inKeyPress = false;
m_pendingTextChanged = false;
// This signal handler is unblocked in StartHandlingKeyPress(), so
// we need to block it initially to compensate for this.
g_signal_handler_block(widget, m_handlerAfterKeyPress);
}
void StartHandlingKeyPress(GtkWidget* widget)
{
m_inKeyPress = true;
m_pendingTextChanged = false;
g_signal_handler_unblock(widget, m_handlerAfterKeyPress);
}
bool SetPendingIfInKeyPress()
{
if ( !m_inKeyPress )
return false;
m_pendingTextChanged = true;
return true;
}
bool EndHandlingKeyPressAndCheckIfPending(GtkWidget* widget)
{
g_signal_handler_block(widget, m_handlerAfterKeyPress);
wxASSERT( m_inKeyPress );
m_inKeyPress = false;
if ( !m_pendingTextChanged )
return false;
m_pendingTextChanged = false;
return true;
}
private:
bool m_inKeyPress;
bool m_pendingTextChanged;
const gulong m_handlerAfterKeyPress;
wxDECLARE_NO_COPY_CLASS(wxTextCoalesceData);
};
//-----------------------------------------------------------------------------
// helper function to get the length of the text
//-----------------------------------------------------------------------------
@ -59,10 +118,39 @@ static int GetEntryTextLength(GtkEntry* entry)
extern "C" {
// "event-after" handler is only connected when we get a "key-press-event", so
// it's effectively called after the end of processing of this event and used
// to send a single wxEVT_TEXT even if we received several (typically two, when
// the selected text in the control is replaced by new text) "changed" signals.
static gboolean
wx_gtk_text_after_key_press(GtkWidget* widget,
GdkEventKey* WXUNUSED(gdk_event),
wxTextEntry* entry)
{
wxTextCoalesceData* const data = entry->GTKGetCoalesceData();
wxCHECK_MSG( data, FALSE, "must be non-null if this handler is called" );
if ( data->EndHandlingKeyPressAndCheckIfPending(widget) )
{
entry->GTKOnTextChanged();
}
return FALSE;
}
// "changed" handler for GtkEntry
static void
wx_gtk_text_changed_callback(GtkWidget* WXUNUSED(widget), wxTextEntry* entry)
{
if ( wxTextCoalesceData* const data = entry->GTKGetCoalesceData() )
{
if ( data->SetPendingIfInKeyPress() )
{
// Don't send the event right now as more might be coming.
return;
}
}
entry->GTKOnTextChanged();
}
@ -518,11 +606,13 @@ wx_gtk_entry_parent_grab_notify (GtkWidget *widget,
wxTextEntry::wxTextEntry()
{
m_autoCompleteData = NULL;
m_coalesceData = NULL;
m_isUpperCase = false;
}
wxTextEntry::~wxTextEntry()
{
delete m_coalesceData;
delete m_autoCompleteData;
}
@ -862,8 +952,38 @@ void wxTextEntry::ForceUpper()
// IM handling
// ----------------------------------------------------------------------------
void wxTextEntry::GTKEntryOnKeypress(GtkWidget* widget) const
{
// We coalesce possibly multiple events resulting from a single key press
// (this always happens when there is a selection, as we always get a
// "changed" event when the selection is removed and another one when the
// new text is inserted) into a single wxEVT_TEXT and to do this we need
// this extra handler.
if ( !m_coalesceData )
{
// We can't use g_signal_connect_after("key-press-event") because the
// emission of this signal is stopped by GtkEntry own key-press-event
// handler, so we have to use the generic "event-after" instead to be
// notified about the end of handling of this key press and to send any
// pending events a.s.a.p.
const gulong handler = g_signal_connect
(
widget,
"event-after",
G_CALLBACK(wx_gtk_text_after_key_press),
const_cast<wxTextEntry*>(this)
);
m_coalesceData = new wxTextCoalesceData(widget, handler);
}
m_coalesceData->StartHandlingKeyPress(widget);
}
int wxTextEntry::GTKEntryIMFilterKeypress(GdkEventKey* event) const
{
GTKEntryOnKeypress(GTK_WIDGET(GetEntry()));
int result = false;
#if GTK_CHECK_VERSION(2, 22, 0)
if (wx_is_at_least_gtk2(22))