using terminate: terminates prematurely, OnExit is not called anymore
[wxWidgets.git] / src / osx / cocoa / evtloop.mm
1 ///////////////////////////////////////////////////////////////////////////////
2 // Name:        src/osx/cocoa/evtloop.mm
3 // Purpose:     implementation of wxEventLoop for OS X
4 // Author:      Vadim Zeitlin, Stefan Csomor
5 // Modified by:
6 // Created:     2006-01-12
7 // Copyright:   (c) 2006 Vadim Zeitlin <vadim@wxwindows.org>
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 #ifdef __BORLANDC__
23     #pragma hdrstop
24 #endif
25
26 #include "wx/evtloop.h"
27
28 #ifndef WX_PRECOMP
29     #include "wx/app.h"
30     #include "wx/nonownedwnd.h"
31 #endif // WX_PRECOMP
32
33 #include "wx/log.h"
34 #include "wx/scopeguard.h"
35
36 #include "wx/osx/private.h"
37
38 // ============================================================================
39 // wxEventLoop implementation
40 // ============================================================================
41
42 #if 0
43
44 // in case we want to integrate this
45
46 static NSUInteger CalculateNSEventMaskFromEventCategory(wxEventCategory cat)
47 {
48     // the masking system doesn't really help, as only the lowlevel UI events
49     // are split in a useful way, all others are way to broad
50         
51     if ( (cat | wxEVT_CATEGORY_USER_INPUT) && (cat | (~wxEVT_CATEGORY_USER_INPUT) ) )
52         return NSAnyEventMask;
53     
54     NSUInteger mask = 0;
55
56     if ( cat | wxEVT_CATEGORY_USER_INPUT )
57     {
58         mask |=
59             NSLeftMouseDownMask |
60             NSLeftMouseUpMask |
61             NSRightMouseDownMask |
62             NSRightMouseUpMask |
63             NSMouseMovedMask |
64             NSLeftMouseDraggedMask |
65             NSRightMouseDraggedMask |
66             NSMouseEnteredMask |
67             NSMouseExitedMask |
68             NSScrollWheelMask |
69 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4
70             NSTabletPointMask |
71             NSTabletProximityMask |
72 #endif
73             NSOtherMouseDownMask |
74             NSOtherMouseUpMask |
75             NSOtherMouseDraggedMask |
76
77             NSKeyDownMask |
78             NSKeyUpMask |
79             NSFlagsChangedMask |
80 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5
81             NSEventMaskGesture |
82             NSEventMaskMagnify |
83             NSEventMaskSwipe |
84             NSEventMaskRotate |
85             NSEventMaskBeginGesture |
86             NSEventMaskEndGesture |
87 #endif
88             0;
89     }
90     
91     if ( cat | (~wxEVT_CATEGORY_USER_INPUT) )
92     {
93         mask |= 
94             NSAppKitDefinedMask |
95             NSSystemDefinedMask |
96             NSApplicationDefinedMask |
97             NSPeriodicMask |
98             NSCursorUpdateMask;
99     }
100     
101     return mask;
102 }
103
104 #endif
105
106 wxGUIEventLoop::wxGUIEventLoop()
107 {
108     m_modalSession = nil;
109     m_dummyWindow = nil;
110     m_modalNestedLevel = 0;
111     m_modalWindow = NULL;
112     m_osxLowLevelWakeUp = false;
113 }
114
115 wxGUIEventLoop::~wxGUIEventLoop()
116 {
117     wxASSERT( m_modalSession == nil );
118     wxASSERT( m_dummyWindow == nil );
119     wxASSERT( m_modalNestedLevel == 0 );
120 }
121
122 //-----------------------------------------------------------------------------
123 // events dispatch and loop handling
124 //-----------------------------------------------------------------------------
125
126 #if 0
127
128 bool wxGUIEventLoop::Pending() const
129 {
130 #if 0
131     // this code doesn't reliably detect pending events
132     // so better return true and have the dispatch deal with it
133     // as otherwise we end up in a tight loop when idle events are responded
134     // to by RequestMore(true)
135     wxMacAutoreleasePool autoreleasepool;
136   
137     return [[NSApplication sharedApplication]
138             nextEventMatchingMask: NSAnyEventMask
139             untilDate: nil
140             inMode: NSDefaultRunLoopMode
141             dequeue: NO] != nil;
142 #else
143     return true;
144 #endif
145 }
146
147
148 bool wxGUIEventLoop::Dispatch()
149 {
150     if ( !wxTheApp )
151         return false;
152
153     wxMacAutoreleasePool autoreleasepool;
154
155     if(NSEvent *event = [NSApp
156                 nextEventMatchingMask:NSAnyEventMask
157                 untilDate:[NSDate dateWithTimeIntervalSinceNow: m_sleepTime]
158                 inMode:NSDefaultRunLoopMode
159                 dequeue: YES])
160     {
161         WXEVENTREF formerEvent = wxTheApp == NULL ? NULL : wxTheApp->MacGetCurrentEvent();
162         WXEVENTHANDLERCALLREF formerHandler = wxTheApp == NULL ? NULL : wxTheApp->MacGetCurrentEventHandlerCallRef();
163
164         if (wxTheApp)
165             wxTheApp->MacSetCurrentEvent(event, NULL);
166         m_sleepTime = 0.0;
167         [NSApp sendEvent: event];
168
169         if (wxTheApp)
170             wxTheApp->MacSetCurrentEvent(formerEvent , formerHandler);
171     }
172     else
173     {
174         if (wxTheApp)
175             wxTheApp->ProcessPendingEvents();
176         
177         if ( wxTheApp->ProcessIdle() )
178             m_sleepTime = 0.0 ;
179         else
180         {
181             m_sleepTime = 1.0;
182 #if wxUSE_THREADS
183             wxMutexGuiLeave();
184             wxMilliSleep(20);
185             wxMutexGuiEnter();
186 #endif
187         }
188     }
189
190     return true;
191 }
192
193 #endif
194
195 int wxGUIEventLoop::DoDispatchTimeout(unsigned long timeout)
196 {
197     wxMacAutoreleasePool autoreleasepool;
198
199     if ( m_modalSession )
200     {
201         NSInteger response = [NSApp runModalSession:(NSModalSession)m_modalSession];
202         
203         switch (response) 
204         {
205             case NSRunContinuesResponse:
206             {
207                 if ( [[NSApplication sharedApplication]
208                         nextEventMatchingMask: NSAnyEventMask
209                         untilDate: nil
210                         inMode: NSDefaultRunLoopMode
211                         dequeue: NO] != nil )
212                     return 1;
213                 
214                 return -1;
215             }
216                 
217             case NSRunStoppedResponse:
218             case NSRunAbortedResponse:
219                 return -1;
220             default:
221                 wxFAIL_MSG("unknown response code");
222                 break;
223         }
224         return -1;
225     }
226     else 
227     {        
228         NSEvent *event = [NSApp
229                     nextEventMatchingMask:NSAnyEventMask
230                     untilDate:[NSDate dateWithTimeIntervalSinceNow: timeout/1000]
231                     inMode:NSDefaultRunLoopMode
232                     dequeue: YES];
233         
234         if ( event == nil )
235             return -1;
236
237         [NSApp sendEvent: event];
238
239         return 1;
240     }
241 }
242
243 static int gs_loopNestingLevel = 0;
244
245 void wxGUIEventLoop::OSXDoRun()
246 {
247     /*
248        In order to properly nest GUI event loops in Cocoa, it is important to
249        have [NSApp run] only as the main/outermost event loop.  There are many
250        problems if [NSApp run] is used as an inner event loop.  The main issue
251        is that a call to [NSApp stop] is needed to exit an [NSApp run] event
252        loop. But the [NSApp stop] has some side effects that we do not want -
253        such as if there was a modal dialog box with a modal event loop running,
254        that event loop would also get exited, and the dialog would be closed.
255        The call to [NSApp stop] would also cause the enclosing event loop to
256        exit as well.
257
258        webkit's webcore library uses CFRunLoopRun() for nested event loops. See
259        the log of the commit log about the change in webkit's webcore module:
260        http://www.mail-archive.com/webkit-changes@lists.webkit.org/msg07397.html
261
262        See here for the latest run loop that is used in webcore:
263        https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/mac/RunLoopMac.mm
264
265        CFRunLoopRun() was tried for the nested event loop here but it causes a
266        problem in that all user input is disabled - and there is no way to
267        re-enable it.  The caller of this event loop may not want user input
268        disabled (such as synchronous wxExecute with wxEXEC_NODISABLE flag).
269
270        In order to have an inner event loop where user input can be enabled,
271        the old wxCocoa code that used the [NSApp nextEventMatchingMask] was
272        borrowed but changed to use blocking instead of polling. By specifying
273        'distantFuture' in 'untildate', we can have it block until the next
274        event.  Then we can keep looping on each new event until m_shouldExit is
275        raised to exit the event loop.
276     */
277     gs_loopNestingLevel++;
278     wxON_BLOCK_EXIT_SET(gs_loopNestingLevel, gs_loopNestingLevel - 1);
279
280     while ( !m_shouldExit )
281     {
282         // By putting this inside the loop, we can drain it in each
283         // loop iteration.
284         wxMacAutoreleasePool autoreleasepool;
285
286         if ( gs_loopNestingLevel == 1 )
287         {
288             // Use -[NSApplication run] for the main run loop.
289             [NSApp run];
290         }
291         else
292         {
293             // We use this blocking call to [NSApp nextEventMatchingMask:...]
294             // because the other methods (such as CFRunLoopRun() and [runLoop
295             // runMode:beforeDate] were always disabling input to the windows
296             // (even if we wanted it enabled).
297             //
298             // Here are the other run loops which were tried, but always left
299             // user input disabled:
300             //
301             // [runLoop runMode:NSDefaultRunLoopMode beforeDate:date];
302             // CFRunLoopRun();
303             // CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10 , true);
304             //
305             // Using [NSApp nextEventMatchingMask:...] would leave windows
306             // enabled if we wanted them to be, so that is why it is used.
307             NSEvent *event = [NSApp
308                     nextEventMatchingMask:NSAnyEventMask
309                     untilDate:[NSDate distantFuture]
310                     inMode:NSDefaultRunLoopMode
311                     dequeue: YES];
312
313             [NSApp sendEvent: event];
314
315             /**
316               The NSApplication documentation states that:
317
318               "
319               This method is invoked automatically in the main event loop
320               after each event when running in NSDefaultRunLoopMode or
321               NSModalRunLoopMode. This method is not invoked automatically
322               when running in NSEventTrackingRunLoopMode.
323               "
324
325               So to be safe, we also invoke it here in this event loop.
326
327               See: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSApplication_Class/Reference/Reference.html
328             */
329             [NSApp updateWindows];
330         }
331     }
332
333     // Wake up the enclosing loop so that it can check if it also needs
334     // to exit.
335     WakeUp();
336 }
337
338 void wxGUIEventLoop::OSXDoStop()
339 {
340     // We should only stop the top level event loop.
341     if ( gs_loopNestingLevel <= 1 )
342     {
343         [NSApp stop:0];
344     }
345
346     // For the top level loop only calling stop: is not enough when called from
347     // a runloop-observer, therefore add a dummy event, to make sure the
348     // runloop gets another round. And for the nested loops we need to wake it
349     // up to notice that it should exit, so do this unconditionally.
350     WakeUp();
351 }
352
353 void wxGUIEventLoop::WakeUp()
354 {
355     // NSEvent* cevent = [NSApp currentEvent];
356     // NSString* mode = [[NSRunLoop mainRunLoop] currentMode];
357     
358     // when already in a mouse event handler, don't add higher level event
359     // if ( cevent != nil && [cevent type] <= NSMouseMoved && )
360     if ( m_osxLowLevelWakeUp /* [NSEventTrackingRunLoopMode isEqualToString:mode] */ )
361     {
362         // NSLog(@"event for wakeup %@ in mode %@",cevent,mode);
363         wxCFEventLoop::WakeUp();        
364     }
365     else
366     {
367         wxMacAutoreleasePool autoreleasepool;
368         NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined 
369                                         location:NSMakePoint(0.0, 0.0) 
370                                    modifierFlags:0 
371                                        timestamp:0 
372                                     windowNumber:0 
373                                          context:nil
374                                          subtype:0 data1:0 data2:0]; 
375         [NSApp postEvent:event atStart:FALSE];
376     }
377 }
378
379 CFRunLoopRef wxGUIEventLoop::CFGetCurrentRunLoop() const
380 {
381     NSRunLoop* nsloop = [NSRunLoop currentRunLoop];
382     return [nsloop getCFRunLoop];
383 }
384
385
386 // TODO move into a evtloop_osx.cpp
387
388 wxModalEventLoop::wxModalEventLoop(wxWindow *modalWindow)
389 {
390     m_modalWindow = dynamic_cast<wxNonOwnedWindow*> (modalWindow);
391     wxASSERT_MSG( m_modalWindow != NULL, "must pass in a toplevel window for modal event loop" );
392     m_modalNativeWindow = m_modalWindow->GetWXWindow();
393 }
394
395 wxModalEventLoop::wxModalEventLoop(WXWindow modalNativeWindow)
396 {
397     m_modalWindow = NULL;
398     wxASSERT_MSG( modalNativeWindow != NULL, "must pass in a toplevel window for modal event loop" );
399     m_modalNativeWindow = modalNativeWindow;
400 }
401
402 // END move into a evtloop_osx.cpp
403
404 void wxModalEventLoop::OSXDoRun()
405 {
406     wxMacAutoreleasePool pool;
407
408     // If the app hasn't started, flush the event queue
409     // If we don't do this, the Dock doesn't get the message that
410     // the app has started so will refuse to activate it.
411     [NSApplication sharedApplication];
412     if (![NSApp isRunning])
413     {
414         while(NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])
415         {
416             [NSApp sendEvent:event];
417         }
418     }
419     
420     [NSApp runModalForWindow:m_modalNativeWindow];
421 }
422
423 void wxModalEventLoop::OSXDoStop()
424 {
425     [NSApp abortModal];
426 }
427
428 void wxGUIEventLoop::BeginModalSession( wxWindow* modalWindow )
429 {
430     WXWindow nsnow = nil;
431
432     if ( m_modalNestedLevel > 0 )
433     {
434         wxASSERT_MSG( m_modalWindow == modalWindow, "Nested Modal Sessions must be based on same window");
435         m_modalNestedLevel++;
436         return;
437     }
438     
439     m_modalWindow = modalWindow;
440     m_modalNestedLevel = 1;
441     
442     if ( modalWindow )
443     {
444         // we must show now, otherwise beginModalSessionForWindow does it but it
445         // also would do a centering of the window before overriding all our position
446         if ( !modalWindow->IsShownOnScreen() )
447             modalWindow->Show();
448         
449         wxNonOwnedWindow* now = dynamic_cast<wxNonOwnedWindow*> (modalWindow);
450         wxASSERT_MSG( now != NULL, "must pass in a toplevel window for modal event loop" );
451         nsnow = now ? now->GetWXWindow() : nil;
452     }
453     else
454     {
455         NSRect r = NSMakeRect(10, 10, 0, 0);
456         nsnow = [NSPanel alloc];
457         [nsnow initWithContentRect:r
458                                styleMask:NSBorderlessWindowMask
459                                  backing:NSBackingStoreBuffered
460                                    defer:YES
461          ];
462         [nsnow orderOut:nil];
463         m_dummyWindow = nsnow;
464     }
465     m_modalSession = [NSApp beginModalSessionForWindow:nsnow];
466     wxASSERT_MSG(m_modalSession != NULL, "modal session couldn't be started");
467 }
468
469 void wxGUIEventLoop::EndModalSession()
470 {
471     wxASSERT_MSG(m_modalSession != NULL, "no modal session active");
472     
473     wxASSERT_MSG(m_modalNestedLevel > 0, "incorrect modal nesting level");
474     
475     if ( --m_modalNestedLevel == 0 )
476     {
477         [NSApp endModalSession:(NSModalSession)m_modalSession];
478         m_modalSession = nil;
479         if ( m_dummyWindow )
480         {
481             [m_dummyWindow release];
482             m_dummyWindow = nil;
483         }
484     }
485 }
486
487 //
488 // 
489 //
490
491 wxWindowDisabler::wxWindowDisabler(bool disable)
492 {
493     m_modalEventLoop = NULL;
494     m_disabled = disable;
495     if ( disable )
496         DoDisable();
497 }
498
499 wxWindowDisabler::wxWindowDisabler(wxWindow *winToSkip)
500 {
501     m_disabled = true;
502     DoDisable(winToSkip);
503 }
504
505 void wxWindowDisabler::DoDisable(wxWindow *winToSkip)
506 {    
507     // remember the top level windows which were already disabled, so that we
508     // don't reenable them later
509     m_winDisabled = NULL;
510     
511     wxWindowList::compatibility_iterator node;
512     for ( node = wxTopLevelWindows.GetFirst(); node; node = node->GetNext() )
513     {
514         wxWindow *winTop = node->GetData();
515         if ( winTop == winToSkip )
516             continue;
517         
518         // we don't need to disable the hidden or already disabled windows
519         if ( winTop->IsEnabled() && winTop->IsShown() )
520         {
521             winTop->Disable();
522         }
523         else
524         {
525             if ( !m_winDisabled )
526             {
527                 m_winDisabled = new wxWindowList;
528             }
529             
530             m_winDisabled->Append(winTop);
531         }
532     }
533     
534     m_modalEventLoop = (wxEventLoop*)wxEventLoopBase::GetActive();
535     if (m_modalEventLoop)
536         m_modalEventLoop->BeginModalSession(winToSkip);
537 }
538
539 wxWindowDisabler::~wxWindowDisabler()
540 {
541     if ( !m_disabled )
542         return;
543     
544     if (m_modalEventLoop)
545         m_modalEventLoop->EndModalSession();
546     
547     wxWindowList::compatibility_iterator node;
548     for ( node = wxTopLevelWindows.GetFirst(); node; node = node->GetNext() )
549     {
550         wxWindow *winTop = node->GetData();
551         if ( !m_winDisabled || !m_winDisabled->Find(winTop) )
552         {
553             winTop->Enable();
554         }
555         //else: had been already disabled, don't reenable
556     }
557     
558     delete m_winDisabled;
559 }