/////////////////////////////////////////////////////////////////////////////// // Name: tests/events/propagation.cpp // Purpose: Test events propagation // Author: Vadim Zeitlin // Created: 2009-01-16 // Copyright: (c) 2009 Vadim Zeitlin /////////////////////////////////////////////////////////////////////////////// // ---------------------------------------------------------------------------- // headers // ---------------------------------------------------------------------------- #include "testprec.h" #ifdef __BORLANDC__ #pragma hdrstop #endif #ifndef WX_PRECOMP #include "wx/app.h" #include "wx/event.h" #include "wx/scrolwin.h" #include "wx/window.h" #endif // WX_PRECOMP #include "wx/docmdi.h" #include "wx/frame.h" #include "wx/menu.h" #include "wx/scopedptr.h" #include "wx/scopeguard.h" #include "wx/toolbar.h" #include "wx/uiaction.h" // FIXME: Currently under OS X testing paint event doesn't work because neither // calling Refresh()+Update() nor even sending wxPaintEvent directly to // the window doesn't result in calls to its event handlers, so disable // some tests there. But this should be fixed and the tests reenabled // because wxPaintEvent propagation in wxScrolledWindow is a perfect // example of fragile code that could be broken under OS X. #ifndef __WXOSX__ #define CAN_TEST_PAINT_EVENTS #endif namespace { // this string will record the execution of all handlers wxString g_str; // a custom event wxDEFINE_EVENT(TEST_EVT, wxCommandEvent); // a custom event handler tracing the propagation of the events of the // specified types template class TestEvtHandlerBase : public wxEvtHandler { public: TestEvtHandlerBase(wxEventType evtType, char tag) : m_evtType(evtType), m_tag(tag) { Connect(evtType, static_cast(&TestEvtHandlerBase::OnTest)); } // override ProcessEvent() to confirm that it is called for all event // handlers in the chain virtual bool ProcessEvent(wxEvent& event) { if ( event.GetEventType() == m_evtType ) g_str += 'o'; // "o" == "overridden" return wxEvtHandler::ProcessEvent(event); } private: void OnTest(wxEvent& event) { g_str += m_tag; event.Skip(); } const wxEventType m_evtType; const char m_tag; wxDECLARE_NO_COPY_TEMPLATE_CLASS(TestEvtHandlerBase, Event); }; struct TestEvtHandler : TestEvtHandlerBase { TestEvtHandler(char tag) : TestEvtHandlerBase(TEST_EVT, tag) { } }; struct TestMenuEvtHandler : TestEvtHandlerBase { TestMenuEvtHandler(char tag) : TestEvtHandlerBase(wxEVT_MENU, tag) { } }; struct TestPaintEvtHandler : TestEvtHandlerBase { TestPaintEvtHandler(char tag) : TestEvtHandlerBase(wxEVT_PAINT, tag) { } }; // Another custom event handler, suitable for use with Connect(). struct TestEvtSink : wxEvtHandler { TestEvtSink(char tag) : m_tag(tag) { } void Handle(wxEvent& event) { g_str += m_tag; event.Skip(); } const char m_tag; wxDECLARE_NO_COPY_CLASS(TestEvtSink); }; // a window handling the test event class TestWindow : public wxWindow { public: TestWindow(wxWindow *parent, char tag) : wxWindow(parent, wxID_ANY), m_tag(tag) { Connect(TEST_EVT, wxCommandEventHandler(TestWindow::OnTest)); } private: void OnTest(wxCommandEvent& event) { g_str += m_tag; event.Skip(); } const char m_tag; DECLARE_NO_COPY_CLASS(TestWindow) }; // a scroll window handling paint event: we want to have a special test case // for this because the event propagation is complicated even further than // usual here by the presence of wxScrollHelperEvtHandler in the event handlers // chain and the fact that OnDraw() virtual method must be called if EVT_PAINT // is not handled class TestScrollWindow : public wxScrolledWindow { public: TestScrollWindow(wxWindow *parent) : wxScrolledWindow(parent, wxID_ANY) { Connect(wxEVT_PAINT, wxPaintEventHandler(TestScrollWindow::OnPaint)); } void GeneratePaintEvent() { #ifdef __WXGTK__ // We need to map the window, otherwise we're not going to get any // paint events for it. wxYield(); // Ignore events generated during the initial mapping. g_str.clear(); #endif // __WXGTK__ Refresh(); Update(); } virtual void OnDraw(wxDC& WXUNUSED(dc)) { g_str += 'D'; // draw } private: void OnPaint(wxPaintEvent& event) { g_str += 'P'; // paint event.Skip(); } wxDECLARE_NO_COPY_CLASS(TestScrollWindow); }; int DoFilterEvent(wxEvent& event) { if ( event.GetEventType() == TEST_EVT || event.GetEventType() == wxEVT_MENU ) g_str += 'a'; return -1; } bool DoProcessEvent(wxEvent& event) { if ( event.GetEventType() == TEST_EVT || event.GetEventType() == wxEVT_MENU ) g_str += 'A'; return false; } } // anonymous namespace // -------------------------------------------------------------------------- // test class // -------------------------------------------------------------------------- class EventPropagationTestCase : public CppUnit::TestCase { public: EventPropagationTestCase() {} virtual void setUp(); virtual void tearDown(); private: CPPUNIT_TEST_SUITE( EventPropagationTestCase ); CPPUNIT_TEST( OneHandler ); CPPUNIT_TEST( TwoHandlers ); CPPUNIT_TEST( WindowWithoutHandler ); CPPUNIT_TEST( WindowWithHandler ); CPPUNIT_TEST( ForwardEvent ); CPPUNIT_TEST( ScrollWindowWithoutHandler ); CPPUNIT_TEST( ScrollWindowWithHandler ); CPPUNIT_TEST( MenuEvent ); CPPUNIT_TEST( DocView ); // FIXME: this test will cause test_gui segmentation fault. Disable it for now. #if !defined(__WXUNIVERSAL__) && !defined (__WXX11__) WXUISIM_TEST( ContextMenuEvent ); #endif CPPUNIT_TEST_SUITE_END(); void OneHandler(); void TwoHandlers(); void WindowWithoutHandler(); void WindowWithHandler(); void ForwardEvent(); void ScrollWindowWithoutHandler(); void ScrollWindowWithHandler(); void MenuEvent(); void DocView(); void ContextMenuEvent(); DECLARE_NO_COPY_CLASS(EventPropagationTestCase) }; // register in the unnamed registry so that these tests are run by default CPPUNIT_TEST_SUITE_REGISTRATION( EventPropagationTestCase ); // also include in its own registry so that these tests can be run alone CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( EventPropagationTestCase, "EventPropagationTestCase" ); void EventPropagationTestCase::setUp() { SetFilterEventFunc(DoFilterEvent); SetProcessEventFunc(DoProcessEvent); g_str.clear(); } void EventPropagationTestCase::tearDown() { SetFilterEventFunc(NULL); SetProcessEventFunc(NULL); } void EventPropagationTestCase::OneHandler() { wxCommandEvent event(TEST_EVT); TestEvtHandler h1('1'); h1.ProcessEvent(event); CPPUNIT_ASSERT_EQUAL( "oa1A", g_str ); } void EventPropagationTestCase::TwoHandlers() { wxCommandEvent event(TEST_EVT); TestEvtHandler h1('1'); TestEvtHandler h2('2'); h1.SetNextHandler(&h2); h2.SetPreviousHandler(&h1); h1.ProcessEvent(event); CPPUNIT_ASSERT_EQUAL( "oa1o2A", g_str ); } void EventPropagationTestCase::WindowWithoutHandler() { wxCommandEvent event(TEST_EVT); TestWindow * const parent = new TestWindow(wxTheApp->GetTopWindow(), 'p'); wxON_BLOCK_EXIT_OBJ0( *parent, wxWindow::Destroy ); TestWindow * const child = new TestWindow(parent, 'c'); child->GetEventHandler()->ProcessEvent(event); CPPUNIT_ASSERT_EQUAL( "acpA", g_str ); } void EventPropagationTestCase::WindowWithHandler() { wxCommandEvent event(TEST_EVT); TestWindow * const parent = new TestWindow(wxTheApp->GetTopWindow(), 'p'); wxON_BLOCK_EXIT_OBJ0( *parent, wxWindow::Destroy ); TestWindow * const child = new TestWindow(parent, 'c'); TestEvtHandler h1('1'); child->PushEventHandler(&h1); wxON_BLOCK_EXIT_OBJ1( *child, wxWindow::PopEventHandler, false ); TestEvtHandler h2('2'); child->PushEventHandler(&h2); wxON_BLOCK_EXIT_OBJ1( *child, wxWindow::PopEventHandler, false ); child->HandleWindowEvent(event); CPPUNIT_ASSERT_EQUAL( "oa2o1cpA", g_str ); } void EventPropagationTestCase::ForwardEvent() { // The idea of this test is to check that the events explicitly forwarded // to another event handler still get pre/post-processed as usual as this // used to be broken by the fixes trying to avoid duplicate processing. TestWindow * const win = new TestWindow(wxTheApp->GetTopWindow(), 'w'); wxON_BLOCK_EXIT_OBJ0( *win, wxWindow::Destroy ); TestEvtHandler h1('1'); win->PushEventHandler(&h1); wxON_BLOCK_EXIT_OBJ1( *win, wxWindow::PopEventHandler, false ); class ForwardEvtHandler : public wxEvtHandler { public: ForwardEvtHandler(wxEvtHandler& h) : m_h(&h) { } virtual bool ProcessEvent(wxEvent& event) { g_str += 'f'; return m_h->ProcessEvent(event); } private: wxEvtHandler *m_h; } f(h1); // First send the event directly to f. wxCommandEvent event1(TEST_EVT); f.ProcessEvent(event1); CPPUNIT_ASSERT_EQUAL( "foa1wA", g_str ); g_str.clear(); // And then also test sending it to f indirectly. wxCommandEvent event2(TEST_EVT); TestEvtHandler h2('2'); h2.SetNextHandler(&f); h2.ProcessEvent(event2); CPPUNIT_ASSERT_EQUAL( "oa2fo1wAA", g_str ); } void EventPropagationTestCase::ScrollWindowWithoutHandler() { TestWindow * const parent = new TestWindow(wxTheApp->GetTopWindow(), 'p'); wxON_BLOCK_EXIT_OBJ0( *parent, wxWindow::Destroy ); TestScrollWindow * const win = new TestScrollWindow(parent); #ifdef CAN_TEST_PAINT_EVENTS win->GeneratePaintEvent(); CPPUNIT_ASSERT_EQUAL( "PD", g_str ); #endif g_str.clear(); wxCommandEvent eventCmd(TEST_EVT); win->HandleWindowEvent(eventCmd); CPPUNIT_ASSERT_EQUAL( "apA", g_str ); } void EventPropagationTestCase::ScrollWindowWithHandler() { TestWindow * const parent = new TestWindow(wxTheApp->GetTopWindow(), 'p'); wxON_BLOCK_EXIT_OBJ0( *parent, wxWindow::Destroy ); TestScrollWindow * const win = new TestScrollWindow(parent); #ifdef CAN_TEST_PAINT_EVENTS TestPaintEvtHandler h('h'); win->PushEventHandler(&h); wxON_BLOCK_EXIT_OBJ1( *win, wxWindow::PopEventHandler, false ); win->GeneratePaintEvent(); CPPUNIT_ASSERT_EQUAL( "ohPD", g_str ); #endif g_str.clear(); wxCommandEvent eventCmd(TEST_EVT); win->HandleWindowEvent(eventCmd); CPPUNIT_ASSERT_EQUAL( "apA", g_str ); } // Create a menu bar with a single menu containing wxID_APPLY menu item and // attach it to the specified frame. wxMenu* CreateTestMenu(wxFrame* frame) { wxMenu* const menu = new wxMenu; menu->Append(wxID_APPLY); wxMenuBar* const mb = new wxMenuBar; mb->Append(menu, "&Menu"); frame->SetMenuBar(mb); return menu; } // Helper for checking that the menu event processing resulted in the expected // output from the handlers. // // Notice that this is supposed to be used with ASSERT_MENU_EVENT_RESULT() // macro to make the file name and line number of the caller appear in the // failure messages. void CheckMenuEvent(wxMenu* menu, const char* result, CppUnit::SourceLine sourceLine) { g_str.clear(); // Trigger the menu event: this is more reliable than using // wxUIActionSimulator and currently works in all ports as they all call // wxMenuBase::SendEvent() from their respective menu event handlers. menu->SendEvent(wxID_APPLY); CPPUNIT_NS::assertEquals( result, g_str, sourceLine, "" ); } #define ASSERT_MENU_EVENT_RESULT(menu, result) \ CheckMenuEvent((menu), (result), CPPUNIT_SOURCELINE()) void EventPropagationTestCase::MenuEvent() { wxFrame* const frame = static_cast(wxTheApp->GetTopWindow()); // Create a minimal menu bar. wxMenu* const menu = CreateTestMenu(frame); wxMenuBar* const mb = menu->GetMenuBar(); wxScopedPtr ensureMenuBarDestruction(mb); wxON_BLOCK_EXIT_OBJ1( *frame, wxFrame::SetMenuBar, (wxMenuBar*)NULL ); // Check that wxApp gets the event exactly once. ASSERT_MENU_EVENT_RESULT( menu, "aA" ); // Check that the menu event handler is called. TestMenuEvtHandler hm('m'); // 'm' for "menu" menu->SetNextHandler(&hm); wxON_BLOCK_EXIT_OBJ1( *menu, wxEvtHandler::SetNextHandler, (wxEvtHandler*)NULL ); ASSERT_MENU_EVENT_RESULT( menu, "aomA" ); // Test that the event handler associated with the menu bar gets the event. TestMenuEvtHandler hb('b'); // 'b' for "menu Bar" mb->PushEventHandler(&hb); wxON_BLOCK_EXIT_OBJ1( *mb, wxWindow::PopEventHandler, false ); ASSERT_MENU_EVENT_RESULT( menu, "aomobA" ); // Also test that the window to which the menu belongs gets the event. TestMenuEvtHandler hw('w'); // 'w' for "Window" frame->PushEventHandler(&hw); wxON_BLOCK_EXIT_OBJ1( *frame, wxWindow::PopEventHandler, false ); ASSERT_MENU_EVENT_RESULT( menu, "aomobowA" ); } // Minimal viable implementations of wxDocument and wxView. class EventTestDocument : public wxDocument { public: EventTestDocument() { } wxDECLARE_DYNAMIC_CLASS(EventTestDocument); }; class EventTestView : public wxView { public: EventTestView() { } virtual void OnDraw(wxDC*) { } wxDECLARE_DYNAMIC_CLASS(EventTestView); }; wxIMPLEMENT_DYNAMIC_CLASS(EventTestDocument, wxDocument); wxIMPLEMENT_DYNAMIC_CLASS(EventTestView, wxView); void EventPropagationTestCase::DocView() { // Set up the parent frame and its menu bar. wxDocManager docManager; wxScopedPtr parent(new wxDocMDIParentFrame(&docManager, NULL, wxID_ANY, "Parent")); wxMenu* const menu = CreateTestMenu(parent.get()); // Set up the event handlers. TestEvtSink sinkDM('m'); docManager.Connect(wxEVT_MENU, wxEventHandler(TestEvtSink::Handle), NULL, &sinkDM); TestEvtSink sinkParent('p'); parent->Connect(wxEVT_MENU, wxEventHandler(TestEvtSink::Handle), NULL, &sinkParent); // Check that wxDocManager and wxFrame get the event in order. ASSERT_MENU_EVENT_RESULT( menu, "ampA" ); // Now check what happens if we have an active document. wxDocTemplate docTemplate(&docManager, "Test", "", "", "", "Test Document", "Test View", wxCLASSINFO(EventTestDocument), wxCLASSINFO(EventTestView)); wxDocument* const doc = docTemplate.CreateDocument(""); wxView* const view = doc->GetFirstView(); wxScopedPtr child(new wxDocMDIChildFrame(doc, view, parent.get(), wxID_ANY, "Child")); wxMenu* const menuChild = CreateTestMenu(child.get()); // Ensure that the child that we've just created is the active one. child->Activate(); #ifdef __WXGTK__ // There are a lot of hacks related to child frame menu bar handling in // wxGTK and, in particular, the code in src/gtk/mdi.cpp relies on getting // idle events to really put everything in place. Moreover, as wxGTK uses // GtkNotebook as its MDI pages container, the frame must be shown for all // this to work as gtk_notebook_set_current_page() doesn't do anything if // called for a hidden window (this incredible fact cost me quite some time // to find empirically -- only to notice its confirmation in GTK+ // documentation immediately afterwards). So just do whatever it takes to // make things work "as usual". child->Show(); parent->Show(); wxYield(); #endif // __WXGTK__ TestEvtSink sinkDoc('d'); doc->Connect(wxEVT_MENU, wxEventHandler(TestEvtSink::Handle), NULL, &sinkDoc); TestEvtSink sinkView('v'); view->Connect(wxEVT_MENU, wxEventHandler(TestEvtSink::Handle), NULL, &sinkView); TestEvtSink sinkChild('c'); child->Connect(wxEVT_MENU, wxEventHandler(TestEvtSink::Handle), NULL, &sinkChild); // Check that wxDocument, wxView, wxDocManager, child frame and the parent // get the event in order. ASSERT_MENU_EVENT_RESULT( menuChild, "advmcpA" ); #if wxUSE_TOOLBAR // Also check that toolbar events get forwarded to the active child. wxToolBar* const tb = parent->CreateToolBar(wxTB_NOICONS); tb->AddTool(wxID_APPLY, "Apply", wxNullBitmap); tb->Realize(); // As in CheckMenuEvent(), use toolbar method actually sending the event // instead of bothering with wxUIActionSimulator which would have been // trickier. g_str.clear(); tb->OnLeftClick(wxID_APPLY, true /* doesn't matter */); CPPUNIT_ASSERT_EQUAL( "advmcpA", g_str ); #endif // wxUSE_TOOLBAR } #if wxUSE_UIACTIONSIMULATOR class ContextMenuTestWindow : public wxWindow { public: ContextMenuTestWindow(wxWindow *parent, char tag) : wxWindow(parent, wxID_ANY), m_tag(tag) { Connect(wxEVT_CONTEXT_MENU, wxContextMenuEventHandler(ContextMenuTestWindow::OnMenu)); } private: void OnMenu(wxContextMenuEvent& event) { g_str += m_tag; event.Skip(); } const char m_tag; wxDECLARE_NO_COPY_CLASS(ContextMenuTestWindow); }; void EventPropagationTestCase::ContextMenuEvent() { ContextMenuTestWindow * const parent = new ContextMenuTestWindow(wxTheApp->GetTopWindow(), 'p'); wxON_BLOCK_EXIT_OBJ0( *parent, wxWindow::Destroy ); ContextMenuTestWindow * const child = new ContextMenuTestWindow(parent, 'c'); parent->SetSize(100, 100); child->SetSize(0, 0, 50, 50); child->SetFocus(); wxUIActionSimulator sim; const wxPoint origin = parent->ClientToScreen(wxPoint(0, 0)); // Right clicking in the child should generate an event for it and the // parent. g_str.clear(); sim.MouseMove(origin + wxPoint(10, 10)); sim.MouseClick(wxMOUSE_BTN_RIGHT); // At least with MSW, for WM_CONTEXTMENU to be synthesized by the system // from the right mouse click event, we must dispatch the mouse messages. wxYield(); CPPUNIT_ASSERT_EQUAL( "cp", g_str ); // Right clicking outside the child should generate the event just in the // parent. g_str.clear(); sim.MouseMove(origin + wxPoint(60, 60)); sim.MouseClick(wxMOUSE_BTN_RIGHT); wxYield(); CPPUNIT_ASSERT_EQUAL( "p", g_str ); } #endif // wxUSE_UIACTIONSIMULATOR