From 2c6dcc2e510dcf0f6cbd16a43eb542f25982ec5d Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Wed, 10 Jul 2019 02:14:21 +0200 Subject: [PATCH] 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. --- include/wx/gtk/textentry.h | 17 +++++- src/gtk/textctrl.cpp | 6 +- src/gtk/textentry.cpp | 120 +++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/include/wx/gtk/textentry.h b/include/wx/gtk/textentry.h index a4f92fc7b7..6198348bc7 100644 --- a/include/wx/gtk/textentry.h +++ b/include/wx/gtk/textentry.h @@ -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; }; diff --git a/src/gtk/textctrl.cpp b/src/gtk/textctrl.cpp index c30726362b..190c6840eb 100644 --- a/src/gtk/textctrl.cpp +++ b/src/gtk/textctrl.cpp @@ -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) diff --git a/src/gtk/textentry.cpp b/src/gtk/textentry.cpp index 6d5d04edbe..61819c6df3 100644 --- a/src/gtk/textentry.cpp +++ b/src/gtk/textentry.cpp @@ -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(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))