Files
cappuccino/AppKit/CPApplication.j
T

1461 lines
43 KiB
Plaintext

/*
* CPApplication.j
* AppKit
*
* Created by Francisco Tolmasky.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#import "../Foundation/CPRange.h"
@import <Foundation/CPBundle.j>
@import "CPCompatibility.j"
@import "CPColorPanel.j"
@import "CPCursor.j"
@import "CPDocumentController.j"
@import "CPEvent.j"
@import "CPMenu.j"
@import "CPResponder.j"
@import "CPThemeBlend.j"
@import "CPCibLoading.j"
@import "CPPanel.j"
@import "CPPlatform.j"
@import "CPWindowController.j"
var CPMainCibFile = @"CPMainCibFile",
CPMainCibFileHumanFriendly = @"Main cib file base name",
CPEventModifierFlags = 0;
CPApp = nil;
CPApplicationWillFinishLaunchingNotification = @"CPApplicationWillFinishLaunchingNotification";
CPApplicationDidFinishLaunchingNotification = @"CPApplicationDidFinishLaunchingNotification";
CPApplicationWillTerminateNotification = @"CPApplicationWillTerminateNotification";
CPApplicationWillBecomeActiveNotification = @"CPApplicationWillBecomeActiveNotification";
CPApplicationDidBecomeActiveNotification = @"CPApplicationDidBecomeActiveNotification";
CPApplicationWillResignActiveNotification = @"CPApplicationWillResignActiveNotification";
CPApplicationDidResignActiveNotification = @"CPApplicationDidResignActiveNotification";
CPTerminateNow = YES;
CPTerminateCancel = NO;
CPTerminateLater = -1; // not currently supported
CPRunStoppedResponse = -1000;
CPRunAbortedResponse = -1001;
CPRunContinuesResponse = -1002;
/*!
@ingroup appkit
@class CPApplication
CPApplication is THE way to start up the Cappuccino framework for your application to use.
Every GUI application has exactly one instance of CPApplication (or of a custom subclass of
CPApplication). Your program's main() function can create that instance by calling the
\c CPApplicationMain function. A simple example looks like this:
<pre>
function main(args, namedArgs)
{
CPApplicationMain(args, namedArgs);
}
</pre>
@delegate -(void)applicationDidFinishLaunching:(CPNotification)aNotification;
Sent from the notification center after the app initializes, but before
receiving events.
@param aNotification contains information about the event
@delegate -(void)applicationWillFinishLaunching:(CPNotification)aNotification;
Sent from the notification center before the app is initialized.
@param aNotification contains information about the event
*/
@implementation CPApplication : CPResponder
{
CPArray _eventListeners;
CPEvent _currentEvent;
CPWindow _lastMouseMoveWindow;
CPArray _windows;
CPWindow _keyWindow;
CPWindow _mainWindow;
CPWindow _previousKeyWindow;
CPWindow _previousMainWindow;
CPDocumentController _documentController;
CPModalSession _currentSession;
//
id _delegate;
BOOL _finishedLaunching;
BOOL _isActive;
CPDictionary _namedArgs;
CPArray _args;
CPString _fullArgsString;
CPImage _applicationIconImage;
CPPanel _aboutPanel;
CPThemeBlend _themeBlend @accessors(property=themeBlend);
}
/*!
Returns the singleton instance of the running application. If it
doesn't exist, it will be created, and then returned.
@return the application singleton
*/
+ (CPApplication)sharedApplication
{
if (!CPApp)
CPApp = [[CPApplication alloc] init];
return CPApp;
}
/*!
Initializes the Document based application with basic menu functions.
Functions are \c New, \c Open, \c Undo, \c Redo, \c Save, \c Cut, \c Copy, \c Paste.
@return the initialized application
*/
- (id)init
{
self = [super init];
CPApp = self;
if (self)
{
_eventListeners = [];
_windows = [[CPNull null]];
}
return self;
}
// Configuring Applications
/*!
Sets the delegate for this application. The delegate will receive various notifications
caused by user interactions during the application's run. The delegate can choose to
react to these events.
@param aDelegate the delegate object
*/
- (void)setDelegate:(id)aDelegate
{
if (_delegate == aDelegate)
return;
var defaultCenter = [CPNotificationCenter defaultCenter],
delegateNotifications =
[
CPApplicationWillFinishLaunchingNotification, @selector(applicationWillFinishLaunching:),
CPApplicationDidFinishLaunchingNotification, @selector(applicationDidFinishLaunching:),
CPApplicationWillBecomeActiveNotification, @selector(applicationWillBecomeActive:),
CPApplicationDidBecomeActiveNotification, @selector(applicationDidBecomeActive:),
CPApplicationWillResignActiveNotification, @selector(applicationWillResignActive:),
CPApplicationDidResignActiveNotification, @selector(applicationDidResignActive:),
CPApplicationWillTerminateNotification, @selector(applicationWillTerminate:)
],
count = [delegateNotifications count];
if (_delegate)
{
var index = 0;
for (; index < count; index += 2)
{
var notificationName = delegateNotifications[index],
selector = delegateNotifications[index + 1];
if ([_delegate respondsToSelector:selector])
[defaultCenter removeObserver:_delegate name:notificationName object:self];
}
}
_delegate = aDelegate;
var index = 0;
for (; index < count; index += 2)
{
var notificationName = delegateNotifications[index],
selector = delegateNotifications[index + 1];
if ([_delegate respondsToSelector:selector])
[defaultCenter addObserver:_delegate selector:selector name:notificationName object:self];
}
}
/*!
Returns the application's delegate. The app can only have one delegate at a time.
*/
- (id)delegate
{
return _delegate;
}
/*!
This method is called by \c -run before the event loop begins.
When it successfully completes, it posts the notification
CPApplicationDidFinishLaunchingNotification. If you override
\c -finishLaunching, the subclass method should invoke the superclass method.
*/
- (void)finishLaunching
{
// At this point we clear the window.status to eliminate Safari's "Cancelled" error message
// The message shouldn't be displayed, because only an XHR is cancelled, but it is a usability issue.
// We do it here so that applications can change it in willFinish or didFinishLaunching
#if PLATFORM(DOM)
window.status = " ";
#endif
// We also want to set the default cursor on the body, so that buttons and things don't have an iBeam
[[CPCursor arrowCursor] set];
var bundle = [CPBundle mainBundle],
delegateClassName = [bundle objectForInfoDictionaryKey:@"CPApplicationDelegateClass"];
if (delegateClassName)
{
var delegateClass = objj_getClass(delegateClassName);
if (delegateClass)
[self setDelegate:[[delegateClass alloc] init]];
}
var defaultCenter = [CPNotificationCenter defaultCenter];
[defaultCenter
postNotificationName:CPApplicationWillFinishLaunchingNotification
object:self];
var types = [bundle objectForInfoDictionaryKey:@"CPBundleDocumentTypes"];
if ([types count] > 0)
_documentController = [CPDocumentController sharedDocumentController];
var needsUntitled = !!_documentController,
URLStrings = nil;
#if PLATFORM(DOM)
URLStrings = window.cpOpeningURLStrings && window.cpOpeningURLStrings();
#endif
var index = 0,
count = [URLStrings count];
for (; index < count; ++index)
needsUntitled = ![self _openURL:[CPURL URLWithString:URLStrings[index]]] && needsUntitled;
if (needsUntitled && [_delegate respondsToSelector:@selector(applicationShouldOpenUntitledFile:)])
needsUntitled = [_delegate applicationShouldOpenUntitledFile:self];
if (needsUntitled)
[_documentController newDocument:self];
[_documentController _updateRecentDocumentsMenu];
[defaultCenter
postNotificationName:CPApplicationDidFinishLaunchingNotification
object:self];
[[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
_finishedLaunching = YES;
}
- (void)terminate:(id)aSender
{
[[CPNotificationCenter defaultCenter]
postNotificationName:CPApplicationWillTerminateNotification
object:self];
if (![CPPlatform isBrowser])
{
[[CPDocumentController sharedDocumentController] closeAllDocumentsWithDelegate:self
didCloseAllSelector:@selector(_documentController:didCloseAll:context:)
contextInfo:nil];
}
else
{
[[[self keyWindow] platformWindow] _propagateCurrentDOMEvent:YES];
}
}
/*!
Sets the applications icon image. This image is used in the default "About" window.
By default this value is pulled from the CPApplicationIcon key in your info.plist file.
@param anImage - The image to set.
*/
- (void)setApplicationIconImage:(CPImage)anImage
{
_applicationIconImage = anImage;
}
/*!
Returns the application icon image. By default this is pulled from the CPApplicationIcon key of info.plist.
@return CPImage - Your application icon image.
*/
- (CPImage)applicationIconImage
{
if (_applicationIconImage)
return _applicationIconImage;
var imagePath = [[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPApplicationIcon"];
if (imagePath)
_applicationIconImage = [[CPImage alloc] initWithContentsOfFile:imagePath];
return _applicationIconImage;
}
/*!
Opens the standard about panel with no options.
*/
- (void)orderFrontStandardAboutPanel:(id)sender
{
[self orderFrontStandardAboutPanelWithOptions:nil];
}
/*!
Opens the standard about panel. This method takes a single argument \c options.
Options is a dictionary that can contain any of the following keys:
<pre>
ApplicationName - The name of your application.
ApplicationIcon - Application icon image.
Version - The full version of your application
ApplicationVersion - The shorter version number of your application.
Copyright - Human readable copyright information.
</pre>
If you choose not the include any of the above keys, they will default
to the following respective keys in your info.plist file.
<pre>
CPBundleName
CPApplicationIcon (through a call to -applicationIconImage, see documentation for that method for more details)
CPBundleVersion
CPBundleShortVersionString
CPHumanReadableCopyright
</pre>
@param options - A dictionary with the aboe listed keys. You can pass nil to default to your plist values.
*/
- (void)orderFrontStandardAboutPanelWithOptions:(CPDictionary)options
{
if (!_aboutPanel)
{
var mainInfo = [[CPBundle mainBundle] infoDictionary],
applicationTitle = [options objectForKey:"ApplicationName"] || [mainInfo objectForKey:@"CPBundleName"],
applicationIcon = [options objectForKey:@"ApplicationIcon"] || [self applicationIconImage],
version = [options objectForKey:@"Version"] || [mainInfo objectForKey:@"CPBundleVersion"],
applicationVersion = [options objectForKey:@"ApplicationVersion"] || [mainInfo objectForKey:@"CPBundleShortVersionString"],
copyright = [options objectForKey:@"Copyright"] || [mainInfo objectForKey:@"CPHumanReadableCopyright"];
var aboutPanelPath = [[CPBundle bundleForClass:[CPWindowController class]] pathForResource:@"AboutPanel.cib"],
aboutPanelController = [CPWindowController alloc],
aboutPanelController = [aboutPanelController initWithWindowCibPath:aboutPanelPath owner:aboutPanelController],
aboutPanel = [aboutPanelController window],
contentView = [aboutPanel contentView],
imageView = [contentView viewWithTag:1],
applicationLabel = [contentView viewWithTag:2],
versionLabel = [contentView viewWithTag:3],
copyrightLabel = [contentView viewWithTag:4],
standardPath = [[CPBundle bundleForClass:[self class]] pathForResource:@"standardApplicationIcon.png"];
// FIXME move this into the CIB eventually
[applicationLabel setFont:[CPFont boldSystemFontOfSize:[CPFont systemFontSize] + 2]];
[applicationLabel setAlignment:CPCenterTextAlignment];
[versionLabel setAlignment:CPCenterTextAlignment];
[copyrightLabel setAlignment:CPCenterTextAlignment];
[imageView setImage:applicationIcon || [[CPImage alloc] initWithContentsOfFile:standardPath
size:CGSizeMake(256, 256)]];
[applicationLabel setStringValue:applicationTitle || ""];
if (applicationVersion && version)
[versionLabel setStringValue:@"Version " + applicationVersion + " (" + version + ")"];
else if (applicationVersion || version)
[versionLabel setStringValue:@"Version " + (applicationVersion || version)];
else
[versionLabel setStringValue:@""];
[copyrightLabel setStringValue:copyright || ""];
[aboutPanel center];
_aboutPanel = aboutPanel;
}
[_aboutPanel orderFront:self];
}
- (void)_documentController:(CPDocumentController)docController didCloseAll:(BOOL)didCloseAll context:(Object)info
{
// callback method for terminate:
if (didCloseAll)
{
if ([_delegate respondsToSelector:@selector(applicationShouldTerminate:)])
[self replyToApplicationShouldTerminate:[_delegate applicationShouldTerminate:self]];
else
[self replyToApplicationShouldTerminate:YES];
}
}
- (void)replyToApplicationShouldTerminate:(BOOL)terminate
{
if (terminate == CPTerminateNow)
{
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationWillTerminateNotification object:self];
[CPPlatform terminateApplication];
}
}
- (void)activateIgnoringOtherApps:(BOOL)shouldIgnoreOtherApps
{
[self _willBecomeActive];
[CPPlatform activateIgnoringOtherApps:shouldIgnoreOtherApps];
_isActive = YES;
[self _willResignActive];
}
- (void)deactivate
{
[self _willResignActive];
[CPPlatform deactivate];
_isActive = NO;
[self _didResignActive];
}
- (void)isActive
{
return _isActive;
}
- (void)hideOtherApplications:(id)aSender
{
[CPPlatform hideOtherApplications:self];
}
/*!
Calls \c -finishLaunching method which results in starting
the main event loop.
*/
- (void)run
{
[self finishLaunching];
}
// Managing the Event Loop
/*!
Starts a modal event loop for \c aWindow
@param aWindow the window to start the event loop for
*/
- (void)runModalForWindow:(CPWindow)aWindow
{
[self runModalSession:[self beginModalSessionForWindow:aWindow]];
}
/*!
Stops the event loop started by \c -runModalForWindow: and
sets the code that \c -runModalForWindow: will return.
@param aCode the return code for the modal event
*/
- (void)stopModalWithCode:(int)aCode
{
if (!_currentSession)
{
return;
// raise exception;
}
_currentSession._state = aCode;
_currentSession = _currentSession._previous;
// if (aCode == CPRunAbortedResponse)
[self _removeRunModalLoop];
}
/* @ignore */
- (void)_removeRunModalLoop
{
var count = _eventListeners.length;
while (count--)
if (_eventListeners[count]._callback === _CPRunModalLoop)
{
_eventListeners.splice(count, 1);
return;
}
}
/*!
Stops the modal event loop
*/
- (void)stopModal
{
[self stopModalWithCode:CPRunStoppedResponse]
}
/*!
Aborts the event loop started by \c -runModalForWindow:
*/
- (void)abortModal
{
[self stopModalWithCode:CPRunAbortedResponse];
}
/*!
Sets up a modal session with \c theWindow.
@param aWindow the window to set up the modal session for
*/
- (CPModalSession)beginModalSessionForWindow:(CPWindow)aWindow
{
return _CPModalSessionMake(aWindow, 0);
}
/*!
Runs a modal session
@param CPModalSession the session to run
*/
- (void)runModalSession:(CPModalSession)aModalSession
{
aModalSession._previous = _currentSession;
_currentSession = aModalSession;
var theWindow = aModalSession._window;
[theWindow center];
[theWindow makeKeyWindow];
[theWindow orderFront:self];
// [theWindow._bridge _obscureWindowsBelowModalWindow];
[CPApp setCallback:_CPRunModalLoop forNextEventMatchingMask:CPAnyEventMask untilDate:nil inMode:0 dequeue:YES];
}
/*!
Returns the window for the current modal session. If there is no
modal session, it returns \c nil.
*/
- (CPWindow)modalWindow
{
if (!_currentSession)
return nil;
return _currentSession._window;
}
/* @ignore */
- (BOOL)_handleKeyEquivalent:(CPEvent)anEvent
{
return [[self keyWindow] performKeyEquivalent:anEvent] ||
[[self mainMenu] performKeyEquivalent:anEvent];
}
/*!
Dispatches events to other objects.
@param anEvent the event to dispatch
*/
- (void)sendEvent:(CPEvent)anEvent
{
_currentEvent = anEvent;
CPEventModifierFlags = [anEvent modifierFlags];
var theWindow = [anEvent window];
#if PLATFORM(DOM)
var willPropagate = [[theWindow platformWindow] _willPropagateCurrentDOMEvent];
// temporarily pretend we won't propagate the event. we'll restore the saved value later
// we do this outside the if so that changes user code might make in _handleKeyEquiv. are preserved
[[theWindow platformWindow] _propagateCurrentDOMEvent:NO];
#endif
// Check if this is a candidate for key equivalent...
if ([anEvent _couldBeKeyEquivalent] && [self _handleKeyEquivalent:anEvent])
{
#if PLATFORM(DOM)
var characters = [anEvent characters],
modifierFlags = [anEvent modifierFlags];
// Unconditionally propagate on these keys to solve browser copy paste bugs
if ((characters == "c" || characters == "x" || characters == "v") && (modifierFlags & CPPlatformActionKeyMask))
[[theWindow platformWindow] _propagateCurrentDOMEvent:YES];
#endif
return;
}
#if PLATFORM(DOM)
// if we make it this far, then restore the original willPropagate value
[[theWindow platformWindow] _propagateCurrentDOMEvent:willPropagate];
#endif
if ([anEvent type] == CPMouseMoved)
{
if (theWindow !== _lastMouseMoveWindow)
[_lastMouseMoveWindow _mouseExitedResizeRect];
_lastMouseMoveWindow = theWindow;
}
if (_eventListeners.length)
{
var listener = _eventListeners[_eventListeners.length - 1];
if (listener._mask & (1 << [anEvent type]))
{
_eventListeners.pop();
listener._callback(anEvent);
}
/*
FIXME: This does not match Cocoa documented behavior for the dequeue
flag. Cocoa says the event is dequeued only if it matches the mask.
Unfortunately event handling code in Cappuccino is depending
on an event being dequeued even if it does not match.
*/
if (listener._dequeue)
return;
}
if (theWindow)
[theWindow sendEvent:anEvent];
}
/*!
If the delegate responds to the given selector it will call the method on the delegate,
otherwise the method will be passed to CPResponder.
*/
- (void)doCommandBySelector:(SEL)aSelector
{
if ([_delegate respondsToSelector:aSelector])
[_delegate performSelector:aSelector];
else
[super doCommandBySelector:aSelector];
}
/*!
Returns the key window.
*/
- (CPWindow)keyWindow
{
return _keyWindow;
}
/*!
Returns the main window.
*/
- (CPWindow)mainWindow
{
return _mainWindow;
}
/*!
Returns the CPWindow object corresponding to \c aWindowNumber.
*/
- (CPWindow)windowWithWindowNumber:(int)aWindowNumber
{
// Never allow _windows[0] to be returned - it's an internal CPNull placeholder.
if (!aWindowNumber)
return nil;
return _windows[aWindowNumber];
}
/*!
Returns an array of the application's CPWindows
*/
- (CPArray)windows
{
// Return all windows, but not the CPNull placeholder in _windows[0].
return [_windows subarrayWithRange:_CPMakeRange(1, [_windows count] - 1)];
}
/*!
Returns an array of visible CPWindow objects, ordered by their front to back order on the screen.
*/
- (CPArray)orderedWindows
{
#if PLATFORM(DOM)
return CPWindowObjectList();
#else
return [];
#endif
}
- (void)hide:(id)aSender
{
[CPPlatform hide:self];
}
// Accessing the Main Menu
/*!
Returns the application's main menu
*/
- (CPMenu)mainMenu
{
return [self menu];
}
/*!
Sets the main menu for the application
@param aMenu the menu to set for the application
*/
- (void)setMainMenu:(CPMenu)aMenu
{
[self setMenu:aMenu];
}
- (void)setMenu:(CPMenu)aMenu
{
if ([aMenu _menuName] === "CPMainMenu")
{
if ([self menu] === aMenu)
return;
[super setMenu:aMenu];
if ([CPPlatform supportsNativeMainMenu])
window.cpSetMainMenu([self menu]);
}
else
[aMenu _setMenuName:@"CPMainMenu"];
}
/*!
Opens the shared color panel.
@param aSender
*/
- (void)orderFrontColorPanel:(id)aSender
{
[[CPColorPanel sharedColorPanel] orderFront:self];
}
// Posting Actions
/*!
Tries to perform the action with an argument. Performs
the action on itself (if it responds to it), then
tries to perform the action on the delegate.
@param anAction the action to perform.
@param anObject the argument for the action
method
@return \c YES if the action was performed
*/
- (BOOL)tryToPerform:(SEL)anAction with:(id)anObject
{
if (!anAction)
return NO;
if ([super tryToPerform:anAction with:anObject])
return YES;
if ([_delegate respondsToSelector:anAction])
{
[_delegate performSelector:anAction withObject:anObject];
return YES;
}
return NO;
}
/*!
Sends an action to a target.
@param anAction the action to send
@param aTarget the target for the action
@param aSender the action sender
@return \c YES
*/
- (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)aSender
{
var target = [self targetForAction:anAction to:aTarget from:aSender];
if (!target)
return NO;
[target performSelector:anAction withObject:aSender];
return YES;
}
/*!
Finds a target for the specified action. If the
action is \c nil, returns \c nil.
If the target is not \c nil, \c aTarget is
returned. Otherwise, it calls \c -targetForAction:
to search for a target.
@param anAction the action to find a target for
@param aTarget if not \c nil, this will be returned
@aSender not used
@return a target for the action
*/
- (id)targetForAction:(SEL)anAction to:(id)aTarget from:(id)aSender
{
if (!anAction)
return nil;
if (aTarget)
return aTarget;
return [self targetForAction:anAction];
}
/*!
Finds an action-target for a specified window.
It finds a matching target in the following order:
<ol>
<li>the window's first responder</li>
<li>all the next responders after first responder</li>
<li>the window</li>
<li>the window's delegate</li>
<li>the window's controller</li>
<li>the window's associated document</li>
</ol>
@param aWindow the window to search for a target
@param anAction the action to find a responder to
@return the object that responds to the action, or \c nil
if no matching target was found
@ignore
*/
- (id)_targetForWindow:(CPWindow)aWindow action:(SEL)anAction
{
var responder = [aWindow firstResponder],
checkWindow = YES;
while (responder)
{
if ([responder respondsToSelector:anAction])
return responder;
if (responder == aWindow)
checkWindow = NO;
responder = [responder nextResponder];
}
if (checkWindow && [aWindow respondsToSelector:anAction])
return aWindow;
var delegate = [aWindow delegate];
if ([delegate respondsToSelector:anAction])
return delegate;
var windowController = [aWindow windowController];
if ([windowController respondsToSelector:anAction])
return windowController;
var theDocument = [windowController document];
if (theDocument !== delegate && [theDocument respondsToSelector:anAction])
return theDocument;
return nil;
}
/*!
Looks for a target that can handle the specified action.
Checks for a target in the following order:
<ol>
<li>a responder from the key window</li>
<li>a responder from the main window</li>
<li>the CPApplication instance</li>
<li>the application delegate</li>
<li>the document controller</li>
</ol>
@param anAction the action to handle
@return a target that can respond, or \c nil
if no match could be found
*/
- (id)targetForAction:(SEL)anAction
{
if (!anAction)
return nil;
var target = [self _targetForWindow:[self keyWindow] action:anAction];
if (target)
return target;
target = [self _targetForWindow:[self mainWindow] action:anAction];
if (target)
return target;
if ([self respondsToSelector:anAction])
return self;
if ([_delegate respondsToSelector:anAction])
return _delegate;
if ([_documentController respondsToSelector:anAction])
return _documentController;
return nil;
}
/*!
Fires a callback function when an event matching a given mask occurs.
@param aCallback A js function to be fired.
@prarm aMask An event mask for the next event.
@param anExpiration The date for which this callback expires (not implemented).
@param inMode (not implemented).
@param shouldDequeue YES to remove the event from the queue after calling the callback,
NO to deliver it normally.
*/
- (void)setCallback:(Function)aCallback forNextEventMatchingMask:(unsigned int)aMask untilDate:(CPDate)anExpiration inMode:(CPString)aMode dequeue:(BOOL)shouldDequeue
{
_eventListeners.push(_CPEventListenerMake(aMask, aCallback, shouldDequeue));
}
/*!
Assigns a target and action for the next event matching a given event mask.
The callback method called will be passed the CPEvent when it fires.
@param aTarget The target object for the callback.
@param aSelector The selector which should be called on the target object.
@param aMask The mask for a given event which should trigger the callback.
@param anExpiration The date for which the callback expires (not implemented).
@param aMode (not implemented).
@param shouldDequeue YES to remove the event from the queue after calling the callback,
NO to deliver it normally.
*/
- (void)setTarget:(id)aTarget selector:(SEL)aSelector forNextEventMatchingMask:(unsigned int)aMask untilDate:(CPDate)anExpiration inMode:(CPString)aMode dequeue:(BOOL)shouldDequeue
{
_eventListeners.push(_CPEventListenerMake(aMask, function (anEvent) { objj_msgSend(aTarget, aSelector, anEvent); }, shouldDequeue));
}
/*!
Returns the last event recieved by your application.
*/
- (CPEvent)currentEvent
{
return _currentEvent;
}
// Managing Sheets
/*!
Displays a window as a sheet.
@param aSheet the window to display as a sheet
@param aWindow the window that will hold the sheet as a child
@param aModalDelegate
@param aDidEndSelector
@param aContextInfo
*/
- (void)beginSheet:(CPWindow)aSheet modalForWindow:(CPWindow)aWindow modalDelegate:(id)aModalDelegate didEndSelector:(SEL)aDidEndSelector contextInfo:(id)aContextInfo
{
if ([aWindow isSheet])
{
[CPException raise:CPInternalInconsistencyException reason:@"The target window of beginSheet: cannot be a sheet"];
return;
}
[aSheet._windowView _enableSheet:YES];
// -dw- if a sheet is already visible, we skip this since it serves no purpose and causes
// orderOut: to be called on the sheet, which is not what we want.
if (![aWindow isVisible])
{
[aWindow orderFront:self];
[aSheet setPlatformWindow:[aWindow platformWindow]];
}
[aWindow _attachSheet:aSheet modalDelegate:aModalDelegate didEndSelector:aDidEndSelector contextInfo:aContextInfo];
}
/*!
Ends a sheet modal.
The following are predefined return codes:
<pre>
CPRunStoppedResponse
CPRunAbortedResponse
CPRunContinuesResponse
</pre>
@param sheet - The window object (sheet) to dismiss.
@param returnCode - The return code to send to the delegate. You can use one of the return codes above or a custom value that you define.
*/
- (void)endSheet:(CPWindow)sheet returnCode:(int)returnCode
{
var count = [_windows count];
while (--count >= 0)
{
var aWindow = [_windows objectAtIndex:count],
context = aWindow._sheetContext;
if (context != nil && context["sheet"] === sheet)
{
context["returnCode"] = returnCode;
[aWindow _endSheet];
return;
}
}
}
/*!
Ends a sheet and sends the return code "0".
@param sheet - The CPWindow object (sheet) that should be dismissed.
*/
- (void)endSheet:(CPWindow)sheet
{
// FIX ME: this is wrong: by Cocoa this should be: CPRunStoppedResponse.
[self endSheet:sheet returnCode:0];
}
/*!
Returns and array of slash seperated arugments to your application.
These values are pulled from your window location hash.
For exampled if your application loaded:
<pre>
index.html#280north/cappuccino/issues
</pre>
The follow array would be returned:
<pre>
["280north", "cappuccino", "issues"]
</pre>
@return CPArray - The array of arguments.
*/
- (CPArray)arguments
{
// FIXME This should probably not access the window object #if !PLATFORM(DOM), but the unit tests rely on it.
if (window && window.location && _fullArgsString !== window.location.hash)
[self _reloadArguments];
return _args;
}
/*!
Sets the arguments of your application.
That is, set the slash seperated values of an array as the window location hash.
For example if you pass an array:
<pre>
["280north", "cappuccino", "issues"]
</pre>
The new window location would be
<pre>
index.html#280north/cappuccino/issues
</pre>
@param args - An array of arguments.
*/
- (void)setArguments:(CPArray)args
{
if (!args || args.length == 0)
{
_args = [];
// Don't use if PLATFORM(DOM) here - the unit test fakes window.location so we should play along.
if (window && window.location)
window.location.hash = @"#";
return;
}
if (![args isKindOfClass:CPArray])
args = [CPArray arrayWithObject:args];
_args = args;
var toEncode = [_args copy];
for (var i = 0, count = toEncode.length; i < count; i++)
toEncode[i] = encodeURIComponent(toEncode[i]);
var hash = [toEncode componentsJoinedByString:@"/"];
// Don't use if PLATFORM(DOM) here - the unit test fakes window.location so we should play along.
if (window && window.location)
window.location.hash = @"#" + hash;
}
- (void)_reloadArguments
{
// FIXME This should probably not access the window object #if !PLATFORM(DOM), but the unit tests rely on it.
_fullArgsString = (window && window.location) ? window.location.hash : "";
if (_fullArgsString.length)
{
var args = _fullArgsString.substring(1).split("/");
for (var i = 0, count = args.length; i < count; i++)
args[i] = decodeURIComponent(args[i]);
_args = args;
}
else
_args = [];
}
/*!
Returns a dictionary of the window location named arguments.
For example if your location was:
<pre>
index.html?owner=280north&repo=cappuccino&type=issues
</pre>
a CPDictionary with the keys:
<pre>
owner, repo, type
</pre>
and respective values:
<pre>
280north, cappuccino, issues
</pre>
Will be returned.
*/
- (CPDictionary)namedArguments
{
return _namedArgs;
}
- (BOOL)_openURL:(CPURL)aURL
{
if (_delegate && [_delegate respondsToSelector:@selector(application:openFile:)])
{
CPLog.warn("application:openFile: is deprecated, use application:openURL: instead.");
return [_delegate application:self openFile:[aURL absoluteString]];
}
if (_delegate && [_delegate respondsToSelector:@selector(application:openURL:)])
return [_delegate application:self openURL:aURL];
return !![_documentController openDocumentWithContentsOfURL:aURL display:YES error:NULL];
}
- (void)_willBecomeActive
{
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationWillBecomeActiveNotification
object:self
userInfo:nil];
}
- (void)_didBecomeActive
{
if (![self keyWindow] && _previousKeyWindow &&
[[self windows] indexOfObjectIdenticalTo:_previousKeyWindow] !== CPNotFound)
[_previousKeyWindow makeKeyWindow];
if (![self mainWindow] && _previousMainWindow &&
[[self windows] indexOfObjectIdenticalTo:_previousMainWindow] !== CPNotFound)
[_previousMainWindow makeMainWindow];
if ([self keyWindow])
[[self keyWindow] orderFront:self];
else if ([self mainWindow])
[[self mainWindow] makeKeyAndOrderFront:self];
else
[[self mainMenu]._menuWindow makeKeyWindow]; //FIXME this may not actually work
_previousKeyWindow = nil;
_previousMainWindow = nil;
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationDidBecomeActiveNotification
object:self
userInfo:nil];
}
- (void)_willResignActive
{
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationWillResignActiveNotification
object:self
userInfo:nil];
}
- (void)_didResignActive
{
if (self._activeMenu)
[self._activeMenu cancelTracking];
if ([self keyWindow])
{
_previousKeyWindow = [self keyWindow];
[_previousKeyWindow resignKeyWindow];
}
if ([self mainWindow])
{
_previousMainWindow = [self mainWindow];
[_previousMainWindow resignMainWindow];
}
[[CPNotificationCenter defaultCenter] postNotificationName:CPApplicationDidResignActiveNotification
object:self
userInfo:nil];
}
+ (CPString)defaultThemeName
{
return ([[CPBundle mainBundle] objectForInfoDictionaryKey:"CPDefaultTheme"] || @"Aristo2");
}
@end
var _CPModalSessionMake = function(aWindow, aStopCode)
{
return { _window:aWindow, _state:CPRunContinuesResponse , _previous:nil };
};
var _CPEventListenerMake = function(anEventMask, aCallback, shouldDequeue)
{
return { _mask:anEventMask, _callback:aCallback, _dequeue:shouldDequeue };
};
// Make this a global for use in CPPlatformWindow+DOM.j.
_CPRunModalLoop = function(anEvent)
{
[CPApp setCallback:_CPRunModalLoop forNextEventMatchingMask:CPAnyEventMask untilDate:nil inMode:0 dequeue:YES];
var theWindow = [anEvent window],
modalSession = CPApp._currentSession;
/*
The special case for popovers here is not clear. In Cocoa the popover window does not respond YES to worksWhenModal, yet it works when there is a modal window. Maybe it starts its own modal session, but interaction with the original modal window seems to continue working as well. Regardless of correctness, this solution beats popovers not working at all from sheets.
*/
if (theWindow == modalSession._window ||
[theWindow worksWhenModal] ||
[theWindow attachedSheet] == modalSession._window || // -dw- allow modal parent of sheet to be repositioned
([theWindow isKindOfClass:_CPPopoverWindow] && [[theWindow targetView] window] === modalSession._window))
{
[theWindow sendEvent:anEvent];
}
};
/*!
Starts the GUI and Cappuccino frameworks. This function should be
called from the \c main() function of your program.
@class CPApplication
@return void
*/
function CPApplicationMain(args, namedArgs)
{
#if PLATFORM(DOM)
// hook to allow recorder, etc to manipulate things before starting AppKit
if (window.parent !== window && typeof window.parent._childAppIsStarting === "function")
{
try
{
window.parent._childAppIsStarting(window);
}
catch(err)
{
// This could happen if we're in an iframe without access to the parent frame.
CPLog.warn("Failed to call parent frame's _childAppIsStarting().");
}
}
#endif
var mainBundle = [CPBundle mainBundle],
principalClass = [mainBundle principalClass];
if (!principalClass)
principalClass = [CPApplication class];
[principalClass sharedApplication];
if ([args containsObject:"debug"])
CPLogRegister(CPLogPopup);
CPApp._args = args;
CPApp._namedArgs = namedArgs;
[_CPAppBootstrapper performActions];
}
var _CPAppBootstrapperActions = nil;
@implementation _CPAppBootstrapper : CPObject
{
}
+ (CPArray)actions
{
return [@selector(bootstrapPlatform), @selector(loadDefaultTheme), @selector(loadMainCibFile)];
}
+ (void)performActions
{
if (!_CPAppBootstrapperActions)
_CPAppBootstrapperActions = [self actions];
while (_CPAppBootstrapperActions.length)
{
var action = _CPAppBootstrapperActions.shift();
if (objj_msgSend(self, action))
return;
}
[CPApp run];
}
+ (BOOL)bootstrapPlatform
{
return [CPPlatform bootstrap];
}
+ (BOOL)loadDefaultTheme
{
var defaultThemeName = [CPApplication defaultThemeName],
themeURL = nil;
if (defaultThemeName === @"Aristo" || defaultThemeName === @"Aristo2")
themeURL = [[CPBundle bundleForClass:[CPApplication class]] pathForResource:defaultThemeName + @".blend"];
else
themeURL = [[CPBundle mainBundle] pathForResource:defaultThemeName + @".blend"];
var blend = [[CPThemeBlend alloc] initWithContentsOfURL:themeURL];
[blend loadWithDelegate:self];
return YES;
}
+ (void)blendDidFinishLoading:(CPThemeBlend)aThemeBlend
{
[[CPApplication sharedApplication] setThemeBlend:aThemeBlend];
[CPTheme setDefaultTheme:[CPTheme themeNamed:[CPApplication defaultThemeName]]];
[self performActions];
}
+ (BOOL)loadMainCibFile
{
var mainBundle = [CPBundle mainBundle],
mainCibFile = [mainBundle objectForInfoDictionaryKey:CPMainCibFile] || [mainBundle objectForInfoDictionaryKey:CPMainCibFileHumanFriendly];
if (mainCibFile)
{
[mainBundle loadCibFile:mainCibFile
externalNameTable:@{ CPCibOwner: CPApp }
loadDelegate:self];
return YES;
}
else
[self loadCiblessBrowserMainMenu];
return NO;
}
+ (void)loadCiblessBrowserMainMenu
{
var mainMenu = [[CPMenu alloc] initWithTitle:@"MainMenu"];
// FIXME: We should implement autoenabling.
[mainMenu setAutoenablesItems:NO];
var newMenuItem = [[CPMenuItem alloc] initWithTitle:@"New" action:@selector(newDocument:) keyEquivalent:@"n"];
[newMenuItem setImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-new" forClass:_CPMenuView]];
[newMenuItem setAlternateImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-new" inState:CPThemeStateHighlighted forClass:_CPMenuView]];
[mainMenu addItem:newMenuItem];
var openMenuItem = [[CPMenuItem alloc] initWithTitle:@"Open" action:@selector(openDocument:) keyEquivalent:@"o"];
[openMenuItem setImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-open" forClass:_CPMenuView]];
[openMenuItem setAlternateImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-open" inState:CPThemeStateHighlighted forClass:_CPMenuView]];
[mainMenu addItem:openMenuItem];
var saveMenu = [[CPMenu alloc] initWithTitle:@"Save"],
saveMenuItem = [[CPMenuItem alloc] initWithTitle:@"Save" action:@selector(saveDocument:) keyEquivalent:nil];
[saveMenuItem setImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-save" forClass:_CPMenuView]];
[saveMenuItem setAlternateImage:[[CPTheme defaultTheme] valueForAttributeWithName:@"menu-general-icon-save" inState:CPThemeStateHighlighted forClass:_CPMenuView]];
[saveMenu addItem:[[CPMenuItem alloc] initWithTitle:@"Save" action:@selector(saveDocument:) keyEquivalent:@"s"]];
[saveMenu addItem:[[CPMenuItem alloc] initWithTitle:@"Save As" action:@selector(saveDocumentAs:) keyEquivalent:nil]];
[saveMenuItem setSubmenu:saveMenu];
[mainMenu addItem:saveMenuItem];
var editMenuItem = [[CPMenuItem alloc] initWithTitle:@"Edit" action:nil keyEquivalent:nil],
editMenu = [[CPMenu alloc] initWithTitle:@"Edit"],
undoMenuItem = [[CPMenuItem alloc] initWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:CPUndoKeyEquivalent],
redoMenuItem = [[CPMenuItem alloc] initWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:CPRedoKeyEquivalent];
[undoMenuItem setKeyEquivalentModifierMask:CPUndoKeyEquivalentModifierMask];
[redoMenuItem setKeyEquivalentModifierMask:CPRedoKeyEquivalentModifierMask];
[editMenu addItem:undoMenuItem];
[editMenu addItem:redoMenuItem];
[editMenu addItem:[[CPMenuItem alloc] initWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"]];
[editMenu addItem:[[CPMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]];
[editMenu addItem:[[CPMenuItem alloc] initWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]];
[editMenuItem setSubmenu:editMenu];
[editMenuItem setHidden:YES];
[mainMenu addItem:editMenuItem];
[mainMenu addItem:[CPMenuItem separatorItem]];
[CPApp setMainMenu:mainMenu];
}
+ (void)cibDidFinishLoading:(CPCib)aCib
{
[self performActions];
}
+ (void)cibDidFailToLoad:(CPCib)aCib
{
throw new Error("Could not load main cib file (Did you forget to nib2cib it?).");
}
+ (void)reset
{
_CPAppBootstrapperActions = nil;
}
@end
@implementation CPEvent (CPApplicationModifierFlags)
/*!
Returns the currently pressed modifier flags.
*/
+ (unsigned)modifierFlags
{
return CPEventModifierFlags;
}
@end