1 ///////////////////////////////////////////////////////////////////////////////
2 // Name: src/common/popupcmn.cpp
3 // Purpose: implementation of wxPopupTransientWindow
4 // Author: Vadim Zeitlin
5 // Modified by:
6 // Created: 06.01.01
7 // Copyright: (c) 2001 Vadim Zeitlin <zeitlin@dptmaths.ens-cachan.fr>
8 // Licence: wxWindows licence
9 ///////////////////////////////////////////////////////////////////////////////
10
11 // ============================================================================
12 // declarations
13 // ============================================================================
14
15 // ----------------------------------------------------------------------------
16 // headers
17 // ----------------------------------------------------------------------------
18
19 // For compilers that support precompilation, includes "wx.h".
20 #include "wx/wxprec.h"
21
22
23 #if wxUSE_POPUPWIN
24
25 #include "wx/popupwin.h"
26
27 #ifndef WX_PRECOMP
28 #include "wx/combobox.h" // wxComboCtrl
29 #include "wx/app.h" // wxPostEvent
30 #include "wx/log.h"
31 #endif //WX_PRECOMP
32
33 #include "wx/display.h"
34 #include "wx/recguard.h"
35
36 #ifdef __WXUNIVERSAL__
37 #include "wx/univ/renderer.h"
38 #include "wx/scrolbar.h"
39 #endif // __WXUNIVERSAL__
40
41 #ifdef __WXGTK20__
42 #include "wx/gtk/private/wrapgtk.h"
43 #elif defined(__WXGTK__)
44 #include <gtk/gtk.h>
45 #define gtk_widget_get_window(x) x->window
46 #elif defined(__WXX11__)
47 #include "wx/x11/private.h"
48 #endif
49
50 wxIMPLEMENT_DYNAMIC_CLASS(wxPopupWindow, wxWindow);
51 wxIMPLEMENT_DYNAMIC_CLASS(wxPopupTransientWindow, wxPopupWindow);
52
53 #if wxUSE_COMBOBOX && defined(__WXUNIVERSAL__)
54 wxIMPLEMENT_DYNAMIC_CLASS(wxPopupComboWindow, wxPopupTransientWindow);
55 #endif
56
57 #ifndef __WXMSW__
58
59 // ----------------------------------------------------------------------------
60 // private classes
61 // ----------------------------------------------------------------------------
62
63 // event handlers which we use to intercept events which cause the popup to
64 // disappear
65 class wxPopupWindowHandler : public wxEvtHandler
66 {
67 public:
wxPopupWindowHandler(wxPopupTransientWindow * popup)68 wxPopupWindowHandler(wxPopupTransientWindow *popup) : m_popup(popup) {}
69
70 protected:
71 // event handlers
72 void OnLeftDown(wxMouseEvent& event);
73 void OnCaptureLost(wxMouseCaptureLostEvent& event);
74
75 private:
76 wxPopupTransientWindow *m_popup;
77
78 wxDECLARE_EVENT_TABLE();
79 wxDECLARE_NO_COPY_CLASS(wxPopupWindowHandler);
80 };
81
82 class wxPopupFocusHandler : public wxEvtHandler
83 {
84 public:
wxPopupFocusHandler(wxPopupTransientWindow * popup)85 wxPopupFocusHandler(wxPopupTransientWindow *popup) : m_popup(popup) {}
86
87 protected:
88 void OnKillFocus(wxFocusEvent& event);
89 void OnChar(wxKeyEvent& event);
90
91 private:
92 wxPopupTransientWindow *m_popup;
93
94 wxDECLARE_EVENT_TABLE();
95 wxDECLARE_NO_COPY_CLASS(wxPopupFocusHandler);
96 };
97
98 // ----------------------------------------------------------------------------
99 // event tables
100 // ----------------------------------------------------------------------------
101
wxBEGIN_EVENT_TABLE(wxPopupWindowHandler,wxEvtHandler)102 wxBEGIN_EVENT_TABLE(wxPopupWindowHandler, wxEvtHandler)
103 EVT_LEFT_DOWN(wxPopupWindowHandler::OnLeftDown)
104 EVT_MOUSE_CAPTURE_LOST(wxPopupWindowHandler::OnCaptureLost)
105 wxEND_EVENT_TABLE()
106
107 wxBEGIN_EVENT_TABLE(wxPopupFocusHandler, wxEvtHandler)
108 EVT_KILL_FOCUS(wxPopupFocusHandler::OnKillFocus)
109 EVT_CHAR(wxPopupFocusHandler::OnChar)
110 wxEND_EVENT_TABLE()
111
112 wxBEGIN_EVENT_TABLE(wxPopupTransientWindow, wxPopupWindow)
113 #if defined(__WXMAC__) && wxOSX_USE_COCOA_OR_CARBON
114 EVT_IDLE(wxPopupTransientWindow::OnIdle)
115 #endif
116 wxEND_EVENT_TABLE()
117
118 #endif // !__WXMSW__
119
120 // ============================================================================
121 // implementation
122 // ============================================================================
123
124 // ----------------------------------------------------------------------------
125 // wxPopupWindowBase
126 // ----------------------------------------------------------------------------
127
128 wxPopupWindowBase::~wxPopupWindowBase()
129 {
130 // this destructor is required for Darwin
131 }
132
Create(wxWindow * WXUNUSED (parent),int WXUNUSED (flags))133 bool wxPopupWindowBase::Create(wxWindow* WXUNUSED(parent), int WXUNUSED(flags))
134 {
135 // By default, block event propagation at this window as it usually
136 // doesn't make sense. This notably prevents wxScrolledWindow from trying
137 // to scroll popup contents into view if a popup is shown from it but
138 // extends beyond its window boundaries.
139 SetExtraStyle(GetExtraStyle() | wxWS_EX_BLOCK_EVENTS);
140
141 return true;
142 }
143
Position(const wxPoint & ptOrigin,const wxSize & size)144 void wxPopupWindowBase::Position(const wxPoint& ptOrigin,
145 const wxSize& size)
146 {
147 // determine the position and size of the screen we clamp the popup to
148 wxPoint posScreen;
149 wxSize sizeScreen;
150
151 const int displayNum = wxDisplay::GetFromPoint(ptOrigin);
152 if ( displayNum != wxNOT_FOUND )
153 {
154 const wxRect rectScreen = wxDisplay(displayNum).GetGeometry();
155 posScreen = rectScreen.GetPosition();
156 sizeScreen = rectScreen.GetSize();
157 }
158 else // outside of any display?
159 {
160 // just use the primary one then
161 posScreen = wxPoint(0, 0);
162 sizeScreen = wxGetDisplaySize();
163 }
164
165
166 const wxSize sizeSelf = GetSize();
167
168 // is there enough space to put the popup below the window (where we put it
169 // by default)?
170 wxCoord y = ptOrigin.y + size.y;
171 if ( y + sizeSelf.y > posScreen.y + sizeScreen.y )
172 {
173 // check if there is enough space above
174 if ( ptOrigin.y > sizeSelf.y )
175 {
176 // do position the control above the window
177 y -= size.y + sizeSelf.y;
178 }
179 //else: not enough space below nor above, leave below
180 }
181
182 // now check left/right too
183 wxCoord x = ptOrigin.x;
184
185 if ( wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft )
186 {
187 // shift the window to the left instead of the right.
188 x -= size.x;
189 x -= sizeSelf.x; // also shift it by window width.
190 }
191 else
192 x += size.x;
193
194
195 if ( x + sizeSelf.x > posScreen.x + sizeScreen.x )
196 {
197 // check if there is enough space to the left
198 if ( ptOrigin.x > sizeSelf.x )
199 {
200 // do position the control to the left
201 x -= size.x + sizeSelf.x;
202 }
203 //else: not enough space there neither, leave in default position
204 }
205
206 Move(x, y, wxSIZE_NO_ADJUSTMENTS);
207 }
208
209 // ----------------------------------------------------------------------------
210 // wxPopupTransientWindowBase
211 // ----------------------------------------------------------------------------
212
Destroy()213 bool wxPopupTransientWindowBase::Destroy()
214 {
215 // The popup window can be deleted at any moment, even while some events
216 // are still being processed for it, so delay its real destruction until
217 // the next idle time when we're sure that it's safe to really destroy it.
218
219 wxCHECK_MSG( !wxPendingDelete.Member(this), false,
220 wxS("Shouldn't destroy the popup twice.") );
221
222 wxPendingDelete.Append(this);
223
224 return true;
225 }
226
227 // MSW implementation is in platform-specific src/msw/popupwin.cpp.
228 #ifndef __WXMSW__
229
230 // ----------------------------------------------------------------------------
231 // wxPopupTransientWindow
232 // ----------------------------------------------------------------------------
233
Init()234 void wxPopupTransientWindow::Init()
235 {
236 m_child =
237 m_focus = NULL;
238
239 m_handlerFocus = NULL;
240 m_handlerPopup = NULL;
241 }
242
wxPopupTransientWindow(wxWindow * parent,int style)243 wxPopupTransientWindow::wxPopupTransientWindow(wxWindow *parent, int style)
244 {
245 Init();
246
247 (void)Create(parent, style);
248 }
249
~wxPopupTransientWindow()250 wxPopupTransientWindow::~wxPopupTransientWindow()
251 {
252 if (m_handlerPopup && m_handlerPopup->GetNextHandler())
253 PopHandlers();
254
255 wxASSERT(!m_handlerFocus || !m_handlerFocus->GetNextHandler());
256 wxASSERT(!m_handlerPopup || !m_handlerPopup->GetNextHandler());
257
258 delete m_handlerFocus;
259 delete m_handlerPopup;
260 }
261
PopHandlers()262 void wxPopupTransientWindow::PopHandlers()
263 {
264 if ( m_child )
265 {
266 if ( !m_child->RemoveEventHandler(m_handlerPopup) )
267 {
268 // something is very wrong and someone else probably deleted our
269 // handler - so don't risk deleting it second time
270 m_handlerPopup = NULL;
271 }
272 if (m_child->HasCapture())
273 {
274 m_child->ReleaseMouse();
275 }
276 m_child = NULL;
277 }
278
279 if ( m_focus )
280 {
281 if ( !m_focus->RemoveEventHandler(m_handlerFocus) )
282 {
283 // see above
284 m_handlerFocus = NULL;
285 }
286 }
287 m_focus = NULL;
288 }
289
Popup(wxWindow * winFocus)290 void wxPopupTransientWindow::Popup(wxWindow *winFocus)
291 {
292 // If we have a single child, we suppose that it must cover the entire
293 // popup window and hence we give the mouse capture to it instead of
294 // keeping it for ourselves.
295 //
296 // Notice that this works best for combobox-like popups which have a single
297 // control inside them and not so well for popups containing a single
298 // wxPanel with multiple children inside it but OTOH it does no harm in
299 // this case neither and we can't reliably distinguish between them.
300 const wxWindowList& children = GetChildren();
301 if ( children.GetCount() == 1 )
302 {
303 m_child = children.GetFirst()->GetData();
304 }
305 else
306 {
307 m_child = this;
308 }
309
310 Show();
311
312 // There is a problem if these are still in use
313 wxASSERT(!m_handlerFocus || !m_handlerFocus->GetNextHandler());
314 wxASSERT(!m_handlerPopup || !m_handlerPopup->GetNextHandler());
315
316 if (!m_handlerPopup)
317 m_handlerPopup = new wxPopupWindowHandler(this);
318
319 m_child->PushEventHandler(m_handlerPopup);
320
321 m_focus = winFocus ? winFocus : this;
322 m_focus->SetFocus();
323
324 #if defined( __WXMAC__) && wxOSX_USE_COCOA_OR_CARBON
325 // MSW doesn't allow to set focus to the popup window, but we need to
326 // subclass the window which has the focus, and not winFocus passed in or
327 // otherwise everything else breaks down
328 m_focus = FindFocus();
329 #elif defined(__WXGTK__)
330 // GTK+ catches the activate events from the popup
331 // window, not the focus events from the child window
332 m_focus = this;
333 #endif
334
335 if ( m_focus )
336 {
337 if (!m_handlerFocus)
338 m_handlerFocus = new wxPopupFocusHandler(this);
339
340 m_focus->PushEventHandler(m_handlerFocus);
341 }
342 }
343
Show(bool show)344 bool wxPopupTransientWindow::Show( bool show )
345 {
346 #ifdef __WXGTK__
347 if (!show)
348 {
349 #ifdef __WXGTK3__
350 GdkDisplay* display = gtk_widget_get_display(m_widget);
351 #ifdef __WXGTK4__
352 gdk_seat_ungrab(gdk_display_get_default_seat(display));
353 #else
354 wxGCC_WARNING_SUPPRESS(deprecated-declarations)
355 GdkDeviceManager* manager = gdk_display_get_device_manager(display);
356 GdkDevice* device = gdk_device_manager_get_client_pointer(manager);
357 gdk_device_ungrab(device, unsigned(GDK_CURRENT_TIME));
358 wxGCC_WARNING_RESTORE()
359 #endif
360 #else
361 gdk_pointer_ungrab( (guint32)GDK_CURRENT_TIME );
362 #endif
363
364 gtk_grab_remove( m_widget );
365 }
366 #endif
367
368 #ifdef __WXX11__
369 if (!show)
370 {
371 XUngrabPointer( wxGlobalDisplay(), CurrentTime );
372 }
373 #endif
374
375 #if defined( __WXMAC__)
376 if (!show && m_child && m_child->HasCapture())
377 {
378 m_child->ReleaseMouse();
379 }
380 #endif
381
382 bool ret = wxPopupWindow::Show( show );
383
384 #ifdef __WXGTK__
385 if (show)
386 {
387 gtk_grab_add( m_widget );
388
389 GdkWindow* window = gtk_widget_get_window(m_widget);
390 #ifdef __WXGTK4__
391 GdkDisplay* display = gdk_window_get_display(window);
392 GdkSeat* seat = gdk_display_get_default_seat(display);
393 gdk_seat_grab(seat, window, GDK_SEAT_CAPABILITY_POINTER, false, NULL, NULL, NULL, 0);
394 #else
395 const GdkEventMask mask = GdkEventMask(
396 GDK_BUTTON_PRESS_MASK |
397 GDK_BUTTON_RELEASE_MASK |
398 GDK_POINTER_MOTION_HINT_MASK |
399 GDK_POINTER_MOTION_MASK);
400 #ifdef __WXGTK3__
401 GdkDisplay* display = gdk_window_get_display(window);
402 wxGCC_WARNING_SUPPRESS(deprecated-declarations)
403 GdkDeviceManager* manager = gdk_display_get_device_manager(display);
404 GdkDevice* device = gdk_device_manager_get_client_pointer(manager);
405 gdk_device_grab(device, window,
406 GDK_OWNERSHIP_NONE, true, mask, NULL, unsigned(GDK_CURRENT_TIME));
407 wxGCC_WARNING_RESTORE()
408 #else
409 gdk_pointer_grab( window, true,
410 mask,
411 NULL,
412 NULL,
413 (guint32)GDK_CURRENT_TIME );
414 #endif
415 #endif // !__WXGTK4__
416 }
417 #endif
418
419 #ifdef __WXX11__
420 if (show)
421 {
422 Window xwindow = (Window) m_clientWindow;
423
424 /* int res =*/ XGrabPointer(wxGlobalDisplay(), xwindow,
425 True,
426 ButtonPressMask | ButtonReleaseMask | ButtonMotionMask | EnterWindowMask | LeaveWindowMask | PointerMotionMask,
427 GrabModeAsync,
428 GrabModeAsync,
429 None,
430 None,
431 CurrentTime );
432 }
433 #endif
434
435 #if defined( __WXMAC__)
436 if (show && m_child)
437 {
438 // Assume that the mouse is outside the popup to begin with
439 m_child->CaptureMouse();
440 }
441 #endif
442
443 return ret;
444 }
445
Dismiss()446 void wxPopupTransientWindow::Dismiss()
447 {
448 Hide();
449 PopHandlers();
450 }
451
452 #if defined(__WXMAC__) && wxOSX_USE_COCOA_OR_CARBON
OnIdle(wxIdleEvent & event)453 void wxPopupTransientWindow::OnIdle(wxIdleEvent& event)
454 {
455 event.Skip();
456
457 if (IsShown() && m_child)
458 {
459 // Store the last mouse position to minimize the number of calls to
460 // wxFindWindowAtPoint() which are quite expensive.
461 static wxPoint s_posLast;
462 const wxPoint pos = wxGetMousePosition();
463 if ( pos != s_posLast )
464 {
465 s_posLast = pos;
466
467 wxWindow* const winUnderMouse = wxFindWindowAtPoint(pos);
468
469 // We release the mouse capture while the mouse is inside the popup
470 // itself to allow using it normally with the controls inside it.
471 if ( wxGetTopLevelParent(winUnderMouse) == this )
472 {
473 if ( m_child->HasCapture() )
474 {
475 m_child->ReleaseMouse();
476 }
477 }
478 else // And we reacquire it as soon as the mouse goes outside.
479 {
480 if ( !m_child->HasCapture() )
481 {
482 m_child->CaptureMouse();
483 }
484 }
485 }
486 }
487 }
488 #endif // wxOSX/Carbon
489
490 #endif // !__WXMSW__
491
492 #if wxUSE_COMBOBOX && defined(__WXUNIVERSAL__)
493
494 // ----------------------------------------------------------------------------
495 // wxPopupComboWindow
496 // ----------------------------------------------------------------------------
497
wxBEGIN_EVENT_TABLE(wxPopupComboWindow,wxPopupTransientWindow)498 wxBEGIN_EVENT_TABLE(wxPopupComboWindow, wxPopupTransientWindow)
499 EVT_KEY_DOWN(wxPopupComboWindow::OnKeyDown)
500 wxEND_EVENT_TABLE()
501
502 wxPopupComboWindow::wxPopupComboWindow(wxComboCtrl *parent)
503 : wxPopupTransientWindow(parent)
504 {
505 m_combo = parent;
506 }
507
Create(wxComboCtrl * parent)508 bool wxPopupComboWindow::Create(wxComboCtrl *parent)
509 {
510 m_combo = parent;
511
512 return wxPopupWindow::Create(parent);
513 }
514
PositionNearCombo()515 void wxPopupComboWindow::PositionNearCombo()
516 {
517 // the origin point must be in screen coords
518 wxPoint ptOrigin = m_combo->ClientToScreen(wxPoint(0,0));
519
520 #if 0 //def __WXUNIVERSAL__
521 // account for the fact that (0, 0) is not the top left corner of the
522 // window: there is also the border
523 wxRect rectBorders = m_combo->GetRenderer()->
524 GetBorderDimensions(m_combo->GetBorder());
525 ptOrigin.x -= rectBorders.x;
526 ptOrigin.y -= rectBorders.y;
527 #endif // __WXUNIVERSAL__
528
529 // position below or above the combobox: the width is 0 to put it exactly
530 // below us, not to the left or to the right
531 Position(ptOrigin, wxSize(0, m_combo->GetSize().y));
532 }
533
OnDismiss()534 void wxPopupComboWindow::OnDismiss()
535 {
536 m_combo->OnPopupDismiss(true);
537 }
538
OnKeyDown(wxKeyEvent & event)539 void wxPopupComboWindow::OnKeyDown(wxKeyEvent& event)
540 {
541 m_combo->ProcessWindowEvent(event);
542 }
543
544 #endif // wxUSE_COMBOBOX && defined(__WXUNIVERSAL__)
545
546 #ifndef __WXMSW__
547
548 // ----------------------------------------------------------------------------
549 // wxPopupWindowHandler
550 // ----------------------------------------------------------------------------
551
OnLeftDown(wxMouseEvent & event)552 void wxPopupWindowHandler::OnLeftDown(wxMouseEvent& event)
553 {
554 // let the window have it first (we're the first event handler in the chain
555 // of handlers for this window)
556 if ( m_popup->ProcessLeftDown(event) )
557 {
558 return;
559 }
560
561 wxPoint pos = event.GetPosition();
562
563 // in non-Univ ports the system manages scrollbars for us
564 #if defined(__WXUNIVERSAL__) && wxUSE_SCROLLBAR
565 // scrollbar on which the click occurred
566 wxWindow *sbar = NULL;
567 #endif // __WXUNIVERSAL__ && wxUSE_SCROLLBAR
568
569 wxWindow *win = (wxWindow *)event.GetEventObject();
570
571 switch ( win->HitTest(pos.x, pos.y) )
572 {
573 case wxHT_WINDOW_OUTSIDE:
574 {
575 // do the coords translation now as after DismissAndNotify()
576 // m_popup may be destroyed
577 wxMouseEvent event2(event);
578
579 m_popup->ClientToScreen(&event2.m_x, &event2.m_y);
580
581 // clicking outside a popup dismisses it
582 m_popup->DismissAndNotify();
583
584 // dismissing a tooltip shouldn't waste a click, i.e. you
585 // should be able to dismiss it and press the button with the
586 // same click, so repost this event to the window beneath us
587 wxWindow *winUnder = wxFindWindowAtPoint(event2.GetPosition());
588 if ( winUnder )
589 {
590 // translate the event coords to the ones of the window
591 // which is going to get the event
592 winUnder->ScreenToClient(&event2.m_x, &event2.m_y);
593
594 event2.SetEventObject(winUnder);
595 wxPostEvent(winUnder->GetEventHandler(), event2);
596 }
597 }
598 break;
599
600 #if defined(__WXUNIVERSAL__) && wxUSE_SCROLLBAR
601 case wxHT_WINDOW_HORZ_SCROLLBAR:
602 sbar = win->GetScrollbar(wxHORIZONTAL);
603 break;
604
605 case wxHT_WINDOW_VERT_SCROLLBAR:
606 sbar = win->GetScrollbar(wxVERTICAL);
607 break;
608 #endif // __WXUNIVERSAL__ && wxUSE_SCROLLBAR
609
610 default:
611 // forgot to update the switch after adding a new hit test code?
612 wxFAIL_MSG( wxT("unexpected HitTest() return value") );
613 wxFALLTHROUGH;
614
615 case wxHT_WINDOW_CORNER:
616 // don't actually know if this one is good for anything, but let it
617 // pass just in case
618
619 case wxHT_WINDOW_INSIDE:
620 // let the normal processing take place
621 event.Skip();
622 break;
623 }
624
625 #if defined(__WXUNIVERSAL__) && wxUSE_SCROLLBAR
626 if ( sbar )
627 {
628 // translate the event coordinates to the scrollbar ones
629 pos = sbar->ScreenToClient(win->ClientToScreen(pos));
630
631 // and give the event to it
632 wxMouseEvent event2 = event;
633 event2.m_x = pos.x;
634 event2.m_y = pos.y;
635
636 (void)sbar->GetEventHandler()->ProcessEvent(event2);
637 }
638 #endif // __WXUNIVERSAL__ && wxUSE_SCROLLBAR
639 }
640
641 void
OnCaptureLost(wxMouseCaptureLostEvent & WXUNUSED (event))642 wxPopupWindowHandler::OnCaptureLost(wxMouseCaptureLostEvent& WXUNUSED(event))
643 {
644 m_popup->DismissAndNotify();
645
646 // There is no need to skip the event here, normally we've already dealt
647 // with the focus loss.
648 }
649
650 // ----------------------------------------------------------------------------
651 // wxPopupFocusHandler
652 // ----------------------------------------------------------------------------
653
OnKillFocus(wxFocusEvent & event)654 void wxPopupFocusHandler::OnKillFocus(wxFocusEvent& event)
655 {
656 // when we lose focus we always disappear - unless it goes to the popup (in
657 // which case we don't really lose it)
658 wxWindow *win = event.GetWindow();
659 while ( win )
660 {
661 if ( win == m_popup )
662 return;
663 win = win->GetParent();
664 }
665
666 m_popup->DismissAndNotify();
667 }
668
OnChar(wxKeyEvent & event)669 void wxPopupFocusHandler::OnChar(wxKeyEvent& event)
670 {
671 // we can be associated with the popup itself in which case we should avoid
672 // infinite recursion
673 static int s_inside;
674 wxRecursionGuard guard(s_inside);
675 if ( guard.IsInside() )
676 {
677 event.Skip();
678 return;
679 }
680
681 // let the window have it first, it might process the keys
682 if ( !m_popup->GetEventHandler()->ProcessEvent(event) )
683 {
684 // by default, dismiss the popup
685 m_popup->DismissAndNotify();
686 }
687 }
688
689 #endif // !__WXMSW__
690
691 #endif // wxUSE_POPUPWIN
692