/* * tkMacOSXNotify.c -- * * This file contains the implementation of a tcl event source * for the AppKit event loop. * * Copyright (c) 1995-1997 Sun Microsystems, Inc. * Copyright 2001-2009, Apple Inc. * Copyright (c) 2005-2009 Daniel A. Steffen * Copyright 2015 Marc Culler. * * See the file "license.terms" for information on usage and redistribution * of this file, and for a DISCLAIMER OF ALL WARRANTIES. */ #include #include "tkMacOSXPrivate.h" #include "tkMacOSXInt.h" #include "tkMacOSXConstants.h" #import /* This is not used for anything at the moment. */ typedef struct ThreadSpecificData { int initialized; } ThreadSpecificData; static Tcl_ThreadDataKey dataKey; #define TSD_INIT() ThreadSpecificData *tsdPtr = \ Tcl_GetThreadData(&dataKey, sizeof(ThreadSpecificData)) static void TkMacOSXNotifyExitHandler(ClientData clientData); static void TkMacOSXEventsSetupProc(ClientData clientData, int flags); static void TkMacOSXEventsCheckProc(ClientData clientData, int flags); #ifdef TK_MAC_DEBUG_EVENTS static const char *Tk_EventName[39] = { "", "", "KeyPress", /*2*/ "KeyRelease", /*3*/ "ButtonPress", /*4*/ "ButtonRelease", /*5*/ "MotionNotify", /*6*/ "EnterNotify", /*7*/ "LeaveNotify", /*8*/ "FocusIn", /*9*/ "FocusOut", /*10*/ "KeymapNotify", /*11*/ "Expose", /*12*/ "GraphicsExpose", /*13*/ "NoExpose", /*14*/ "VisibilityNotify", /*15*/ "CreateNotify", /*16*/ "DestroyNotify", /*17*/ "UnmapNotify", /*18*/ "MapNotify", /*19*/ "MapRequest", /*20*/ "ReparentNotify", /*21*/ "ConfigureNotify", /*22*/ "ConfigureRequest", /*23*/ "GravityNotify", /*24*/ "ResizeRequest", /*25*/ "CirculateNotify", /*26*/ "CirculateRequest", /*27*/ "PropertyNotify", /*28*/ "SelectionClear", /*29*/ "SelectionRequest", /*30*/ "SelectionNotify", /*31*/ "ColormapNotify", /*32*/ "ClientMessage", /*33*/ "MappingNotify", /*34*/ "VirtualEvent", /*35*/ "ActivateNotify", /*36*/ "DeactivateNotify", /*37*/ "MouseWheelEvent" /*38*/ }; static Tk_RestrictAction InspectQueueRestrictProc( ClientData arg, XEvent *eventPtr) { XVirtualEvent* ve = (XVirtualEvent*) eventPtr; const char *name; long serial = ve->serial; long time = eventPtr->xkey.time; if (eventPtr->type == VirtualEvent) { name = ve->name; } else { name = Tk_EventName[eventPtr->type]; } fprintf(stderr, " > %s;serial = %lu; time=%lu)\n", name, serial, time); return TK_DEFER_EVENT; } /* * Debugging tool which prints the current Tcl queue. */ void DebugPrintQueue(void) { ClientData oldArg; Tk_RestrictProc *oldProc; oldProc = Tk_RestrictEvents(InspectQueueRestrictProc, NULL, &oldArg); fprintf(stderr, "Current queue:\n"); while (Tcl_DoOneEvent(TCL_ALL_EVENTS|TCL_DONT_WAIT)) {}; Tk_RestrictEvents(oldProc, oldArg, &oldArg); } # endif #pragma mark TKApplication(TKNotify) @implementation TKApplication(TKNotify) /* * Earlier versions of Tk would override nextEventMatchingMask here, adding a * call to displayIfNeeded on all windows after calling super. This would cause * windows to be redisplayed (if necessary) each time that an event was * received. This was intended to replace Apple's default autoDisplay * mechanism, which the earlier versions of Tk would disable. When autoDisplay * is set to the default value of YES, the Apple event loop will call * displayIfNeeded on all windows at the beginning of each iteration of their * event loop. Since Tk does not call the Apple event loop, it was thought * that the autoDisplay behavior needed to be replicated. * * However, as of OSX 10.14 (Mojave) the autoDisplay property became * deprecated. Luckily it turns out that, even though we don't ever start the * Apple event loop, the Apple window manager still calls displayIfNeeded on * all windows on a regular basis, perhaps each time the queue is empty. So we * no longer, and perhaps never did need to set autoDisplay to NO, nor call * displayIfNeeded on our windows. We can just leave all of that to the window * manager. */ /* * Since the contentView is the first responder for a Tk Window, it is * responsible for sending events up the responder chain. We also check the * pasteboard here. */ - (void) sendEvent: (NSEvent *) theEvent { /* * Workaround for an Apple bug. When an accented character is selected * from an NSTextInputClient popup character viewer with the mouse, Apple * sends an event of type NSAppKitDefined and subtype 21. If that event is * sent up the responder chain it causes Apple to print a warning to the * console log and, extremely obnoxiously, also to stderr, which says * "Window move completed without beginning." Apparently they are sending * the "move completed" event without having sent the "move began" event of * subtype 20, and then announcing their error on our stderr. Also, of * course, no movement is occurring. The popup is not movable and is just * being closed. The bug has been reported to Apple. If they ever fix it, * this block should be removed. */ # if MAC_OSX_VERSION_MAX_ALLOWED >= 101500 if ([theEvent type] == NSAppKitDefined) { static Bool aWindowIsMoving = NO; switch([theEvent subtype]) { case 20: aWindowIsMoving = YES; break; case 21: if (aWindowIsMoving) { aWindowIsMoving = NO; break; } else { // printf("Bug!!!!\n"); return; } default: break; } } #endif [super sendEvent:theEvent]; [NSApp tkCheckPasteboard]; #ifdef TK_MAC_DEBUG_EVENTS fprintf(stderr, "Sending event of type %d\n", (int)[theEvent type]); DebugPrintQueue(); #endif } - (void) _runBackgroundLoop { while(Tcl_DoOneEvent(TCL_IDLE_EVENTS|TCL_TIMER_EVENTS|TCL_DONT_WAIT)){ TkMacOSXDrawAllViews(NULL); } } @end #pragma mark - /* *---------------------------------------------------------------------- * * GetRunLoopMode -- * * Results: * RunLoop mode that should be passed to -nextEventMatchingMask: * * Side effects: * None. * *---------------------------------------------------------------------- */ static NSString * GetRunLoopMode(NSModalSession modalSession) { NSString *runLoopMode = nil; if (modalSession) { runLoopMode = NSModalPanelRunLoopMode; } if (!runLoopMode) { runLoopMode = [[NSRunLoop currentRunLoop] currentMode]; } if (!runLoopMode) { runLoopMode = NSDefaultRunLoopMode; } return runLoopMode; } /* *---------------------------------------------------------------------- * * Tk_MacOSXSetupTkNotifier -- * * This procedure is called during Tk initialization to create the event * source for TkAqua events. * * Results: * None. * * Side effects: * A new event source is created. * *---------------------------------------------------------------------- */ void Tk_MacOSXSetupTkNotifier(void) { TSD_INIT(); if (!tsdPtr->initialized) { tsdPtr->initialized = 1; /* * Install TkAqua event source in main event loop thread. */ if (CFRunLoopGetMain() == CFRunLoopGetCurrent()) { if (![NSThread isMainThread]) { /* * Panic if main runloop is not on the main application thread. */ Tcl_Panic("Tk_MacOSXSetupTkNotifier: %s", "first [load] of TkAqua has to occur in the main thread!"); } Tcl_CreateEventSource(TkMacOSXEventsSetupProc, TkMacOSXEventsCheckProc, NULL); TkCreateExitHandler(TkMacOSXNotifyExitHandler, NULL); TclMacOSXNotifierAddRunLoopMode(NSEventTrackingRunLoopMode); TclMacOSXNotifierAddRunLoopMode(NSModalPanelRunLoopMode); } } } /* *---------------------------------------------------------------------- * * TkMacOSXNotifyExitHandler -- * * This function is called during finalization to clean up the * TkMacOSXNotify module. * * Results: * None. * * Side effects: * None. * *---------------------------------------------------------------------- */ static void TkMacOSXNotifyExitHandler( ClientData dummy) /* Not used. */ { (void)dummy; TSD_INIT(); Tcl_DeleteEventSource(TkMacOSXEventsSetupProc, TkMacOSXEventsCheckProc, NULL); tsdPtr->initialized = 0; } /* *---------------------------------------------------------------------- * * TkMacOSXDrawAllViews -- * * This static function is meant to be run as an idle task. It attempts * to redraw all views which have the tkNeedsDisplay property set to YES. * This relies on a feature of [NSApp nextEventMatchingMask: ...] which * is undocumented, namely that it sometimes blocks and calls drawRect * for all views that need display before it returns. We call it with * deQueue=NO so that it will not change anything on the AppKit event * queue, because we only want the side effect that it runs drawRect. The * only time when any NSViews have the needsDisplay property set to YES * is during execution of this function. * * The reason for running this function as an idle task is to try to * arrange that all widgets will be fully configured before they are * drawn. Any idle tasks that might reconfigure them should be higher on * the idle queue, so they should be run before the display procs are run * by drawRect. * * If this function is called directly with non-NULL clientData parameter * then the int which it references will be set to the number of windows * that need display, but the needsDisplay property of those windows will * not be changed. * * Results: * None. * * Side effects: * Parts of windows my get redrawn. * *---------------------------------------------------------------------- */ void TkMacOSXDrawAllViews( ClientData clientData) { int count = 0, *dirtyCount = (int *)clientData; for (NSWindow *window in [NSApp windows]) { if ([[window contentView] isMemberOfClass:[TKContentView class]]) { TKContentView *view = [window contentView]; if ([view tkNeedsDisplay]) { count++; if (dirtyCount) { continue; } [view setNeedsDisplayInRect:[view tkDirtyRect]]; } } else { [window displayIfNeeded]; } } if (dirtyCount) { *dirtyCount = count; } [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:GetRunLoopMode(TkMacOSXGetModalSession()) dequeue:NO]; for (NSWindow *window in [NSApp windows]) { if ([[window contentView] isMemberOfClass:[TKContentView class]]) { TKContentView *view = [window contentView]; /* * If we did not run drawRect, we set needsDisplay back to NO. * Note that if drawRect did run it may have added to Tk's dirty * rect, due to attempts to draw outside of drawRect's dirty rect. */ if ([view needsDisplay]) { [view setNeedsDisplay: NO]; } } } [NSApp setNeedsToDraw:NO]; } /* *---------------------------------------------------------------------- * * TkMacOSXEventsSetupProc -- * * This procedure implements the setup part of the MacOSX event source. It * is invoked by Tcl_DoOneEvent before calling TkMacOSXEventsCheckProc to * process all queued NSEvents. In our case, all we need to do is to set * the Tcl MaxBlockTime to 0 before starting the loop to process all * queued NSEvents. * * Results: * None. * * Side effects: * * If NSEvents are queued, or if there is any drawing that needs to be * done, then the maximum block time will be set to 0 to ensure that * Tcl_WaitForEvent returns immediately. * *---------------------------------------------------------------------- */ #define TICK 200 static Tcl_TimerToken ticker = NULL; static void Heartbeat( TCL_UNUSED(ClientData)) { if (ticker) { ticker = Tcl_CreateTimerHandler(TICK, Heartbeat, NULL); } } static const Tcl_Time zeroBlockTime = { 0, 0 }; static void TkMacOSXEventsSetupProc( ClientData dummy, int flags) { NSString *runloopMode = [[NSRunLoop currentRunLoop] currentMode]; (void)dummy; /* * runloopMode will be nil if we are in a Tcl event loop. */ if (flags & TCL_WINDOW_EVENTS && !runloopMode) { [NSApp _resetAutoreleasePool]; /* * After calling this setup proc, Tcl_DoOneEvent will call * Tcl_WaitForEvent. Then it will call check proc to collect the * events and translate them into XEvents. * * If we have any events waiting or if there is any drawing to be done * we want Tcl_WaitForEvent to return immediately. So we set the block * time to 0 and stop the heatbeat. */ NSEvent *currentEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:GetRunLoopMode(TkMacOSXGetModalSession()) dequeue:NO]; if ((currentEvent) || [NSApp needsToDraw] ) { Tcl_SetMaxBlockTime(&zeroBlockTime); Tcl_DeleteTimerHandler(ticker); ticker = NULL; } else if (ticker == NULL) { /* * When the user is not generating events we schedule a "heartbeat" * TimerHandler to fire every 200 milliseconds. The handler does * nothing, but when its timer fires it causes Tcl_WaitForEvent to * return. This helps avoid hangs when calling vwait during the * non-regression tests. */ ticker = Tcl_CreateTimerHandler(TICK, Heartbeat, NULL); } } } /* *---------------------------------------------------------------------- * * TkMacOSXEventsCheckProc -- * * This procedure loops through all NSEvents waiting in the TKApplication * event queue, generating X events from them. * * Results: * None. * * Side effects: * NSevents are used to generate X events, which are added to the Tcl * event queue. * *---------------------------------------------------------------------- */ static void TkMacOSXEventsCheckProc( TCL_UNUSED(ClientData), int flags) { NSString *runloopMode = [[NSRunLoop currentRunLoop] currentMode]; int eventsFound = 0; /* * runloopMode will be nil if we are in a Tcl event loop. */ if (flags & TCL_WINDOW_EVENTS && !runloopMode) { NSEvent *currentEvent = nil; NSEvent *testEvent = nil; NSModalSession modalSession; /* * It is possible for the SetupProc to be called before this function * returns. This happens, for example, when we process an event which * opens a modal window. To prevent premature release of our * application-wide autorelease pool by a nested call to the SetupProc, * we must lock it here. */ [NSApp _lockAutoreleasePool]; do { modalSession = TkMacOSXGetModalSession(); testEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:GetRunLoopMode(modalSession) dequeue:NO]; /* * We must not steal any events during LiveResize. */ if (testEvent && [[testEvent window] inLiveResize]) { break; } currentEvent = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:GetRunLoopMode(modalSession) dequeue:YES]; if (currentEvent) { /* * Generate Xevents. */ NSEvent *processedEvent = [NSApp tkProcessEvent:currentEvent]; if (processedEvent) { eventsFound++; #ifdef TK_MAC_DEBUG_EVENTS TKLog(@" event: %@", currentEvent); #endif if (modalSession) { [NSApp _modalSession:modalSession sendEvent:currentEvent]; } else { [NSApp sendEvent:currentEvent]; } } } else { break; } } while (1); /* * Now we can unlock the pool. */ [NSApp _unlockAutoreleasePool]; /* * Add an idle task to the end of the idle queue which will redisplay * all of our dirty windows. We want this to happen after all other * idle tasks have run so that all widgets will be configured before * they are displayed. The drawRect method "borrows" the idle queue * while drawing views. That is, it sends expose events which cause * display procs to be posted as idle tasks and then runs an inner * event loop to processes those idle tasks. We are trying to arrange * for the idle queue to be empty when it starts that process and empty * when it finishes. */ int dirtyCount = 0; TkMacOSXDrawAllViews(&dirtyCount); if (dirtyCount > 0) { Tcl_CancelIdleCall(TkMacOSXDrawAllViews, NULL); Tcl_DoWhenIdle(TkMacOSXDrawAllViews, NULL); } } } /* * Local Variables: * mode: objc * c-basic-offset: 4 * fill-column: 79 * coding: utf-8 * End: */