/*
* 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 <das@users.sourceforge.net>
* 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 "tkMacOSXPrivate.h"
#include "tkMacOSXEvent.h"
#include "tkMacOSXConstants.h"
#include <tclInt.h>
#import <objc/objc-auto.h>
/* 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_OS_X_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 clientData) /* Not used. */
{
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 may 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(
ClientData clientData)
{
if (ticker) {
ticker = Tcl_CreateTimerHandler(TICK, Heartbeat, NULL);
}
}
static const Tcl_Time zeroBlockTime = { 0, 0 };
static void
TkMacOSXEventsSetupProc(
ClientData clientData,
int flags)
{
NSString *runloopMode = [[NSRunLoop currentRunLoop] currentMode];
/*
* 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 heartbeat.
*/
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(
ClientData 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:
*/