/*
 * tkMacOSXCarbonEvents.c --
 *
 *	This file implements functions that register for and handle
 *	various Carbon Events and Timers. Most carbon events of interest
 *	to TkAqua are processed in a handler registered on the dispatcher
 *	event target so that we get first crack at them before HIToolbox
 *	dispatchers/processes them further.
 *	As some events are sent directly to the focus or app event target
 *	and not dispatched normally, we also register a handler on the
 *	application event target.
 *
 * Copyright 2001, Apple Computer, Inc.
 * Copyright (c) 2005-2007 Daniel A. Steffen <das@users.sourceforge.net>
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 *	The following terms apply to all files originating from Apple
 *	Computer, Inc. ("Apple") and associated with the software
 *	unless explicitly disclaimed in individual files.
 *
 *
 *	Apple hereby grants permission to use, copy, modify,
 *	distribute, and license this software and its documentation
 *	for any purpose, provided that existing copyright notices are
 *	retained in all copies and that this notice is included
 *	verbatim in any distributions. No written agreement, license,
 *	or royalty fee is required for any of the authorized
 *	uses. Modifications to this software may be copyrighted by
 *	their authors and need not follow the licensing terms
 *	described here, provided that the new terms are clearly
 *	indicated on the first page of each file where they apply.
 *
 *
 *	IN NO EVENT SHALL APPLE, THE AUTHORS OR DISTRIBUTORS OF THE
 *	SOFTWARE BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,
 *	INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
 *	THIS SOFTWARE, ITS DOCUMENTATION, OR ANY DERIVATIVES THEREOF,
 *	EVEN IF APPLE OR THE AUTHORS HAVE BEEN ADVISED OF THE
 *	POSSIBILITY OF SUCH DAMAGE.  APPLE, THE AUTHORS AND
 *	DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING,
 *	BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.	 THIS
 *	SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND APPLE,THE
 *	AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE
 *	MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 *
 *	GOVERNMENT USE: If you are acquiring this software on behalf
 *	of the U.S. government, the Government shall have only
 *	"Restricted Rights" in the software and related documentation
 *	as defined in the Federal Acquisition Regulations (FARs) in
 *	Clause 52.227.19 (c) (2).  If you are acquiring the software
 *	on behalf of the Department of Defense, the software shall be
 *	classified as "Commercial Computer Software" and the
 *	Government shall have only "Restricted Rights" as defined in
 *	Clause 252.227-7013 (c) (1) of DFARs.  Notwithstanding the
 *	foregoing, the authors grant the U.S. Government and others
 *	acting in its behalf permission to use and distribute the
 *	software in accordance with the terms specified in this
 *	license.
 *
 * RCS: @(#) $Id: tkMacOSXCarbonEvents.c,v 1.16 2007/04/23 21:24:33 das Exp $
 */

#include "tkMacOSXInt.h"
#include "tkMacOSXEvent.h"
#include "tkMacOSXDebug.h"

/*
#ifdef TK_MAC_DEBUG
#define TK_MAC_DEBUG_CARBON_EVENTS
#endif
*/

/*
 * Declarations of functions used only in this file:
 */

static OSStatus CarbonEventHandlerProc(EventHandlerCallRef callRef,
	EventRef event, void *userData);
static OSStatus InstallStandardApplicationEventHandler(void);
static void ExitRaelEventHandlerProc(EventHandlerCallRef callRef,
	EventRef event, void *userData) __attribute__ ((__noreturn__));
static void CarbonTimerProc(EventLoopTimerRef timer, void *userData);

/*
 * Static data used by several functions in this file:
 */

static jmp_buf exitRaelJmpBuf;
static EventLoopTimerRef carbonTimer = NULL;
static int carbonTimerEnabled = 0;
static EventHandlerUPP carbonEventHandlerUPP = NULL;
static Tcl_Interp *carbonEventInterp = NULL;
static int inTrackingLoop = 0;


/*
 *----------------------------------------------------------------------
 *
 * CarbonEventHandlerProc --
 *
 *	This procedure is the handler for all registered CarbonEvents.
 *
 * Results:
 *	OS status code.
 *
 * Side effects:
 *	Dispatches CarbonEvents.
 *
 *----------------------------------------------------------------------
 */

