Skip to content

Commit 985e8f4

Browse files
tacaswellmeeseeksmachine
authored andcommitted
Backport PR matplotlib#28981: FIX: macos: Use standard NSApp run loop in our input hook
1 parent f4e9226 commit 985e8f4

File tree

1 file changed

+56
-54
lines changed

1 file changed

+56
-54
lines changed

src/_macosx.m

+56-54
Original file line numberDiff line numberDiff line change
@@ -40,60 +40,84 @@
4040
static bool keyChangeCapsLock = false;
4141
/* Keep track of the current mouse up/down state for open/closed cursor hand */
4242
static bool leftMouseGrabbing = false;
43-
/* Keep track of whether stdin has been received */
44-
static bool stdin_received = false;
45-
static bool stdin_sigint = false;
4643
// Global variable to store the original SIGINT handler
4744
static PyOS_sighandler_t originalSigintAction = NULL;
4845

49-
// Signal handler for SIGINT, only sets a flag to exit the run loop
46+
// Stop the current app's run loop, sending an event to ensure it actually stops
47+
static void stopWithEvent() {
48+
[NSApp stop: nil];
49+
// Post an event to trigger the actual stopping.
50+
[NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined
51+
location: NSZeroPoint
52+
modifierFlags: 0
53+
timestamp: 0
54+
windowNumber: 0
55+
context: nil
56+
subtype: 0
57+
data1: 0
58+
data2: 0]
59+
atStart: YES];
60+
}
61+
62+
// Signal handler for SIGINT, only argument matching for stopWithEvent
5063
static void handleSigint(int signal) {
51-
stdin_sigint = true;
64+
stopWithEvent();
65+
}
66+
67+
// Helper function to flush all events.
68+
// This is needed in some instances to ensure e.g. that windows are properly closed.
69+
// It is used in the input hook as well as wrapped in a version callable from Python.
70+
static void flushEvents() {
71+
while (true) {
72+
NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny
73+
untilDate: [NSDate distantPast]
74+
inMode: NSDefaultRunLoopMode
75+
dequeue: YES];
76+
if (!event) {
77+
break;
78+
}
79+
[NSApp sendEvent:event];
80+
}
5281
}
5382

5483
static int wait_for_stdin() {
55-
@autoreleasepool {
56-
stdin_received = false;
57-
stdin_sigint = false;
84+
// Short circuit if no windows are active
85+
// Rely on Python's input handling to manage CPU usage
86+
// This queries the NSApp, rather than using our FigureWindowCount because that is decremented when events still
87+
// need to be processed to properly close the windows.
88+
if (![[NSApp windows] count]) {
89+
flushEvents();
90+
return 1;
91+
}
5892

93+
@autoreleasepool {
5994
// Set up a SIGINT handler to interrupt the event loop if ctrl+c comes in too
6095
originalSigintAction = PyOS_setsig(SIGINT, handleSigint);
6196

6297
// Create an NSFileHandle for standard input
6398
NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput];
6499

100+
65101
// Register for data available notifications on standard input
66-
[[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification
67-
object: stdinHandle
68-
queue: [NSOperationQueue mainQueue] // Use the main queue
69-
usingBlock: ^(NSNotification *notification) {
70-
// Mark that input has been received
71-
stdin_received = true;
72-
}
102+
id notificationID = [[NSNotificationCenter defaultCenter] addObserverForName: NSFileHandleDataAvailableNotification
103+
object: stdinHandle
104+
queue: [NSOperationQueue mainQueue] // Use the main queue
105+
usingBlock: ^(NSNotification *notification) {stopWithEvent();}
73106
];
74107

75108
// Wait in the background for anything that happens to stdin
76109
[stdinHandle waitForDataInBackgroundAndNotify];
77110

78-
// continuously run an event loop until the stdin_received flag is set to exit
79-
while (!stdin_received && !stdin_sigint) {
80-
// This loop is similar to the main event loop and flush_events which have
81-
// Py_[BEGIN|END]_ALLOW_THREADS surrounding the loop.
82-
// This should not be necessary here because PyOS_InputHook releases the GIL for us.
83-
while (true) {
84-
NSEvent *event = [NSApp nextEventMatchingMask: NSEventMaskAny
85-
untilDate: [NSDate distantPast]
86-
inMode: NSDefaultRunLoopMode
87-
dequeue: YES];
88-
if (!event) { break; }
89-
[NSApp sendEvent: event];
90-
}
91-
}
111+
// Run the application's event loop, which will be interrupted on stdin or SIGINT
112+
[NSApp run];
113+
92114
// Remove the input handler as an observer
93-
[[NSNotificationCenter defaultCenter] removeObserver: stdinHandle];
115+
[[NSNotificationCenter defaultCenter] removeObserver: notificationID];
116+
94117

95118
// Restore the original SIGINT handler upon exiting the function
96119
PyOS_setsig(SIGINT, originalSigintAction);
120+
97121
return 1;
98122
}
99123
}
@@ -236,18 +260,7 @@ static void lazy_init(void) {
236260
static PyObject*
237261
stop(PyObject* self)
238262
{
239-
[NSApp stop: nil];
240-
// Post an event to trigger the actual stopping.
241-
[NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined
242-
location: NSZeroPoint
243-
modifierFlags: 0
244-
timestamp: 0
245-
windowNumber: 0
246-
context: nil
247-
subtype: 0
248-
data1: 0
249-
data2: 0]
250-
atStart: YES];
263+
stopWithEvent();
251264
Py_RETURN_NONE;
252265
}
253266

@@ -382,20 +395,9 @@ static CGFloat _get_device_scale(CGContextRef cr)
382395
// We run the app, matching any events that are waiting in the queue
383396
// to process, breaking out of the loop when no events remain and
384397
// displaying the canvas if needed.
385-
NSEvent *event;
386-
387398
Py_BEGIN_ALLOW_THREADS
388399

389-
while (true) {
390-
event = [NSApp nextEventMatchingMask: NSEventMaskAny
391-
untilDate: [NSDate distantPast]
392-
inMode: NSDefaultRunLoopMode
393-
dequeue: YES];
394-
if (!event) {
395-
break;
396-
}
397-
[NSApp sendEvent:event];
398-
}
400+
flushEvents();
399401

400402
Py_END_ALLOW_THREADS
401403

0 commit comments

Comments
 (0)