1/*
2 * tkMacOSXInit.c --
3 *
4 *	This file contains Mac OS X -specific interpreter initialization
5 *	functions.
6 *
7 * Copyright (c) 1995-1997 Sun Microsystems, Inc.
8 * Copyright 2001-2009, Apple Inc.
9 * Copyright (c) 2005-2009 Daniel A. Steffen <das@users.sourceforge.net>
10 *
11 * See the file "license.terms" for information on usage and redistribution of
12 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
13 *
14 * RCS: @(#) $Id$
15 */
16
17#include "tkMacOSXPrivate.h"
18
19#include "tclInt.h" /* for Tcl_GetStartupScript() & Tcl_SetStartupScript() */
20
21#include <sys/stat.h>
22#include <sys/utsname.h>
23#include <dlfcn.h>
24#include <objc/objc-auto.h>
25
26static char tkLibPath[PATH_MAX + 1] = "";
27
28/*
29 * If the App is in an App package, then we want to add the Scripts directory
30 * to the auto_path.
31 */
32
33static char scriptPath[PATH_MAX + 1] = "";
34
35int tkMacOSXGCEnabled = 0;
36long tkMacOSXMacOSXVersion = 0;
37
38#pragma mark TKApplication(TKInit)
39
40#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060
41#define NSTextInputContextKeyboardSelectionDidChangeNotification @"NSTextInputContextKeyboardSelectionDidChangeNotification"
42static void keyboardChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
43    [[NSNotificationCenter defaultCenter] postNotificationName:NSTextInputContextKeyboardSelectionDidChangeNotification object:nil userInfo:nil];
44}
45#endif
46
47@interface TKApplication(TKKeyboard)
48- (void)keyboardChanged:(NSNotification *)notification;
49@end
50
51#if MAC_OS_X_VERSION_MAX_ALLOWED >= 1060
52#define TKApplication_NSApplicationDelegate <NSApplicationDelegate>
53#else
54#define TKApplication_NSApplicationDelegate
55#endif
56@interface TKApplication(TKWindowEvent) TKApplication_NSApplicationDelegate
57- (void)_setupWindowNotifications;
58@end
59
60@interface TKApplication(TKScrlbr)
61- (void)_setupScrollBarNotifications;
62@end
63
64@interface TKApplication(TKMenus)
65- (void)_setupMenus;
66@end
67
68@implementation TKApplication
69@end
70
71@implementation TKApplication(TKInit)
72#ifdef TK_MAC_DEBUG_NOTIFICATIONS
73- (void)_postedNotification:(NSNotification *)notification {
74    TKLog(@"-[%@(%p) %s] %@", [self class], self, _cmd, notification);
75}
76#endif
77- (void)_setupApplicationNotifications {
78    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
79#define observe(n, s) [nc addObserver:self selector:@selector(s) name:(n) object:nil]
80    observe(NSApplicationDidBecomeActiveNotification, applicationActivate:);
81    observe(NSApplicationDidResignActiveNotification, applicationDeactivate:);
82    observe(NSApplicationDidUnhideNotification, applicationShowHide:);
83    observe(NSApplicationDidHideNotification, applicationShowHide:);
84    observe(NSApplicationDidChangeScreenParametersNotification, displayChanged:);
85    observe(NSTextInputContextKeyboardSelectionDidChangeNotification, keyboardChanged:);
86#undef observe
87#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060
88    CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), NULL, &keyboardChanged, kTISNotifySelectedKeyboardInputSourceChanged, NULL, CFNotificationSuspensionBehaviorCoalesce);
89#endif
90}
91- (void)_setupEventLoop {
92    _running = 1;
93    if (!_appFlags._hasBeenRun) {
94        _appFlags._hasBeenRun = YES;
95	[self finishLaunching];
96    }
97    [self setWindowsNeedUpdate:YES];
98}
99- (void)_setup:(Tcl_Interp *)interp {
100    _eventInterp = interp;
101    _defaultMainMenu = nil;
102    [self _setupMenus];
103    [self setDelegate:self];
104#ifdef TK_MAC_DEBUG_NOTIFICATIONS
105    [[NSNotificationCenter defaultCenter] addObserver:self
106	    selector:@selector(_postedNotification:) name:nil object:nil];
107#endif
108    [self _setupWindowNotifications];
109    [self _setupScrollBarNotifications];
110    [self _setupApplicationNotifications];
111}
112- (NSString *)tkFrameworkImagePath:(NSString*)image {
113    NSString *path = nil;
114    if (tkLibPath[0] != '\0') {
115	path = [[NSBundle bundleWithPath:[[NSString stringWithUTF8String:
116		tkLibPath] stringByAppendingString:@"/../.."]]
117		pathForImageResource:image];
118    }
119    if (!path) {
120	const char *tk_library = Tcl_GetVar2(_eventInterp, "tk_library", NULL,
121		TCL_GLOBAL_ONLY);
122	if (tk_library) {
123	    NSFileManager *fm = [NSFileManager defaultManager];
124	    path = [[NSString stringWithUTF8String:tk_library]
125		    stringByAppendingFormat:@"/%@", image];
126	    if (![fm isReadableFileAtPath:path]) {
127		path = [[NSString stringWithUTF8String:tk_library]
128			stringByAppendingFormat:@"/../macosx/%@", image];
129		if (![fm isReadableFileAtPath:path]) {
130		    path = nil;
131		}
132	    }
133	}
134    }
135#ifdef TK_MAC_DEBUG
136    if (!path && getenv("TK_SRCROOT")) {
137	path = [[NSString stringWithUTF8String:getenv("TK_SRCROOT")]
138		stringByAppendingFormat:@"/macosx/%@", image];
139	if (![[NSFileManager defaultManager] isReadableFileAtPath:path]) {
140	    path = nil;
141	}
142    }
143#endif
144    return path;
145}
146@end
147
148#pragma mark -
149
150/*
151 *----------------------------------------------------------------------
152 *
153 * DoWindowActivate --
154 *
155 *	Idle handler that sets the application icon to the generic Tk icon.
156 *
157 * Results:
158 *	None.
159 *
160 * Side effects:
161 *	None.
162 *
163 *----------------------------------------------------------------------
164 */
165
166static void
167SetApplicationIcon(
168    ClientData clientData)
169{
170    NSString *path = [NSApp tkFrameworkImagePath:@"Tk.icns"];
171    if (path) {
172	NSImage *image = [[NSImage alloc] initWithContentsOfFile:path];
173	if (image) {
174	    [NSApp setApplicationIconImage:image];
175	    [image release];
176	}
177    }
178}
179
180/*
181 *----------------------------------------------------------------------
182 *
183 * TkpInit --
184 *
185 *	Performs Mac-specific interpreter initialization related to the
186 *	tk_library variable.
187 *
188 * Results:
189 *	Returns a standard Tcl result. Leaves an error message or result in
190 *	the interp's result.
191 *
192 * Side effects:
193 *	Sets "tk_library" Tcl variable, runs "tk.tcl" script.
194 *
195 *----------------------------------------------------------------------
196 */
197
198int
199TkpInit(
200    Tcl_Interp *interp)
201{
202    static int initialized = 0;
203
204    /*
205     * Since it is possible for TkInit to be called multiple times and we
206     * don't want to do the following initialization multiple times we protect
207     * against doing it more than once.
208     */
209
210    if (!initialized) {
211	int bundledExecutable = 0;
212	CFBundleRef bundleRef;
213	CFURLRef bundleUrl = NULL;
214	struct utsname name;
215	struct stat st;
216
217	initialized = 1;
218
219	/*
220	 * Initialize/check OS version variable for runtime checks.
221	 */
222
223#if MAC_OS_X_VERSION_MIN_REQUIRED < 1050
224#   error Mac OS X 10.5 required
225#endif
226
227	if (!uname(&name)) {
228	    tkMacOSXMacOSXVersion = (strtod(name.release, NULL) + 96) * 10;
229	}
230	if (tkMacOSXMacOSXVersion &&
231		tkMacOSXMacOSXVersion/10 < MAC_OS_X_VERSION_MIN_REQUIRED/10) {
232	    Tcl_Panic("Mac OS X 10.%d or later required !",
233		    (MAC_OS_X_VERSION_MIN_REQUIRED/10)-100);
234	}
235
236#ifdef TK_FRAMEWORK
237	/*
238	 * When Tk is in a framework, force tcl_findLibrary to look in the
239	 * framework scripts directory.
240	 * FIXME: Should we come up with a more generic way of doing this?
241	 */
242
243	if (Tcl_MacOSXOpenVersionedBundleResources(interp,
244		"com.tcltk.tklibrary", TK_FRAMEWORK_VERSION, 0, PATH_MAX,
245		tkLibPath) != TCL_OK) {
246	    TkMacOSXDbgMsg("Tcl_MacOSXOpenVersionedBundleResources failed");
247	}
248#endif
249
250	static NSAutoreleasePool *pool = nil;
251	if (!pool) {
252	    pool = [NSAutoreleasePool new];
253	}
254	tkMacOSXGCEnabled = ([NSGarbageCollector defaultCollector] != nil);
255	[[NSUserDefaults standardUserDefaults] registerDefaults:
256		[NSDictionary dictionaryWithObjectsAndKeys:
257		[NSNumber numberWithBool:YES],
258		@"_NSCanWrapButtonTitles",
259		[NSNumber numberWithInt:-1],
260		@"NSStringDrawingTypesetterBehavior",
261		nil]];
262	[TKApplication sharedApplication];
263	[NSApp _setup:interp];
264
265	/* Check whether we are a bundled executable: */
266	bundleRef = CFBundleGetMainBundle();
267	if (bundleRef) {
268	    bundleUrl = CFBundleCopyBundleURL(bundleRef);
269	}
270	if (bundleUrl) {
271	    /*
272	     * A bundled executable is two levels down from its main bundle
273	     * directory (e.g. Wish.app/Contents/MacOS/Wish), whereas an
274	     * unbundled executable's main bundle directory is just the
275	     * directory containing the executable. So to check whether we are
276	     * bundled, we delete the last three path components of the
277	     * executable's url and compare the resulting url with the main
278	     * bundle url.
279	     */
280
281	    int j = 3;
282	    CFURLRef url = CFBundleCopyExecutableURL(bundleRef);
283
284	    while (url && j--) {
285		CFURLRef parent =
286			CFURLCreateCopyDeletingLastPathComponent(NULL, url);
287
288		CFRelease(url);
289		url = parent;
290	    }
291	    if (url) {
292		bundledExecutable = CFEqual(bundleUrl, url);
293		CFRelease(url);
294	    }
295	    CFRelease(bundleUrl);
296	}
297
298	if (!bundledExecutable) {
299	    /*
300	     * If we are loaded into an executable that is not a bundled
301	     * application, the window server does not let us come to the
302	     * foreground. For such an executable, notify the window server
303	     * that we are now a full GUI application.
304	     */
305
306	    OSStatus err = procNotFound;
307	    ProcessSerialNumber psn = { 0, kCurrentProcess };
308
309	    err = ChkErr(TransformProcessType, &psn,
310		    kProcessTransformToForegroundApplication);
311
312	    /*
313	     * Set application icon to generic Tk icon, do it at idle time
314	     * instead of now to ensure tk_library is setup.
315	     */
316
317	    Tcl_DoWhenIdle(SetApplicationIcon, NULL);
318	}
319
320	[NSApp _setupEventLoop];
321	TkMacOSXInitAppleEvents(interp);
322	TkMacOSXUseAntialiasedText(interp, -1);
323	TkMacOSXInitCGDrawing(interp, TRUE, 0);
324	[pool drain];
325	pool = [NSAutoreleasePool new];
326
327	/*
328	 * FIXME: Close stdin & stdout for remote debugging otherwise we will
329	 * fight with gdb for stdin & stdout
330	 */
331
332	if (getenv("XCNOSTDIN") != NULL) {
333	    close(0);
334	    close(1);
335	}
336
337	/*
338	 * If we don't have a TTY and stdin is a special character file of
339	 * length 0, (e.g. /dev/null, which is what Finder sets when double
340	 * clicking Wish) then use the Tk based console interpreter.
341	 */
342
343	if (getenv("TK_CONSOLE") ||
344		(!isatty(0) && (fstat(0, &st) ||
345		(S_ISCHR(st.st_mode) && st.st_blocks == 0)))) {
346	    Tk_InitConsoleChannels(interp);
347	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDIN));
348	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDOUT));
349	    Tcl_RegisterChannel(interp, Tcl_GetStdChannel(TCL_STDERR));
350
351	    /*
352	     * Only show the console if we don't have a startup script
353	     * and tcl_interactive hasn't been set already.
354	     */
355
356	    if (Tcl_GetStartupScript(NULL) == NULL) {
357		const char *intvar = Tcl_GetVar(interp,
358			"tcl_interactive", TCL_GLOBAL_ONLY);
359
360		if (intvar == NULL) {
361		    Tcl_SetVar(interp, "tcl_interactive", "1",
362			    TCL_GLOBAL_ONLY);
363		}
364	    }
365	    if (Tk_CreateConsoleWindow(interp) == TCL_ERROR) {
366		return TCL_ERROR;
367	    }
368	}
369    }
370
371    Tk_MacOSXSetupTkNotifier();
372
373    if (tkLibPath[0] != '\0') {
374	Tcl_SetVar(interp, "tk_library", tkLibPath, TCL_GLOBAL_ONLY);
375    }
376
377    if (scriptPath[0] != '\0') {
378	Tcl_SetVar(interp, "auto_path", scriptPath,
379		TCL_GLOBAL_ONLY|TCL_LIST_ELEMENT|TCL_APPEND_VALUE);
380    }
381
382    Tcl_CreateObjCommand(interp, "::tk::mac::standardAboutPanel",
383	    TkMacOSXStandardAboutPanelObjCmd, NULL, NULL);
384    Tcl_CreateObjCommand(interp, "::tk::mac::iconBitmap",
385	    TkMacOSXIconBitmapObjCmd, NULL, NULL);
386
387    return TCL_OK;
388}
389
390/*
391 *----------------------------------------------------------------------
392 *
393 * TkpGetAppName --
394 *
395 *	Retrieves the name of the current application from a platform specific
396 *	location. For Unix, the application name is the tail of the path
397 *	contained in the tcl variable argv0.
398 *
399 * Results:
400 *	Returns the application name in the given Tcl_DString.
401 *
402 * Side effects:
403 *	None.
404 *
405 *----------------------------------------------------------------------
406 */
407
408void
409TkpGetAppName(
410    Tcl_Interp *interp,
411    Tcl_DString *namePtr)	/* A previously initialized Tcl_DString. */
412{
413    const char *p, *name;
414
415    name = Tcl_GetVar(interp, "argv0", TCL_GLOBAL_ONLY);
416    if ((name == NULL) || (*name == 0)) {
417	name = "tk";
418    } else {
419	p = strrchr(name, '/');
420	if (p != NULL) {
421	    name = p+1;
422	}
423    }
424    Tcl_DStringAppend(namePtr, name, -1);
425}
426
427/*
428 *----------------------------------------------------------------------
429 *
430 * TkpDisplayWarning --
431 *
432 *	This routines is called from Tk_Main to display warning messages that
433 *	occur during startup.
434 *
435 * Results:
436 *	None.
437 *
438 * Side effects:
439 *	Generates messages on stdout.
440 *
441 *----------------------------------------------------------------------
442 */
443
444void
445TkpDisplayWarning(
446    const char *msg,		/* Message to be displayed. */
447    const char *title)		/* Title of warning. */
448{
449    Tcl_Channel errChannel = Tcl_GetStdChannel(TCL_STDERR);
450
451    if (errChannel) {
452	Tcl_WriteChars(errChannel, title, -1);
453	Tcl_WriteChars(errChannel, ": ", 2);
454	Tcl_WriteChars(errChannel, msg, -1);
455	Tcl_WriteChars(errChannel, "\n", 1);
456    }
457}
458
459/*
460 *----------------------------------------------------------------------
461 *
462 * TkMacOSXDefaultStartupScript --
463 *
464 *	On MacOS X, we look for a file in the Resources/Scripts directory
465 *	called AppMain.tcl and if found, we set argv[1] to that, so that the
466 *	rest of the code will find it, and add the Scripts folder to the
467 *	auto_path. If we don't find the startup script, we just bag it,
468 *	assuming the user is starting up some other way.
469 *
470 * Results:
471 *	None.
472 *
473 * Side effects:
474 *	Tcl_SetStartupScript() called when AppMain.tcl found.
475 *
476 *----------------------------------------------------------------------
477 */
478
479MODULE_SCOPE void
480TkMacOSXDefaultStartupScript(void)
481{
482    CFBundleRef bundleRef;
483
484    bundleRef = CFBundleGetMainBundle();
485
486    if (bundleRef != NULL) {
487	CFURLRef appMainURL = CFBundleCopyResourceURL(bundleRef,
488		CFSTR("AppMain"), CFSTR("tcl"), CFSTR("Scripts"));
489
490	if (appMainURL != NULL) {
491	    CFURLRef scriptFldrURL;
492	    char startupScript[PATH_MAX + 1];
493
494	    if (CFURLGetFileSystemRepresentation (appMainURL, true,
495		    (unsigned char *) startupScript, PATH_MAX)) {
496		Tcl_SetStartupScript(Tcl_NewStringObj(startupScript,-1), NULL);
497		scriptFldrURL = CFURLCreateCopyDeletingLastPathComponent(NULL,
498			appMainURL);
499		if (scriptFldrURL != NULL) {
500		    CFURLGetFileSystemRepresentation(scriptFldrURL, true,
501			    (unsigned char *) scriptPath, PATH_MAX);
502		    CFRelease(scriptFldrURL);
503		}
504	    }
505	    CFRelease(appMainURL);
506	}
507    }
508}
509
510/*
511 *----------------------------------------------------------------------
512 *
513 * TkMacOSXGetNamedSymbol --
514 *
515 *	Dynamically acquire address of a named symbol from a loaded dynamic
516 *	library, so that we can use API that may not be available on all OS
517 *	versions.
518 *
519 * Results:
520 *	Address of given symbol or NULL if unavailable.
521 *
522 * Side effects:
523 *	None.
524 *
525 *----------------------------------------------------------------------
526 */
527
528MODULE_SCOPE void*
529TkMacOSXGetNamedSymbol(
530    const char* module,
531    const char* symbol)
532{
533    void *addr = dlsym(RTLD_NEXT, symbol);
534    if (!addr) {
535	(void) dlerror(); /* Clear dlfcn error state */
536    }
537    return addr;
538}
539
540/*
541 *----------------------------------------------------------------------
542 *
543 * TkMacOSXGetStringObjFromCFString --
544 *
545 *	Get a string object from a CFString as efficiently as possible.
546 *
547 * Results:
548 *	New string object or NULL if conversion failed.
549 *
550 * Side effects:
551 *	None.
552 *
553 *----------------------------------------------------------------------
554 */
555
556MODULE_SCOPE Tcl_Obj*
557TkMacOSXGetStringObjFromCFString(
558    CFStringRef str)
559{
560    Tcl_Obj *obj = NULL;
561    const char *c = CFStringGetCStringPtr(str, kCFStringEncodingUTF8);
562
563    if (c) {
564	obj = Tcl_NewStringObj(c, -1);
565    } else {
566	CFRange all = CFRangeMake(0, CFStringGetLength(str));
567	CFIndex len;
568
569	if (CFStringGetBytes(str, all, kCFStringEncodingUTF8, 0, false, NULL,
570		0, &len) > 0 && len < INT_MAX) {
571	    obj = Tcl_NewObj();
572	    Tcl_SetObjLength(obj, len);
573	    CFStringGetBytes(str, all, kCFStringEncodingUTF8, 0, false,
574		    (UInt8*) obj->bytes, len, NULL);
575	}
576    }
577    return obj;
578}
579
580/*
581 * Local Variables:
582 * mode: c
583 * c-basic-offset: 4
584 * fill-column: 79
585 * coding: utf-8
586 * End:
587 */
588