wxDataViewCustomRenderer focus problem Topic is solved

Are you writing your own components and need help with how to set them up or have questions about the components you are deriving from ? Ask them here.
Post Reply
lazy_banana
In need of some credit
In need of some credit
Posts: 8
Joined: Sun Sep 09, 2012 11:28 am

wxDataViewCustomRenderer focus problem

Post 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.
Last edited by lazy_banana on Thu Jan 04, 2018 3:06 pm, edited 1 time in total.
User avatar
eranon
Can't get richer than this
Can't get richer than this
Posts: 867
Joined: Sun May 13, 2012 11:42 pm
Location: France
Contact:

Re: wxDataViewCustomRenderer focus problem

Post 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
[Ind. dev. - wxWidgets 3.0/3.1 under "Win 7 64-bit, TDM64-GCC" + "OS X 10.9, LLVM Clang"]
lazy_banana
In need of some credit
In need of some credit
Posts: 8
Joined: Sun Sep 09, 2012 11:28 am

Re: wxDataViewCustomRenderer focus problem

Post 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.
User avatar
eranon
Can't get richer than this
Can't get richer than this
Posts: 867
Joined: Sun May 13, 2012 11:42 pm
Location: France
Contact:

Re: wxDataViewCustomRenderer focus problem

Post 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?
[Ind. dev. - wxWidgets 3.0/3.1 under "Win 7 64-bit, TDM64-GCC" + "OS X 10.9, LLVM Clang"]
lazy_banana
In need of some credit
In need of some credit
Posts: 8
Joined: Sun Sep 09, 2012 11:28 am

Re: wxDataViewCustomRenderer focus problem

Post 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.
User avatar
eranon
Can't get richer than this
Can't get richer than this
Posts: 867
Joined: Sun May 13, 2012 11:42 pm
Location: France
Contact:

Re: wxDataViewCustomRenderer focus problem

Post 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).
[Ind. dev. - wxWidgets 3.0/3.1 under "Win 7 64-bit, TDM64-GCC" + "OS X 10.9, LLVM Clang"]
lazy_banana
In need of some credit
In need of some credit
Posts: 8
Joined: Sun Sep 09, 2012 11:28 am

Re: wxDataViewCustomRenderer focus problem

Post 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).
Last edited by lazy_banana on Thu Jan 04, 2018 3:08 pm, edited 1 time in total.
lazy_banana
In need of some credit
In need of some credit
Posts: 8
Joined: Sun Sep 09, 2012 11:28 am

Re: wxDataViewCustomRenderer focus problem

Post 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);
		
		//...
	}
}

Post Reply