Merge branch 'gtk-text-changed-coalesce'

Harmonize wxEVT_TEXT events in wxGTK with other ports.

Also use IME for wxComboBox in wxGTK too.

See https://github.com/wxWidgets/wxWidgets/pull/1400
This commit is contained in:
Vadim Zeitlin
2019-07-16 19:41:32 +02:00
9 changed files with 283 additions and 91 deletions

View File

@@ -145,13 +145,16 @@ protected:
virtual GtkEntry *GetEntry() const wxOVERRIDE
{ return m_entry; }
virtual int GTKIMFilterKeypress(GdkEventKey* event) const wxOVERRIDE
{ return GTKEntryIMFilterKeypress(event); }
GtkEntry* m_entry;
private:
// From wxTextEntry:
virtual wxWindow *GetEditableWindow() wxOVERRIDE { return this; }
virtual GtkEditable *GetEditable() const wxOVERRIDE;
virtual void EnableTextChangedEvents(bool enable) wxOVERRIDE;
void Init();

View File

@@ -142,6 +142,8 @@ public:
static wxVisualAttributes
GetClassDefaultAttributes(wxWindowVariant variant = wxWINDOW_VARIANT_NORMAL);
void GTKOnTextChanged() wxOVERRIDE;
protected:
// overridden wxWindow virtual methods
virtual wxSize DoGetBestSize() const wxOVERRIDE;
@@ -182,7 +184,6 @@ private:
// overridden wxTextEntry virtual methods
virtual GtkEditable *GetEditable() const wxOVERRIDE;
virtual GtkEntry *GetEntry() const wxOVERRIDE;
virtual void EnableTextChangedEvents(bool enable) wxOVERRIDE;
// change the font for everything in this control
void ChangeFontGlobally();
@@ -196,7 +197,7 @@ private:
// returns either m_text or m_buffer depending on whether the control is
// single- or multi-line; convenient for the GTK+ functions which work with
// both
void *GetTextObject() const
void *GetTextObject() const wxOVERRIDE
{
return IsMultiLine() ? static_cast<void *>(m_buffer)
: static_cast<void *>(m_text);

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,6 +63,16 @@ public:
bool GTKEntryOnInsertText(const char* text);
bool GTKIsUpperCase() const { return m_isUpperCase; }
// 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.
@@ -70,6 +81,10 @@ protected:
// And this one to connect "insert-text" signal.
void GTKConnectInsertTextSignal(GtkEntry* entry);
// Finally this one connects to the "changed" signal on the object returned
// by GetTextObject().
void GTKConnectChangedSignal();
virtual void DoSetValue(const wxString& value, int flags) wxOVERRIDE;
virtual wxString DoGetValue() const wxOVERRIDE;
@@ -81,11 +96,24 @@ protected:
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;
// Call this from the overridden wxWindow::GTKIMFilterKeypress() to use
// 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.
//
// Note that we make it protected in wxGTK as it is called from wxComboBox
// currently.
virtual void EnableTextChangedEvents(bool enable) wxOVERRIDE;
private:
// implement this to return the associated GtkEntry or another widget
// implementing GtkEditable
@@ -94,6 +122,12 @@ private:
// implement this to return the associated GtkEntry
virtual GtkEntry *GetEntry() const = 0;
// This one exists in order to be overridden by wxTextCtrl which uses
// either GtkEditable or GtkTextBuffer depending on whether it is single-
// or multi-line.
virtual void *GetTextObject() const { return GetEntry(); }
// Various auto-completion-related stuff, only used if any of AutoComplete()
// methods are called.
wxTextAutoCompleteData *m_autoCompleteData;
@@ -101,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

@@ -31,6 +31,7 @@
#include "wx/bitmap.h"
#include "wx/button.h"
#include "wx/checkbox.h"
#include "wx/dcclient.h"
#include "wx/radiobox.h"
#include "wx/statbox.h"
#include "wx/stattext.h"
@@ -986,19 +987,21 @@ void TextWidgetsPage::OnUpdateUIResetButton(wxUpdateUIEvent& event)
(m_radioWrap->GetSelection() != DEFAULTS.wrapStyle) );
}
void TextWidgetsPage::OnText(wxCommandEvent& WXUNUSED(event))
void TextWidgetsPage::OnText(wxCommandEvent& event)
{
// small hack to suppress the very first message: by then the logging is
// not yet redirected and so initial setting of the text value results in
// an annoying message box
static bool s_firstTime = true;
if ( s_firstTime )
{
s_firstTime = false;
if ( !IsUsingLogWindow() )
return;
}
wxLogMessage("Text ctrl value changed");
// Replace middle of long text with ellipsis just to avoid filling up the
// log control with too much unnecessary stuff.
wxLogMessage("Text control value changed (now '%s')",
wxControl::Ellipsize
(
event.GetString(),
wxClientDC(this),
wxELLIPSIZE_MIDDLE,
GetTextExtent('W').x*100
));
}
void TextWidgetsPage::OnTextEnter(wxCommandEvent& event)

