Handling Clipboard Operations in wxWidgets Text Controls
By Luke
Handling common clipboard tasks for your text fields is a must-have for any serious application that needs to accept keyboard input from the users. Surprisingly, it’s not that easy in wxWidgets, especially considering the differences between platforms (Linux, Mac, and Windows).
I explain the problem in detail in my Text Fields video (the “Clipboard Operations” chapter). In the Menus tutorial, I talk about menus in general and constructing the Edit menu with the clipboard commands in particular.
Check these out if you need more information, or scroll down for the solution to the clipboard problem in wxWidgets.
Text Fields and the Main Menu
Here’s a minimal wxWidgets application with a multiline text field and an Edit menu.
#include <wx/wx.h>
class MyApp : public wxApp
{
public:
virtual bool OnInit();
};
wxIMPLEMENT_APP(MyApp);
class MyFrame : public wxFrame
{
public:
MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size);
};
bool MyApp::OnInit()
{
MyFrame *frame = new MyFrame("Hello World", wxDefaultPosition, wxDefaultSize);
frame->Show(true);
return true;
}
MyFrame::MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size)
: wxFrame(nullptr, wxID_ANY, title, pos, size)
{
wxMenuBar *menuBar = new wxMenuBar();
wxMenu *editMenu = new wxMenu();
editMenu->Append(wxID_UNDO);
editMenu->Append(wxID_REDO);
editMenu->AppendSeparator();
editMenu->Append(wxID_CUT);
editMenu->Append(wxID_COPY);
editMenu->Append(wxID_PASTE);
menuBar->Append(editMenu, "&Edit");
SetMenuBar(menuBar);
auto textField = new wxTextCtrl(this, wxID_ANY, "",
wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE);
SetMinClientSize(FromDIP(wxSize(400,300)));
}
Note that we used default IDs for the clipboard commands in the Edit menu: wxID_CUT, wxID_COPY, and wxID_PASTE.
The Problem
In theory using the default IDs should ensure that these menu commands are handled correctly by the text control. After all, if we check out the wxTextCtrl sources, we see that indeed the control handles the related menu events:
// wxTextCtrl sources
wxBEGIN_EVENT_TABLE(wxTextCtrl, wxTextCtrlBase)
EVT_DROP_FILES(wxTextCtrl::OnDropFiles)
EVT_CHAR(wxTextCtrl::OnChar)
EVT_KEY_DOWN(wxTextCtrl::OnKeyDown)
EVT_MENU(wxID_CUT, wxTextCtrl::OnCut)
EVT_MENU(wxID_COPY, wxTextCtrl::OnCopy)
EVT_MENU(wxID_PASTE, wxTextCtrl::OnPaste)
EVT_MENU(wxID_UNDO, wxTextCtrl::OnUndo)
EVT_MENU(wxID_REDO, wxTextCtrl::OnRedo)
EVT_MENU(wxID_CLEAR, wxTextCtrl::OnDelete)
EVT_MENU(wxID_SELECTALL, wxTextCtrl::OnSelectAll)
EVT_CONTEXT_MENU(wxTextCtrl::OnContextMenu)
EVT_UPDATE_UI(wxID_CUT, wxTextCtrl::OnUpdateCut)
EVT_UPDATE_UI(wxID_COPY, wxTextCtrl::OnUpdateCopy)
EVT_UPDATE_UI(wxID_PASTE, wxTextCtrl::OnUpdatePaste)
EVT_UPDATE_UI(wxID_UNDO, wxTextCtrl::OnUpdateUndo)
EVT_UPDATE_UI(wxID_REDO, wxTextCtrl::OnUpdateRedo)
EVT_UPDATE_UI(wxID_CLEAR, wxTextCtrl::OnUpdateDelete)
EVT_UPDATE_UI(wxID_SELECTALL, wxTextCtrl::OnUpdateSelectAll)
wxEND_EVENT_TABLE()
So, we should be able to copy, cut, and paste by using our menu commands. And if you check this on MacOS, it looks like we are able to do so, so the events work!
Well, not so fast. Actually, wxWidgets’ implementation on Mac handles the clipboard events natively, bypassing its own events mechanism (by the way, if you want to know more about wxWidgets events, I recommend this video). If we try the same code on Windows and Linux, we see that there’s no reaction to the clipboard events.
So what’s wrong?
The problem is that the framework sends these events to the main frame, not to our text field. There’s no propagation upward because the main frame sits on the top of the windows hierarchy, so text fields never get the clipboard events.
The Solution
We need to manually propagate the clipboard events to the correct text field. As we may have more than one field, we want to find the one with the focus and send the proper EVT_MENU and EVT_UPDATE_UI there.
To do this, we must override the default ProcessEvent method in MyFrame. First, add the declaration to the MyFrame class:
class MyFrame : public wxFrame
{
public:
MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size);
private:
bool ProcessEvent(wxEvent &event) override;
};
Then add the implementation.
bool MyFrame::ProcessEvent(wxEvent &event)
{
static wxEvent *lastEvent = nullptr;
if (event.GetEventType() == wxEVT_MENU || event.GetEventType() == wxEVT_UPDATE_UI)
{
if (lastEvent != &event)
{
lastEvent = &event;
auto focusedChild = wxFindFocusDescendant(this);
if (focusedChild && focusedChild->GetEventHandler()->ProcessEvent(event))
{
lastEvent = nullptr;
return true;
}
lastEvent = nullptr;
}
}
return wxFrame::ProcessEvent(event);
}
We override the default behavior for two types of events:
- wxEVT_MENU - triggered when the user clicks a menu item. Initiates a clipboard action,
- wxEVT_UPDATE_UI - triggered when the menu item needs a refresh, for example, when the user opens the menu. This is a chance for the control to update the appearance of a UI item, for example, grey out the Cut or Copy menu items when there’s no selection.
Our algorithm finds the focused control and passes the event there. Note the if (lastEvent != &event)
guard. This mechanics is needed to avoid an infinite event loop: when we pass an event to a control, it propagates to the parent, finally reaching MyFrame and going into ProcessEvent again. At this point, we need to break the cycle using our static variable.
Conclusion
And that’s it. This quick fix helps with the Edit menu items behavior on Linux and Windows and makes the clipboard actions work smoothly.