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