View File

@@ -139,6 +139,13 @@ const wxChar *WidgetsCategories[MAX_PAGES] = {
class WidgetsApp : public wxApp
{
public:
WidgetsApp()
{
#if USE_LOG
m_logTarget = NULL;
#endif // USE_LOG
}
// override base class virtuals
// ----------------------------
@@ -146,8 +153,20 @@ public:
// initialization (doing it here and not in the ctor allows to have an error
// return: if OnInit() returns false, the application terminates)
virtual bool OnInit() wxOVERRIDE;
// real implementation of WidgetsPage method with the same name
bool IsUsingLogWindow() const;
private:
#if USE_LOG
wxLog* m_logTarget;
#endif // USE_LOG
wxDECLARE_NO_COPY_CLASS(WidgetsApp);
};
wxDECLARE_APP(WidgetsApp); // This provides a convenient wxGetApp() accessor.
// Define a new frame type: this is going to be our main frame
class WidgetsFrame : public wxFrame
{
@@ -375,9 +394,22 @@ bool WidgetsApp::OnInit()
wxFrame *frame = new WidgetsFrame(title + " widgets demo");
frame->Show();
#if USE_LOG
m_logTarget = wxLog::GetActiveTarget();
#endif // USE_LOG
return true;
}
bool WidgetsApp::IsUsingLogWindow() const
{
#if USE_LOG
return wxLog::GetActiveTarget() == m_logTarget;
#else // !USE_LOG
return false;
#endif // USE_LOG
}
// ----------------------------------------------------------------------------
// WidgetsFrame construction
// ----------------------------------------------------------------------------
@@ -1200,8 +1232,13 @@ void WidgetsFrame::OnSetHint(wxCommandEvent& WXUNUSED(event))
void WidgetsFrame::OnWidgetFocus(wxFocusEvent& event)
{
wxLogMessage("Widgets %s focus",
event.GetEventType() == wxEVT_SET_FOCUS ? "got" : "lost");
// Don't show annoying message boxes when starting or closing the sample,
// only log these events in our own logger.
if ( wxGetApp().IsUsingLogWindow() )
{
wxLogMessage("Widgets %s focus",
event.GetEventType() == wxEVT_SET_FOCUS ? "got" : "lost");
}
event.Skip();
}
@@ -1384,3 +1421,9 @@ wxCheckBox *WidgetsPage::CreateCheckBoxAndAddToSizer(wxSizer *sizer,
return checkbox;
}
/* static */
bool WidgetsPage::IsUsingLogWindow()
{
return wxGetApp().IsUsingLogWindow();
}

View File

@@ -155,6 +155,10 @@ public:
// the default attributes for the widget
static WidgetAttributes& GetAttrs();
// return true if we're showing logs in the log window (always the case
// except during startup and shutdown)
static bool IsUsingLogWindow();
protected:
// several helper functions for page creation

View File

@@ -27,14 +27,6 @@
// ----------------------------------------------------------------------------
extern "C" {
static void
gtkcombobox_text_changed_callback( GtkWidget *WXUNUSED(widget), wxComboBox *combo )
{
wxCommandEvent event( wxEVT_TEXT, combo->GetId() );
event.SetString( combo->GetValue() );
event.SetEventObject( combo );
combo->HandleWindowEvent( event );
}
static void
gtkcombobox_changed_callback( GtkWidget *WXUNUSED(widget), wxComboBox *combo )
@@ -185,9 +177,7 @@ bool wxComboBox::Create( wxWindow *parent, wxWindowID id, const wxString& value,
gtk_entry_set_text( entry, wxGTK_CONV(value) );
}
g_signal_connect_after (entry, "changed",
G_CALLBACK (gtkcombobox_text_changed_callback), this);
GTKConnectChangedSignal();
GTKConnectInsertTextSignal(entry);
GTKConnectClipboardSignals(GTK_WIDGET(entry));
}
@@ -219,7 +209,7 @@ void wxComboBox::GTKCreateComboBoxWidget()
GtkEditable *wxComboBox::GetEditable() const
{
return GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(m_widget)));
return GTK_EDITABLE(m_entry);
}
void wxComboBox::OnChar( wxKeyEvent &event )
@@ -248,23 +238,6 @@ void wxComboBox::OnChar( wxKeyEvent &event )
event.Skip();
}
void wxComboBox::EnableTextChangedEvents(bool enable)
{
if ( !GetEntry() )
return;
if ( enable )
{
g_signal_handlers_unblock_by_func(gtk_bin_get_child(GTK_BIN(m_widget)),
(gpointer)gtkcombobox_text_changed_callback, this);
}
else // disable
{
g_signal_handlers_block_by_func(gtk_bin_get_child(GTK_BIN(m_widget)),
(gpointer)gtkcombobox_text_changed_callback, this);
}
}
void wxComboBox::GTKDisableEvents()
{
EnableTextChangedEvents(false);

View File

@@ -567,24 +567,6 @@ gtk_textctrl_populate_popup( GtkEntry *WXUNUSED(entry), GtkMenu *menu, wxTextCtr
}
}
//-----------------------------------------------------------------------------
// "changed"
//-----------------------------------------------------------------------------
extern "C" {
static void
gtk_text_changed_callback( GtkWidget *WXUNUSED(widget), wxTextCtrl *win )
{
if ( win->IgnoreTextUpdate() )
return;
if ( win->MarkDirtyOnChange() )
win->MarkDirty();
win->SendTextUpdatedEvent();
}
}
//-----------------------------------------------------------------------------
// "mark_set"
//-----------------------------------------------------------------------------
@@ -774,16 +756,7 @@ bool wxTextCtrl::Create( wxWindow *parent,
}
// We want to be notified about text changes.
if (multi_line)
{
g_signal_connect (m_buffer, "changed",
G_CALLBACK (gtk_text_changed_callback), this);
}
else
{
g_signal_connect (m_text, "changed",
G_CALLBACK (gtk_text_changed_callback), this);
}
GTKConnectChangedSignal();
// Catch to disable focus out handling
g_signal_connect (m_text, "populate_popup",
@@ -885,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)
@@ -1371,24 +1348,21 @@ void wxTextCtrl::DiscardEdits()
m_modified = false;
}
void wxTextCtrl::GTKOnTextChanged()
{
if ( IgnoreTextUpdate() )
return;
if ( MarkDirtyOnChange() )
MarkDirty();
SendTextUpdatedEvent();
}
// ----------------------------------------------------------------------------
// event handling
// ----------------------------------------------------------------------------
void wxTextCtrl::EnableTextChangedEvents(bool enable)
{
if ( enable )
{
g_signal_handlers_unblock_by_func(GetTextObject(),
(gpointer)gtk_text_changed_callback, this);
}
else // disable events
{
g_signal_handlers_block_by_func(GetTextObject(),
(gpointer)gtk_text_changed_callback, this);
}
}
bool wxTextCtrl::IgnoreTextUpdate()
{
if ( m_countUpdatesToIgnore > 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
//-----------------------------------------------------------------------------
@@ -57,8 +116,45 @@ static int GetEntryTextLength(GtkEntry* entry)
// signal handlers implementation
// ============================================================================
// "insert_text" handler for GtkEntry
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();
}
// "insert_text" handler for GtkEntry
static void
wx_gtk_insert_text_callback(GtkEditable *editable,
const gchar * new_text,
@@ -510,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;
}
@@ -854,8 +952,38 @@ void wxTextEntry::ForceUpper()
// IM handling
// ----------------------------------------------------------------------------
int wxTextEntry::GTKIMFilterKeypress(GdkEventKey* event) const
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))
@@ -869,6 +997,31 @@ int wxTextEntry::GTKIMFilterKeypress(GdkEventKey* event) const
return result;
}
// ----------------------------------------------------------------------------
// signals and events
// ----------------------------------------------------------------------------
void wxTextEntry::EnableTextChangedEvents(bool enable)
{
if ( enable )
{
g_signal_handlers_unblock_by_func(GetTextObject(),
(gpointer)wx_gtk_text_changed_callback, this);
}
else // disable events
{
g_signal_handlers_block_by_func(GetTextObject(),
(gpointer)wx_gtk_text_changed_callback, this);
}
}
void wxTextEntry::GTKConnectChangedSignal()
{
g_signal_connect(GetTextObject(), "changed",
G_CALLBACK(wx_gtk_text_changed_callback), this);
}
void wxTextEntry::GTKConnectInsertTextSignal(GtkEntry* entry)
{
g_signal_connect(entry, "insert_text",