I solved it using EVT_CHILD_FOCUS events and catching EVT_KILL_FOCUS in the focused child. The idea is that when a child is focused the RecordEditor will get a EVT_CHILD_FOCUS. If a window outside of the RecordEditor is focused then the focused child will receive EVT_KILL_FOCUS. If a child receives a kill event and the RecordEditor doesn't receive a child focus event immediately after then a window outside of the RecordEditor was selected. Note that EVT_CHILD_FOCUS will be received even when the RecordEditor itself is focused.
Firstly RecordEditor inherits from wxWindow or wxControl, not from wxPanel because it causes wierd problems (on windows at least). Secondly I created handlers for EVT_CHILD_FOCUS and EVT_IDLE and allocate a new event handler which handles EVT_KILL_FOCUS using the RecordEditor as a sink.
- On EVT_CHILD_FOCUS call FindFocus to get the focused child. Save it in a member variable called focusedChild and push in it the event handler for kill focus events.
- On EVT_KILL_FOCUS (this will be called by the focused child control) remove the event handler for kill focus events from the focusedChild and set focusedChild to NULL. Save the newly focused window (evt.GetWindow()) in member focusedWindow.
- On EVT_IDLE check focusedChild. If it's NULL then a child lost focus and no other child received it immediately so queue a EVT_KILL_FOCUS with the window focusedWindow for the RecordEditor. It will be caught by the DataViewCtrl's event handler and finish editing. Additionally, to avoid sending the event multiple times check that focusedWindow != NULL and set it to NULL after sending the event.
It works really well and I've wrote a class which can be easily reused. Hopefully it will help someone skip the hours I've wasted on this.
Code: Select all
// Controls inheriting from this class will receive EVT_KILL_FOCUS only when a window outside of them receives focus.
// In other words children don't steal focus from this window.
// Useful for implementing complex editors for wxDataViewCustomRenderer and wxGridCellEditor which listen for EVT_KILL_FOCUS to stop editing.
// T should be an wxWindow or a subclass of it.
template<class T>
class FocusKillGuarded : public T
{
public:
FocusKillGuarded() : focusedChild(NULL), focusedWindow(NULL) {
killFocusHandler = new wxEvtHandler();
killFocusHandler->Connect(wxEVT_KILL_FOCUS, (wxObjectEventFunction)&(FocusKillGuarded<T>::OnFocusKill), NULL, this);
T::Connect(wxEVT_CHILD_FOCUS, (wxObjectEventFunction)&(FocusKillGuarded<T>::OnChildFocus));
T::Connect(wxEVT_IDLE, (wxObjectEventFunction)&(FocusKillGuarded<T>::OnIdle));
}
virtual ~FocusKillGuarded() {
if (focusedChild != NULL) {
focusedChild->RemoveEventHandler(killFocusHandler);
focusedChild = NULL;
}
if (killFocusHandler != NULL) {
delete killFocusHandler;
killFocusHandler = NULL;
}
}
void OnChildFocus(wxChildFocusEvent &evt) {
focusedChild = T::FindFocus();
focusedChild->PushEventHandler(killFocusHandler);
evt.Skip();
}
void OnFocusKill(wxFocusEvent &evt) {
focusedChild->RemoveEventHandler(killFocusHandler);
focusedChild = NULL;
focusedWindow = evt.GetWindow();
evt.Skip();
}
void OnIdle(wxIdleEvent &evt) {
if (focusedChild == NULL && focusedWindow != NULL) {
wxFocusEvent *fevt = new wxFocusEvent(wxEVT_KILL_FOCUS);
fevt->SetWindow(focusedWindow);
T::GetEventHandler()->QueueEvent(fevt);
// avoid queueing the event again
focusedWindow = NULL;
}
evt.Skip();
}
private:
wxEvtHandler *killFocusHandler;
wxWindow *focusedChild;
wxWindow *focusedWindow;
};
Simply inherit from
FocusKillGuarded<baseClass> and call
Create in the constructor to initialize the base class.
Sample:
Code: Select all
class RecordEditor : public FocusKillGuarded<wxControl> {
public:
RecordEditor(wxWindow *parent, wxRect rect /* ... */) {
Create(parent, wxID_ANY, rect.GetTopLeft(), rect.GetSize(), wxBORDER_NONE);
//...
}
}