Page 1 of 1

wxDataViewCustomRenderer focus problem

Posted: Sat Dec 30, 2017 6:04 pm
by lazy_banana
Hello,

I'm trying to write a custom data view renderer which contains a choice box and 3 other controls. This composite control is called RecordEditor. I tried implementing it by inheriting either from wxPanel or wxControl and I've ran against those issues:
  1. When inheriting from wxControl clicking on a child steals focus from the RecordEditor and the data view catches the EVT_KILL_FOCUS and stops the editing process.
  2. When inheriting from wxPanel the RecordEditor never receives focus (as it's expected for wxPanel subclasses) and it neither receives EVT_KILL_FOCUS. So I can double click on another row of the data view and have two RecordEditors open which is a failed state because the first one will never be closed and cleaned up properly. Closing the main window throws an error because the first RecordEditor has the custom event handler added by the data viewer.
So RecordEditor needs to be focusable but not have its focus stolen by its children.

Re: wxDataViewCustomRenderer focus problem

Posted: Sun Dec 31, 2017 6:13 pm
by eranon
Hello, Never dealt with the wxDataViewCtrl class, but if it's like wxGrid, maybe you should derive from wxDataViewRenderer or one of its existing specializations...

EDIT: Sounds to be confirmed here: https://wiki.wxwidgets.org/WxDataViewCtrl where we can read:
Each wxDataViewColumn in turn owns 1 instance of a wxDataViewRenderer to render its cells

Re: wxDataViewCustomRenderer focus problem

Posted: Sun Dec 31, 2017 7:50 pm
by lazy_banana
Yes, I have a class which inherits from wxDataViewCustomRenderer and it returns an instance of RecordEditor in the CreateEditorCtrl method. Everything is working as expected except this problem with focus.

Re: wxDataViewCustomRenderer focus problem

Posted: Mon Jan 01, 2018 1:32 am
by eranon
What I say is pure assumption since I never used wxDataViewCtrl, but why don't you derive from wxWindow, then veto the EVT_KILL_FOCUS (depending on a member flag which could be set at starting of editing) in an handler to prevent prematured end of editing?

Re: wxDataViewCustomRenderer focus problem

Posted: Tue Jan 02, 2018 7:20 pm
by lazy_banana
But when the user clicks outside of the RecordEditor it has to hide the editor to work properly. So EVT_KILL_FOCUS should be vetoed only when a child of RecordEditor is focused. I will try to monitor the focus events on RecordEditor and maybe I can come up with a way to do this but it feels like a hack.

Re: wxDataViewCustomRenderer focus problem

Posted: Tue Jan 02, 2018 8:52 pm
by eranon
lazy_banana wrote:But when the user clicks outside of the RecordEditor it has to hide the editor to work properly. So EVT_KILL_FOCUS should be vetoed only when a child of RecordEditor is focused. I will try to monitor the focus events on RecordEditor and maybe I can come up with a way to do this but it feels like a hack.
It's because of this I said "(depending on a member flag which could be set at starting of editing)". Your veto has to be conditionned to a flag and this flag has to be raised and lowered at appropiate times (eg. on EVT_DATAVIEW_ITEM_START_EDITING and EVT_DATAVIEW_ITEM_EDITING_DONE).

Re: wxDataViewCustomRenderer focus problem

Posted: Tue Jan 02, 2018 10:39 pm
by lazy_banana
EVT_DATAVIEW_ITEM_EDITING_DONE is a consequence of EVT_KILL_FOCUS or Return or Escape key pressed so I can't veto EVT_KILL_FOCUS based on that flag.

I have tried inheriting from the wxPopupTransientWindow as a base class and needed to add the following at the end of the constructor of RecordEditor:

Code: Select all

	// rect is the wxRect received by CreateEditorCtrl
	// panel is the wxPanel containing the controls inside RecordEditor
	wxRect s = wxRect(parent->ClientToScreen(rect.GetTopLeft()), parent->ClientToScreen(rect.GetBottomRight()));
	SetSize(s);
	panel->SetSize(s.GetSize());
	panel->Layout();
	Popup();
And override OnDismiss as:

Code: Select all

virtual void OnDismiss() override {
	// emulates a press of the Return key to finish editing
	GetEventHandler()->QueueEvent(new wxCommandEvent(wxEVT_TEXT_ENTER));
}
The problem now is that wxPopupTransientWindow offers very limited editing. For example the choice box doesn't work and text entries receive no input. I think that only buttons and spin controls are working (based on the popup window sample).

Re: wxDataViewCustomRenderer focus problem

Posted: Wed Jan 03, 2018 3:42 am
by lazy_banana
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);
		
		//...
	}
}