static OSStatus
CarbonEventHandlerProc(
    EventHandlerCallRef callRef,
    EventRef event,
    void *userData)
{
    OSStatus err = eventNotHandledErr;
    TkMacOSXEvent macEvent;
    MacEventStatus eventStatus;

    macEvent.eventRef = event;
    macEvent.eClass = GetEventClass(event);
    macEvent.eKind = GetEventKind(event);
    macEvent.interp = (Tcl_Interp *) userData;
    macEvent.callRef = callRef;
    bzero(&eventStatus, sizeof(eventStatus));

#ifdef TK_MAC_DEBUG_CARBON_EVENTS
    if (macEvent.eKind != kEventMouseMoved &&
	    macEvent.eKind != kEventMouseDragged) {
	TkMacOSXDbgMsg("Started handling %s",
		TkMacOSXCarbonEventToAscii(event));
	TkMacOSXInitNamedDebugSymbol(HIToolbox, void, _DebugPrintEvent,
		EventRef inEvent);
	if (_DebugPrintEvent) {
	    /*
	     * Carbon-internal event debugging (c.f. Technote 2124)
	     */

	    _DebugPrintEvent(event);
	}
    }
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */

    TkMacOSXProcessEvent(&macEvent,&eventStatus);
    if (eventStatus.stopProcessing) {
	err = noErr;
    }

#ifdef TK_MAC_DEBUG_CARBON_EVENTS
    if (macEvent.eKind != kEventMouseMoved &&
	    macEvent.eKind != kEventMouseDragged) {
	TkMacOSXDbgMsg("Finished handling %s: %s handled",
		TkMacOSXCarbonEventToAscii(event),
		eventStatus.stopProcessing ? "   " : "not");
    }
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
    return err;
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXInitCarbonEvents --
 *
 *	This procedure initializes all CarbonEvent handlers.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Handlers for Carbon Events are registered.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE void
TkMacOSXInitCarbonEvents(
    Tcl_Interp *interp)
{
    const EventTypeSpec dispatcherEventTypes[] = {
	{kEventClassKeyboard,	 kEventRawKeyDown},
	{kEventClassKeyboard,	 kEventRawKeyRepeat},
	{kEventClassKeyboard,	 kEventRawKeyUp},
	{kEventClassKeyboard,	 kEventRawKeyModifiersChanged},
	{kEventClassKeyboard,	 kEventRawKeyRepeat},
    };
    const EventTypeSpec applicationEventTypes[] = {
	{kEventClassMenu,	 kEventMenuBeginTracking},
	{kEventClassMenu,	 kEventMenuEndTracking},
	{kEventClassMenu,	 kEventMenuOpening},
	{kEventClassMenu,	 kEventMenuTargetItem},
	{kEventClassCommand,	 kEventCommandProcess},
	{kEventClassCommand,	 kEventCommandUpdateStatus},
	{kEventClassApplication, kEventAppActivated},
	{kEventClassApplication, kEventAppDeactivated},
	{kEventClassApplication, kEventAppQuit},
	{kEventClassApplication, kEventAppHidden},
	{kEventClassApplication, kEventAppShown},
	{kEventClassApplication, kEventAppAvailableWindowBoundsChanged},
	{kEventClassAppearance,	 kEventAppearanceScrollBarVariantChanged},
    };

    carbonEventHandlerUPP = NewEventHandlerUPP(CarbonEventHandlerProc);
    carbonEventInterp = interp;
    ChkErr(InstallStandardApplicationEventHandler);
    ChkErr(InstallEventHandler, GetEventDispatcherTarget(),
	    carbonEventHandlerUPP, GetEventTypeCount(dispatcherEventTypes),
	    dispatcherEventTypes, (void *) carbonEventInterp, NULL);
    ChkErr(InstallEventHandler, GetApplicationEventTarget(),
	    carbonEventHandlerUPP, GetEventTypeCount(applicationEventTypes),
	    applicationEventTypes, (void *) carbonEventInterp, NULL);

#ifdef TK_MAC_DEBUG_CARBON_EVENTS
    TkMacOSXInitNamedDebugSymbol(HIToolbox, void, _TraceEventByName,
	    CFStringRef);
    if (_TraceEventByName) {
	/* Carbon-internal event debugging (c.f. Technote 2124) */
	_TraceEventByName(CFSTR("kEventRawKeyDown"));
	_TraceEventByName(CFSTR("kEventRawKeyRepeat"));
	_TraceEventByName(CFSTR("kEventRawKeyUp"));
	_TraceEventByName(CFSTR("kEventRawKeyModifiersChanged"));
	_TraceEventByName(CFSTR("kEventRawKeyRepeat"));
	_TraceEventByName(CFSTR("kEventMenuBeginTracking"));
	_TraceEventByName(CFSTR("kEventMenuEndTracking"));
	_TraceEventByName(CFSTR("kEventMenuOpening"));
	_TraceEventByName(CFSTR("kEventMenuTargetItem"));
	_TraceEventByName(CFSTR("kEventCommandProcess"));
	_TraceEventByName(CFSTR("kEventCommandUpdateStatus"));
	_TraceEventByName(CFSTR("kEventAppActivated"));
	_TraceEventByName(CFSTR("kEventAppDeactivated"));
	_TraceEventByName(CFSTR("kEventAppQuit"));
	_TraceEventByName(CFSTR("kEventAppHidden"));
	_TraceEventByName(CFSTR("kEventAppShown"));
	_TraceEventByName(CFSTR("kEventAppAvailableWindowBoundsChanged"));
	_TraceEventByName(CFSTR("kEventAppearanceScrollBarVariantChanged"));
	_TraceEventByName(CFSTR("kEventMouseDown"));
	_TraceEventByName(CFSTR("kEventMouseUp"));
#if 0
	_TraceEventByName(CFSTR("kEventMouseMoved"));
	_TraceEventByName(CFSTR("kEventMouseDragged"));
#endif
	_TraceEventByName(CFSTR("kEventMouseWheelMoved"));
	_TraceEventByName(CFSTR("kEventMouseScroll"));
	_TraceEventByName(CFSTR("kEventWindowActivated"));
	_TraceEventByName(CFSTR("kEventWindowDeactivated"));
	_TraceEventByName(CFSTR("kEventWindowUpdate"));
	_TraceEventByName(CFSTR("kEventWindowExpanded"));
	_TraceEventByName(CFSTR("kEventWindowBoundsChanged"));
	_TraceEventByName(CFSTR("kEventWindowDragStarted"));
	_TraceEventByName(CFSTR("kEventWindowDragCompleted"));
	_TraceEventByName(CFSTR("kEventWindowConstrain"));
	_TraceEventByName(CFSTR("kEventWindowGetRegion"));
	_TraceEventByName(CFSTR("kEventWindowDrawContent"));
    }
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXInstallWindowCarbonEventHandler --
 *
 *	This procedure installs our window CarbonEvent handler.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Handler for Carbon Events is registered.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE void
TkMacOSXInstallWindowCarbonEventHandler(
	Tcl_Interp *interp, WindowRef window)
{
    const EventTypeSpec windowEventTypes[] = {
	{kEventClassMouse,	 kEventMouseDown},
	{kEventClassMouse,	 kEventMouseUp},
	{kEventClassMouse,	 kEventMouseMoved},
	{kEventClassMouse,	 kEventMouseDragged},
	{kEventClassMouse,	 kEventMouseWheelMoved},
	{kEventClassWindow,	 kEventWindowActivated},
	{kEventClassWindow,	 kEventWindowDeactivated},
	{kEventClassWindow,	 kEventWindowUpdate},
	{kEventClassWindow,	 kEventWindowExpanded},
	{kEventClassWindow,	 kEventWindowBoundsChanged},
	{kEventClassWindow,	 kEventWindowDragStarted},
	{kEventClassWindow,	 kEventWindowDragCompleted},
	{kEventClassWindow,	 kEventWindowConstrain},
	{kEventClassWindow,	 kEventWindowGetRegion},
	{kEventClassWindow,	 kEventWindowDrawContent},
    };

    ChkErr(InstallEventHandler, GetWindowEventTarget(window),
	    carbonEventHandlerUPP, GetEventTypeCount(windowEventTypes),
	    windowEventTypes, (void *) (interp ? interp : carbonEventInterp),
	    NULL);
}

/*
 *----------------------------------------------------------------------
 *
 * InstallStandardApplicationEventHandler --
 *
 *	This procedure installs the carbon standard application event
 *	handler.
 *
 * Results:
 *	OS status code.
 *
 * Side effects:
 *	Standard handlers for application Carbon Events are registered.
 *
 *----------------------------------------------------------------------
 */

static OSStatus
InstallStandardApplicationEventHandler(void)
{
   /*
    * This is a hack to workaround missing Carbon API to install the standard
    * application event handler (InstallStandardEventHandler() does not work
    * on the application target). The only way to install the standard app
    * handler is to call RunApplicationEventLoop(), but since we are running
    * our own event loop, we'll immediately need to break out of RAEL again:
    * we do this via longjmp out of the ExitRaelEventHandlerProc event handler
    * called first off from RAEL by posting a high priority dummy event.
    * This workaround is derived from a similar approach in Technical Q&A 1061.
    */
    enum {
	kExitRaelEvent = 'ExiT'
    };
    const EventTypeSpec exitRaelEventType = {
	kExitRaelEvent, kExitRaelEvent
    };
    EventHandlerUPP exitRaelEventHandler;
    EventHandlerRef exitRaelEventHandlerRef = NULL;
    EventRef exitRaelEvent = NULL;
    OSStatus err = memFullErr;

    exitRaelEventHandler = NewEventHandlerUPP(
	    (EventHandlerProcPtr) ExitRaelEventHandlerProc);
    if (exitRaelEventHandler) {
	err = ChkErr(InstallEventHandler, GetEventDispatcherTarget(),
		exitRaelEventHandler, 1, &exitRaelEventType, NULL,
		&exitRaelEventHandlerRef);
    }
    if (err == noErr) {
	err = ChkErr(CreateEvent, NULL, kExitRaelEvent, kExitRaelEvent,
		GetCurrentEventTime(), kEventAttributeNone, &exitRaelEvent);
    }
    if (err == noErr) {
	err = ChkErr(PostEventToQueue, GetMainEventQueue(), exitRaelEvent,
		kEventPriorityHigh);
    }
    if (err == noErr) {
	if (!setjmp(exitRaelJmpBuf)) {
	    RunApplicationEventLoop();

	    /*
	     * This point should never be reached!
	     */

	    Tcl_Panic("RunApplicationEventLoop exited !");
	}
    }
    if (exitRaelEvent) {
	ReleaseEvent(exitRaelEvent);
    }
    if (exitRaelEventHandlerRef) {
	RemoveEventHandler(exitRaelEventHandlerRef);
    }
    if (exitRaelEventHandler) {
	DisposeEventHandlerUPP(exitRaelEventHandler);
    }
    return err;
}

/*
 *----------------------------------------------------------------------
 *
 * ExitRaelEventHandlerProc --
 *
 *	This procedure is the dummy event handler used to break out of
 *	RAEL via longjmp, it is called as the first ever event handler
 *	in RAEL by posting a high priority dummy event.
 *
 * Results:
 *	None. Never returns !
 *
 * Side effects:
 *	longjmp back to InstallStandardApplicationEventHandler().
 *
 *----------------------------------------------------------------------
 */

static void
ExitRaelEventHandlerProc(
    EventHandlerCallRef callRef,
    EventRef event,
    void *userData)
{
    longjmp(exitRaelJmpBuf, 1);
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXRunTclEventLoop --
 *
 *	Process a limited number of tcl events.
 *
 * Results:
 *	Returns 1 if events were handled and 0 otherwise.
 *
 * Side effects:
 *	Runs the Tcl event loop.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE int
TkMacOSXRunTclEventLoop(void)
{
    int i = 4, result = 0;

    /* Avoid starving main event loop: process at most 4 events. */
    while(--i && Tcl_ServiceAll()) {
	result = 1;
    }
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * CarbonTimerProc --
 *
 *	This procedure is the carbon timer handler that runs the tcl
 *	event loop periodically.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Runs the Tcl event loop.
 *
 *----------------------------------------------------------------------
 */

static void
CarbonTimerProc(
    EventLoopTimerRef timer,
    void *userData)
{
    if(carbonTimerEnabled > 0 && TkMacOSXRunTclEventLoop()) {
#ifdef TK_MAC_DEBUG_CARBON_EVENTS
	TkMacOSXDbgMsg("Processed tcl events from carbon timer");
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXStartTclEventLoopCarbonTimer --
 *
 *	This procedure installs (if necessary) and starts a carbon
 *	event timer that runs the tcl event loop periodically.
 *	It should be called whenever a nested carbon event loop might
 *	run by HIToolbox (e.g. during mouse tracking) to ensure that
 *	tcl events continue to be processed.
 *
 * Results:
 *	OS status code.
 *
 * Side effects:
 *	Carbon event timer is installed and started.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE OSStatus
TkMacOSXStartTclEventLoopCarbonTimer(void)
{
    OSStatus err = noErr;

    if (++carbonTimerEnabled > 0) {
	if(!carbonTimer) {
	    EventLoopTimerUPP timerUPP = NewEventLoopTimerUPP(CarbonTimerProc);

	    err = ChkErr(InstallEventLoopTimer, GetMainEventLoop(),
		    5 * kEventDurationMillisecond,
		    5 * kEventDurationMillisecond,
		    timerUPP, NULL, &carbonTimer);
	} else {
	    err = ChkErr(SetEventLoopTimerNextFireTime, carbonTimer,
		    5 * kEventDurationMillisecond);
	}
    }
    return err;
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXStopTclEventLoopCarbonTimer --
 *
 *	This procedure stops the carbon event timer started by
 *	TkMacOSXStartTclEventLoopCarbonTimer().
 *
 * Results:
 *	OS status code.
 *
 * Side effects:
 *	Carbon event timer is stopped.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE OSStatus
TkMacOSXStopTclEventLoopCarbonTimer(void)
{
    OSStatus err = noErr;

    if (--carbonTimerEnabled == 0) {
	if(carbonTimer) {
	    err = ChkErr(SetEventLoopTimerNextFireTime, carbonTimer,
		    kEventDurationForever);
	}
    }
    return err;
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXTrackingLoop --
 *
 *	Call with 1 before entering a mouse tracking loop (e.g. window
 *	resizing or menu tracking) to enable tcl event processing but
 *	disable  carbon event processing (except for update events)
 *	during the loop, and with 0 after exiting the loop to reset.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

MODULE_SCOPE void
TkMacOSXTrackingLoop(int tracking)
{
    static int previousServiceMode = TCL_SERVICE_NONE;

    if (tracking) {
	inTrackingLoop++;
	previousServiceMode = Tcl_SetServiceMode(TCL_SERVICE_ALL);
	TkMacOSXStartTclEventLoopCarbonTimer();
#ifdef TK_MAC_DEBUG_CARBON_EVENTS
	TkMacOSXDbgMsg("Entering tracking loop");
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
    } else {
	TkMacOSXStopTclEventLoopCarbonTimer();
	previousServiceMode = Tcl_SetServiceMode(previousServiceMode);
	inTrackingLoop--;
#ifdef TK_MAC_DEBUG_CARBON_EVENTS
	TkMacOSXDbgMsg("Exiting tracking loop");
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkMacOSXReceiveAndDispatchEvent --
 *
 *	This receives a carbon event and sends it to the carbon event
 *	dispatcher.
 *
 * Results:
 *	Mac OS status
 *
 * Side effects:
 *	This receives and dispatches the next Carbon event.
 *
 *----------------------------------------------------------------------
 */
MODULE_SCOPE OSStatus
TkMacOSXReceiveAndDispatchEvent(void)
{
    static EventTargetRef targetRef = NULL;
    int numEventTypes = 0;
    const EventTypeSpec *eventTypes = NULL;
    EventRef eventRef;
    OSStatus err;
    const EventTypeSpec trackingEventTypes[] = {
	{'dniw',		 kEventWindowUpdate},
	{kEventClassWindow,	 kEventWindowUpdate},
    };

    if (inTrackingLoop > 0) {
	eventTypes = trackingEventTypes;
	numEventTypes = GetEventTypeCount(trackingEventTypes);
    }

    /*
     * This is a poll, since we have already counted the events coming
     * into this routine, and are guaranteed to have one waiting.
     */

    err = ReceiveNextEvent(numEventTypes, eventTypes,
	    kEventDurationNoWait, true, &eventRef);
    if (err == noErr) {
#ifdef TK_MAC_DEBUG_CARBON_EVENTS
	UInt32 kind = GetEventKind(eventRef);

	if (kind != kEventMouseMoved && kind != kEventMouseDragged) {
	    TkMacOSXDbgMsg("Dispatching %s", TkMacOSXCarbonEventToAscii(eventRef));
	    TkMacOSXInitNamedDebugSymbol(HIToolbox, void, _DebugPrintEvent,
		    EventRef inEvent);
	    if (_DebugPrintEvent) {
		/* Carbon-internal event debugging (c.f. Technote 2124) */
		_DebugPrintEvent(eventRef);
	    }
	}
#endif /* TK_MAC_DEBUG_CARBON_EVENTS */
	if (!targetRef) {
	    targetRef = GetEventDispatcherTarget();
	}
	TkMacOSXStartTclEventLoopCarbonTimer();
	err = SendEventToEventTarget(eventRef, targetRef);
	TkMacOSXStopTclEventLoopCarbonTimer();
	if (err != noErr && err != eventLoopTimedOutErr
		&& err != eventNotHandledErr) {
	    TkMacOSXDbgMsg("SendEventToEventTarget(%s) failed: %ld",
		    TkMacOSXCarbonEventToAscii(eventRef), err);
	}
	ReleaseEvent(eventRef);
    } else if (err != eventLoopTimedOutErr) {
	TkMacOSXDbgMsg("ReceiveNextEvent failed: %ld", err);
    }
    return err;
}
