blossom
Version:
Modern, Cross-Platform Application Framework
1,410 lines (1,128 loc) • 60.8 kB
JavaScript
// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2010 Apple Inc. All rights reserved.
// Code within if (BLOSSOM) {} sections is ©2012 Fohr Motion
// Picture Studios. All rights reserved.
// License: Most code licensed under MIT license (see SPROUTCORE-LICENSE).
// Code within if (BLOSSOM) {} sections is under GPLv3 license
// (see BLOSSOM-LICENSE).
// ==========================================================================
/*globals sc_assert */
sc_require('system/responder');
sc_require('ext/float32');
sc_require('surfaces/surface');
sc_require('surfaces/container');
sc_require('surfaces/private/ptransition_animation');
/** Set to false to leave the backspace key under the control of the browser.*/
SC.CAPTURE_BACKSPACE_KEY = false ;
SC.needsLayout = false;
SC.needsRendering = false;
/** @class
This class is the brains behind a Blossom application. You must create
exactly one instance of `SC.Application` somewhere in your code.
This instance will be available to you at `SC.app` automatically:
SC.Application.create(); // instance is stored at `SC.app`
You can also store it there explicitly:
SC.app = SC.Application.create(); // also okay
Managing Surfaces
-----------------
`SC.Application` manages the surfaces that are present in your app's
viewport at any given time. To add a surface to the viewport, do:
SC.app.addSurface(aSurface);
To remove a surface from the viewport, do:
SC.app.removeSurface(aSurface);
You can also modify the `surfaces` property directly:
SC.app.get('surfaces').add(aSurface);
SC.app.get('surfaces').remove(aSurface);
Surfaces added to the `surfaces` set, or using `add|removeSurface` play no
special role in the application. You can also add surfaces for particular
roles, the most common of which is the `ui` role:
SC.app.set('ui', aSurface);
The surface representing the app's user interface ("ui") has its layout set
to the viewport automatically. (This surface is *not* added to the app's
`surfaces` set, and it is removed if it's there.) (You should not add the
`ui` surface to the `surfaces` set later; doing so will result in an
assertion failure.)
You can also assign an `inputSurface` that receives text input events
before the `ui` surface is given a chance to respond:
SC.app.set('inputSurface', aSurface);
A `menuSurface` receives text input and keyboard shortcuts before both the
`inputSurface` and `ui` surface have had a chance to respond:
SC.app.set('menuSurface', aSurface);
See "Dispatching Events" below for detailed documentation on how these
surfaces are used by `SC.Application`.
If you remove a surface that is currently either the `menuSurface` or the
`inputSurface`, that surface will be removed and the corresponding property
set to null.
Dispatching Events
------------------
`SC.Application` routes five kinds of events to surfaces:
- **Mouse events.** These are routed to the surface the event occured on.
- **Input events.** These are sent to the `inputSurface`.
- **Viewport events.** When the viewport resizes, each surface will have
its `viewportSizeDidChange` method called, if it implements it.
- **Keyboard shortcuts.** Shortcuts are first sent to the `menuSurface`,
if it exists. If unhandled, the `inputSurface` is a given a chance to
act on the shortcut. Finally, if the shortcut is still unhandled, the
`ui` surface will be given a chance to handle it.
- **Actions.** Actions are generic messages that your application can send
in response to user action or other events. You can either specify a
responder to target, or if not specified, the `ui` surface's
`firstResponder` is made the target. The target is then given the chance
to handle the action, and if not handled, the action moves up the
responder chain until a responder is found that does handle it.
Animated UI Transitions
-----------------------
When the `ui` property is set, one of three animated transitions will
apply. See the documentation for the `ui` property for more information on
how to configure these animations, or turn them off completely.
@extends SC.Responder
@extends SC.DelegateSupport
@since Blossom 1.0
*/
SC.Application = SC.Object.extend(SC.Responder, SC.DelegateSupport,
/** SC.Application.prototype */ {
isApp: true, // Walk like a duck.
isResponderContext: true, // We can dispatch events and actions.
init: function() {
arguments.callee.base.apply(this, arguments);
this.set('surfaces', SC.Set.create());
sc_assert(SC.app === undefined, "You can only create one instance of SC.Application.");
SC.app = this;
SC.ready(this, this.awake);
},
// .......................................................
// LAYOUT AND RENDERING
//
performLayoutAndRendering: function(timestamp) {
// SC.LOG_OBSERVERS = SC.LOG_BINDINGS = true;
// debugger;
// console.log('SC.Application#performLayoutAndRendering()');
// console.log('==========================================');
sc_assert(SC.app === this, "SC.Application#performLayoutAndRendering() called with this != SC.app.");
// sc_assert(!SC.isAnimating, "SC.Application#performLayoutAndRendering() called when SC.isAnimating is true (should be false).");
var benchKey = 'SC.Application#performLayoutAndRendering()';
SC.Benchmark.start(benchKey);
var surfaces = null;
if (SC.surfacesHashNeedsUpdate) {
SC.surfacesHashNeedsUpdate = false;
SC.surfaces = surfaces = {};
}
var uiContainer = this.get('uiContainer');
if (SC.viewportSizeDidChange) {
var sz = this.computeViewportSize(),
frame = uiContainer.get('frame');
// Avoid needless updates.
if (frame[2]/*width*/ !== sz[0] || frame[3]/*height*/ !== sz[1]) {
SC.AnimationTransaction.begin({ duration: 0 });
uiContainer.set('frame', SC.MakeRect(0, 0, sz[0]/*w*/, sz[1]/*h*/));
SC.AnimationTransaction.end();
}
this.get('surfaces').invoke('viewportSizeDidChange', sz);
}
if (SC.needsLayout) {
uiContainer.performLayoutIfNeeded(timestamp);
this.get('surfaces').invoke('performLayoutIfNeeded', timestamp);
// TODO: Run the global constraint solver now.
}
this.updatePsurfaces(surfaces);
if (SC.needsRendering) {
uiContainer.performRenderingIfNeeded(timestamp);
this.get('surfaces').invoke('performRenderingIfNeeded', timestamp);
}
// Set these to false last, we don't want to accidently trigger another
// call to this method on the next run loop.
SC.needsLayout = false;
SC.needsRendering = false;
SC.viewportSizeDidChange = false;
SC.surfaceTransitions = {}; // Reset!
// SC.surfaceAnimations = {}; // Reset!
SC.Benchmark.end(benchKey);
// FIXME: Ideally address this warning at some point.
// if (!SC.RunLoop.currentRunLoop.flushApplicationQueues()) {
// console.log("The run loop should not be needed during layout and rendering.");
// }
SC.ScheduleLayoutAndRendering();
// SC.LOG_BINDINGS = SC.LOG_OBSERVERS = false;
},
// .......................................................
// SURFACE HANDLING
//
/** @property
Contains the set of all surfaces currently present in the viewport, and
that are not in the process of being added or removed from the viewport.
You can add surfaces to this set directly, or use the `addSurface` and
`removeSurface` helpers, which do the same thing but also allow a surface
transition to be specified.
You can also replace this set with an entirely new set of surfaces. If
you do, the current `ui` surface will be automatically added to the set
if not already present. For the `menuSurface` and `inputSurface`, these
properties will be set to `null` if the surface is *not* part of the new
surface set. Any surfaces currently transitioning in or out of the
viewport will be removed immediately.
When a surface is added, its `isPresentInViewport` property is set to
true, and when removed, it is set to false.
@type SC.Set<SC.Surface>
*/
surfaces: null,
/**
Adds a surface to the viewport.
@param {SC.Surface} surface
@param {SC.SurfaceTransition} transition (optional)
*/
addSurface: function(surface) {
var surfaces = this.get('surfaces');
sc_assert(surface && surface.kindOf(SC.Surface));
sc_assert(surface !== this.get('ui'), "Don't add SC.app@ui to the SC.app@surfaces set.");
// sc_assert(!surfaces.contains(surface));
surfaces.add(surface);
},
/**
Removes a surface from the viewport.
@param {SC.Surface} surface
@param {SC.SurfaceTransition} transition (optional)
*/
removeSurface: function(surface) {
var surfaces = this.get('surfaces');
sc_assert(surface && surface.kindOf(SC.Surface));
// sc_assert(surfaces.contains(surface));
surfaces.remove(surface);
},
/** @private */
didAddItem: function(set, surface) {
sc_assert(set === this.get('surfaces'));
sc_assert(surface.kindOf(SC.Surface));
sc_assert(surface !== this.get('ui'), "Don't add SC.app@ui to the SC.app@surfaces set.");
SC.surfacesHashNeedsUpdate = true; // causes the surfaces hash to recache
surface.setIfChanged('isPresentInViewport', true);
surface.setIfChanged('applicationHasFocus', this.get('hasFocus'));
// Some surfaces are created before the application is created, and the
// _sc_firstResponderDidChange() method accesses the SC.app instance. To
// handle this, surfaces that are created before SC.app exists set their
// `__sc_needFirstResponderInit__` property to true, leaving us
// responsible for triggering the surface's _sc_firstResponderDidChange()
// method.
if (surface.__sc_needFirstResponderInit__) {
surface.__sc_needFirstResponderInit__ = false;
surface._sc_firstResponderDidChange();
}
},
/** @private */
didRemoveItem: function(set, surface) {
sc_assert(set === this.get('surfaces'));
sc_assert(surface.kindOf(SC.Surface));
sc_assert(surface !== this.get('ui'), "You must not remove the 'ui' surface directly. Set the 'ui' property to null instead.");
surface.set('isPresentInViewport', false);
SC.surfacesHashNeedsUpdate = true; // causes the surfaces hash to recache
// If we remove a surface that is currently the menuSurface or
// inputSurface, set the correspoding property to null.
SC.Application.TRANSIENT_SURFACES.forEach(function(key) {
if (this.get(key) === surface) this.set(key, null);
}, this);
},
// When the surfaces property changes, we need to observe the new set for
// additions and removals.
_sc_surfacesDidChange: function() {
// console.log("SC.Surface#_sc_surfacesDidChange()");
var cur = this.get('surfaces'),
last = this._sc_surfaces,
ui = this.get('ui');
if (last === cur) return this; // nothing to do
sc_assert(cur && cur.isSet);
// Tear down old set observer and update surface status.
if (last) {
last.removeSetObserver(this);
last.forEach(function(surface) {
this.didRemoveItem(surface);
}, this);
}
// Save new set.
this._sc_surfaces = cur;
// `ui` should never be part of the `surfaces` set.
if (ui && cur.contains(ui)) cur.remove(ui);
// Set up new set observer and update their surface status.
if (cur) {
cur.addSetObserver(this);
cur.forEach(function(surface) {
this.didAddItem(surface);
}, this);
// `menuSurface` and `inputSurface` should be set to null if they are
// no longer present.
SC.Application.TRANSIENT_SURFACES.forEach(function(key) {
if (!cur.contains(this.get(key))) this.set(key, null);
}, this);
}
}.observes('surfaces'),
// .......................................................
// DISPLAY PROPERTIES
//
/** @property
The 3D persective property for the app's UI. You can override this on
individual surfaces if you want; otherwise, the surface will exist in
the same 3D space as the `ui` surface.
@type Integer
*/
perspective: 1000,
_sc_perspectiveDidChange: function() {
var perspective = this.get('perspective');
sc_assert(typeof perspective === "number");
sc_assert(Math.floor(perspective) === perspective); // Integral
document.body.style[SC.vendorPrefix+'Perspective'] = perspective+'px';
}.observes('perspective'),
// .......................................................
// UI SURFACE
//
/** @property
The app's user interface. This surface receives shortcuts and actions if
the `menuSurface` does not respond to them, and the `inputSurface` does
not respond to them. If will also receive text input events if a
separate `inputSurface` has not been defined; otherwise, it does not
receive text input events.
The `ui` surface's parent layout is the size of the viewport.
Animated, hardware-accelerated 3D transitions are available when changing
the 'ui' surface. There are three possible transitions:
- order in (defaults to SC.ENTER_LEFT)
- replace (defaults to SC.SLIDE_FLIP_LEFT)
- order out (defaults to SC.EXIT_RIGHT)
You can change the type of transition for each of these situations, and
that transition will be used whenever your app's 'ui' surface is changed.
@type SC.Surface or null
*/
ui: null,
uiOrderInTransition: SC.ENTER_LEFT,
uiReplaceTransition: SC.SLIDE_FLIP_LEFT,
uiOrderOutTransition: SC.EXIT_RIGHT,
/** @private */
uiContainer: function(key, value) {
sc_assert(value === undefined); // We're read only.
var uiContainer = this._sc_uiContainer;
if (!uiContainer) {
// Don't bind the `ui` property, we need to send some delegate methods
// before the container sees the change to the `ui` property.
uiContainer = this._sc_uiContainer = SC.ContainerSurface.create({
__id__: 'ui',
orderInTransition: this.get('uiOrderInTransition'),
orderInTransitionBinding: SC.Binding.from('uiOrderInTransition', this).oneWay().noDelay(),
replaceTransition: this.get('uiReplaceTransition'),
replaceTransitionBinding: SC.Binding.from('uiReplaceTransition', this).oneWay().noDelay(),
orderOutTransition: this.get('uiOrderOutTransition'),
orderOutTransitionBinding: SC.Binding.from('uiOrderOutTransition', this).oneWay().noDelay()
});
var sz = this.computeViewportSize();
// uiContainer.set('container', this);
uiContainer.set('isPresentInViewport', true);
}
return uiContainer;
}.property(),
bounds: function() {
var sz = this.computeViewportSize();
return SC.MakeRect(0, 0, sz[0]/*width*/, sz[1]/*height*/);
}.property(),
_sc_ui: null, // Note: Required, we're strict about null checking.
_sc_uiDidChange: function() {
var old = this._sc_ui,
cur = this.get('ui'),
uiContainer = this.get('uiContainer');
sc_assert(old === null || old.kindOf(SC.Surface), "Blossom internal error: SC.Application^_sc_ui is invalid.");
sc_assert(cur === null || cur.kindOf(SC.Surface), "SC.Application@ui must either be null or an SC.Surface instance.");
if (old === cur) return; // Nothing to do.
if (old && old.willLoseUserInterfaceTo) {
old.willLoseUserInterfaceTo(cur);
}
if (cur && cur.willBecomeUserInterfaceFrom) {
cur.willBecomeUserInterfaceFrom(old);
}
this._sc_ui = cur;
if (cur) this.removeSurface(cur);
uiContainer.set('contentSurface', cur);
if (old && old.didLoseUserInterfaceTo) {
old.didLoseUserInterfaceTo(cur);
}
if (cur && cur.didBecomeUserInterfaceFrom) {
cur.didBecomeUserInterfaceFrom(old);
}
}.observes('ui'),
// .......................................................
// INPUT SURFACE
//
/**
The current text input surface. This surface receives text input events,
shortcuts, and actions first, unless `menuSurface` is set, in which case
it only receives those events if the `menuSurface` does not handle them.
This surface is usually the highest ordered surface, or if not defined,
the `ui` surface will assume the input surface role automatically.
@type SC.Surface or null
*/
inputSurface: null,
_sc_inputSurface : null, // Note: Required, we're strict about null checking.
_sc_inputSurfaceDidChange: function() {
var old = this._sc_inputSurface,
cur = this.get('inputSurface');
sc_assert(old === null || old.kindOf(SC.Surface), "Blossom internal error: SC.Application^_sc_inputSurface is invalid.");
sc_assert(cur === null || cur.kindOf(SC.Surface), "SC.Application@inputSurface must either be null or an SC.Surface instance.");
if (old === cur) return; // Nothing to do.
if (old && old.willLoseInputSurfaceTo) old.willLoseInputSurfaceTo(cur);
if (cur && cur.willBecomeInputSurfaceFrom) cur.willBecomeInputSurfaceFrom(old);
if (old) old.set('isInputSurface', false);
this._sc_inputSurface = cur;
if (cur) cur.set('isInputSurface', true);
if (old && old.didLoseInputSurfaceTo) old.didLoseInputSurfaceTo(cur);
if (cur && cur.didBecomeInputSurfaceFrom) cur.didBecomeInputSurfaceFrom(old);
}.observes('inputSurface'),
// ..........................................................
// MENU SURFACE
//
/**
The current menu surface. This surface receives text input events before
any other surface, but tends to be transient, as it is usually only set
when a surface representing a "menu" is open.
@type SC.Surface or null
*/
menuSurface: null,
_sc_menuSurface : null, // Note: Required, we're strict about null checking.
_sc_menuSurfaceDidChange: function() {
var old = this._sc_menuSurface,
cur = this.get('menuSurface');
sc_assert(old === null || old.kindOf(SC.Surface), "Blossom internal error: SC.Application^_sc_menuSurface is invalid.");
sc_assert(cur === null || cur.kindOf(SC.Surface), "SC.Application@menuSurface must either be null or an SC.Surface instance.");
if (old === cur) return; // Nothing to do.
if (old && old.willLoseMenuSurfaceTo) old.willLoseMenuSurfaceTo(cur);
if (cur && cur.willBecomeMenuSurfaceFrom) cur.willBecomeMenuSurfaceFrom(old);
if (old) old.set('isMenuSurface', false);
this._sc_menuSurface = cur;
if (cur) cur.set('isMenuSurface', true);
if (old && old.didLoseMenuSurfaceTo) old.didLoseMenuSurfaceTo(cur);
if (cur && cur.didBecomeMenuSurfaceFrom) cur.didBecomeMenuSurfaceFrom(old);
}.observes('menuSurface'),
// ..........................................................
// PSURFACE SUPPORT (Private)
//
updatePsurfaces: function(surfaces) {
// console.log('SC.Application#updatePsurfaces()');
SC.Psurface.start();
this.get('uiContainer').updatePsurfaceTree(surfaces);
this.get('surfaces').invoke('updatePsurfaceTree', surfaces);
SC.Psurface.finish();
},
// ..........................................................
// VIEWPORT STATE
//
/**
The most-recently computed viewport size. Calling `computeViewportSize`
updates this value, and `SC.app` will also update this value whenever a
viewport change is detected.
@type SC.Size
@isReadOnly
*/
viewportSize: SC.MakeSize(0,0),
/**
Computes the viewport size. Also notifies surfaces if the computed value
has changed.
@returns SC.Size
*/
computeViewportSize: function() {
// TODO: Move to a shared buffer.
var old = this.get('viewportSize'),
cur = SC.MakeSize(window.innerWidth, window.innerHeight);
if (!SC.EqualSize(old, cur)) {
this.set('viewportSize', cur);
SC.viewportSizeDidChange = true;
// TODO: Update the constraint solver!
}
return cur;
},
/** @private
On viewport resize, requests layout and rendering.
@returns {Boolean}
*/
resize: function() {
this.computeViewportSize();
return true; // Allow normal processing to continue. FIXME: Is this correct?
},
/**
The most-recently computed orientation. Calling `computeOrientation`
updates this value, and `SC.app` will also update this value whenever an
orientation change is detected.
Note: Desktop web browsers always have a 'horizontal' orientation.
@type String either 'horizontal' or 'vertical'
@isReadOnly
*/
orientation: 'horizontal',
/**
Computes the viewport size. Also notifies surfaces if the computed value
has changed.
@returns SC.Size
*/
computeOrientation: function() {
// Desktop browsers are always 'horizontal'. Mobile clients support
// the 'vertical' orientation as well, computed with native code.
return 'horizontal';
},
// .......................................................
// FOCUS & BLUR SUPPORT
//
/**
Indicates whether or not the application currently has focus. If you
need to do something based on whether or not the application has focus,
you can set up a binding or observer to this property.
Surfaces will automatically have their `applicationHasFocus` property set
to this value when they are added, and whenever it changes.
@type Boolean
*/
hasFocus: false,
/** @private
Handles window focus events. Also notifies surfaces.
*/
focus: function() {
if (!this.get('hasFocus')) this.set('hasFocus', true);
this.get('surfaces').invoke('set', 'applicationHasFocus', true);
this.get('uiContainer').set('applicationHasFocus', true);
return true; // allow default
},
/** @private
Handles window blur events. Also notifies surfaces.
*/
blur: function() {
if (this.get('hasFocus')) this.set('hasFocus', false);
this.get('surfaces').invoke('set', 'applicationHasFocus', false);
this.get('uiContainer').set('applicationHasFocus', false);
return false; // allow default
},
// .......................................................
// THE FIELD EDITOR
//
/** @private
The current field editor. This object receives text input events first.
You don't set this directly, it is set internally when you click on an
SC.TextFieldWidget instance. (This API will be further improved in the
future to accommodate more use cases.)
@type SC.FieldEditor or null
*/
fieldEditor: null,
_sc_fieldEditor : null, // Note: Required, we're strict about null checking.
_sc_fieldEditorDidChange: function() {
var old = this._sc_fieldEditor,
cur = this.get('fieldEditor');
sc_assert(old === null || old.kindOf(SC.FieldEditor), "Blossom internal error: SC.Application^_sc_fieldEditor is invalid.");
sc_assert(cur === null || cur.kindOf(SC.FieldEditor), "SC.Application@fieldEditor must either be null or an SC.FieldEditor instance.");
if (old === cur) return; // Nothing to do.
this._sc_fieldEditor = cur;
}.observes('fieldEditor'),
// .......................................................
// ACTION HANDLING
//
dragDidStart: function(drag, evt) {
this._sc_mouseDownResponder = drag;
this._sc_drag = drag;
this._sc_dragEvent = evt;
},
/**
Set this to a delegate object that can respond to actions as they are sent
down the responder chain.
@type SC.Object
*/
defaultResponder: null,
/**
Route an action message to the appropriate responder. This method will
walk the responder chain, attempting to find a responder that implements
the action name you pass to this method. Set 'target' to null to search
the responder chain.
IMPORTANT: This method's API and implementation will likely change
significantly after SproutCore 1.0 to match the version found in
SC.ResponderContext.
You generally should not call or override this method in your own
applications.
@param {String} action The action to perform - this is a method name.
@param {SC.Responder} target object to set method to (can be null)
@param {Object} sender The sender of the action
@param {SC.Pane} pane optional pane to start search with
@param {Object} context optional. only passed to ResponderContexts
@returns {Boolean} true if action was performed, false otherwise
@test in targetForAction
*/
sendAction: function(action, target, sender, pane, context) {
target = this.targetForAction(action, target, sender, pane) ;
// HACK: If the target is a ResponderContext, forward the action.
if (target && target.isResponderContext) {
return !!target.sendAction(action, sender, context);
} else return target && target.tryToPerform(action, sender);
},
_sc_responderFor: function(target, methodName) {
var defaultResponder = target ? target.get('defaultResponder') : null;
if (target) {
target = target.get('firstResponder') || target;
do {
if (target.respondsTo(methodName)) return target ;
} while ((target = target.get('nextResponder'))) ;
}
// HACK: Eventually we need to normalize the sendAction() method between
// this and the ResponderContext, but for the moment just look for a
// ResponderContext as the defaultResponder and return it if present.
if (typeof defaultResponder === SC.T_STRING) {
defaultResponder = SC.objectForPropertyPath(defaultResponder);
}
if (!defaultResponder) return null;
else if (defaultResponder.isResponderContext) return defaultResponder;
else if (defaultResponder.respondsTo(methodName)) return defaultResponder;
else return null;
},
/**
Attempts to determine the initial target for a given action/target/sender
tuple. This is the method used by sendAction() to try to determine the
correct target starting point for an action before trickling up the
responder chain.
You send actions for user interface events and for menu actions.
This method returns an object if a starting target was found or null if no
object could be found that responds to the target action.
Passing an explicit target or pane constrains the target lookup to just
them; the defaultResponder and other panes are *not* searched.
@param {Object|String} target or null if no target is specified
@param {String} method name for target
@param {Object} sender optional sender
@param {SC.Surface} optional surface
@returns {Object} target object or null if none found
*/
targetForAction: function(methodName, target, sender, surface) {
// 1. no action, no target...
if (!methodName || typeof methodName !== "string") {
return null;
}
// 2. an explicit target was passed...
if (target) {
if (typeof target === 'string') {
target = SC.objectForPropertyPath(target) ||
SC.objectForPropertyPath(target, sender);
}
if (target && !target.isResponderContext) {
if (typeof target.respondsTo === 'function' && !target.respondsTo(methodName)) {
target = null;
} else if (typeof target[methodName] !== 'function') {
target = null;
}
}
return target ;
}
// 3. an explicit surface was passed...
if (surface) return this._sc_responderFor(surface, methodName);
// 4. no target or surface passed... try to find target in the active
// surfaces and the defaultResponder
var fieldEditor = this.get('fieldEditor'),
menuSurface = this.get('menuSurface'),
inputSurface = this.get('inputSurface'),
ui = this.get('ui') ;
// Check the field editor first, then check menu, input and ui surfaces.
if (fieldEditor) {
target = this._sc_responderFor(fieldEditor, methodName);
}
if (!target && menuSurface) {
target = this._sc_responderFor(menuSurface, methodName);
}
if (!target && inputSurface && inputSurface !== menuSurface) {
target = this._sc_responderFor(inputSurface, methodName);
}
if (!target && ui && (ui !== menuSurface || ui !== inputSurface)) {
target = this._sc_responderFor(menuSurface, methodName);
}
// ...still no target? check the defaultResponder...
if (!target && (target = this.get('defaultResponder'))) {
if (typeof target === 'string') {
target = SC.objectForPropertyPath(target) ;
if (target) this.set('defaultResponder', target) ; // cache if found
}
if (target && !target.isResponderContext) {
if (target.respondsTo && !target.respondsTo(methodName)) {
target = null;
} else if (SC.typeOf(target[methodName]) !== SC.T_FUNCTION) {
target = null;
}
}
}
return target ;
},
tooltip: null,
_sc_lastMouseMoved: null,
_sc_showingTooltip: false,
showTooltip: function(evt) {
// console.log('SC.Application#showTooltip()');
var tooltipSurface = this._sc_tooltipSurface,
frame = tooltipSurface.get('frame');
frame.width = tooltipSurface.computeDesiredWidth();
frame.height = 24;
frame.x = evt.pageX - 1;
frame.y = evt.pageY + 18; // Avoid the pointer
tooltipSurface.set('frame', frame); // Won't animate since it's not onscreen yet.
this.addSurface(tooltipSurface);
this._sc_showingTooltip = true;
},
hideTooltip: function() {
// console.log('SC.Application#hideTooltip()');
sc_assert(this._sc_showingTooltip);
this.removeSurface(this._sc_tooltipSurface);
this._sc_showingTooltip = false;
},
/**
Attempts to send an event down the responder chain. This method will
invoke the sendEvent() method on either the surface you pass in, the
owning surface for a responder, or the `menuSurface`, `inputSurface`, or
`ui` (only one, chosen in that order).
If you want to trap additional events, you should use this method to
send the event down the responder chain.
@param {String} action
@param {SC.Event} evt
@param {SC.Responder} target
@returns {Object} object that handled the event or null if not handled
*/
sendEvent: function(action, evt, target) {
// console.log('SC.Application#sendEvent(', action, evt, target, ')');
var surface, ret, fieldEditor = this.get('fieldEditor');
if (fieldEditor && (!target || target.isFieldEditor)) {
ret = fieldEditor.tryToPerform(action, evt) ? target : null ;
} else {
if (target) {
surface = target.get('surface');
sc_assert(surface);
} else {
surface = this.get('menuSurface') || this.get('inputSurface') || this.get('ui');
}
if (surface === this || surface && surface.isFieldEditor) surface = null;
// If we found a valid surface, send the event to it.
ret = (surface) ? surface.sendEvent(action, evt, target) : null ;
}
return ret;
},
// .......................................................
// EVENT LISTENER SETUP
//
/**
Default method to add an event listener for the named event. If you simply
need to add listeners for a type of event, you can use this method as
shorthand. Pass an array of event types to listen for and the element to
listen in. A listener will only be added if a handler is actually installed
on the RootResponder (or receiver) of the same name.
@param {Array} keyNames
@param {Element} target
@param {Object} receiver - optional if you don't want 'this'
@returns {SC.Application} receiver
*/
listenFor: function(keyNames, target, receiver) {
receiver = receiver ? receiver : this;
keyNames.forEach(function(keyName) {
var method = receiver[keyName] ;
if (method) SC.Event.add(target, keyName, receiver, method);
}, this);
target = null; // avoid memory leak
return receiver;
},
// ..........................................................
// TEXT INPUT & KEYBOARD HANDLING
//
keyup: function(evt) {
// to end the simulation of keypress in firefox set the _ffevt to null
if (this._ffevt) this._ffevt = null;
// Modifier keys are handled separately by the 'flagsChanged' event.
// Send event for modifier key changes, but stop processing if this is
// only a modifier change.
var ret = this._sc_handleModifierChanges(evt);
if (this._sc_isModifierKey(evt)) return ret;
// Fix for IME input (japanese, mandarin). If the KeyCode is 229 wait for
// the keyup and trigger a keyDown if it is enter onKeyup.
if (this._IMEInputON && evt.keyCode === 13) {
evt.isIMEInput = true;
this.sendEvent('keyDown', evt);
this._IMEInputON = false;
}
return this.sendEvent('keyUp', evt) ? evt.hasCustomEventHandling : true ;
},
/**
Invoked on a keyDown event that is not handled by any actual value. This
will get the key equivalent string and then walk down the keyPane, then
the focusedPane, then the mainPane, looking for someone to handle it.
Note that this will walk DOWN the surface hierarchy, not up it like most.
@returns {Object} Object that handled evet or null
*/
attemptKeyEquivalent: function(evt) {
// console.log('SC.Application#attemptKeyEquivalent()', evt.commandCodes()[0]);
var ret = null ;
// `keystring` is a method name representing the keys pressed (i.e
// 'alt_shift_escape')
var keystring = evt.commandCodes()[0];
// Couldn't build a keystring for this key event, nothing to do.
if (!keystring) return false;
// HACK: Show SC.View trees.
if (keystring === 'ctrl_alt_meta_s') {
this.toggleShowViewTrees();
return true;
}
var menuPane = this.get('menuPane'),
keyPane = this.get('keyPane'),
mainPane = this.get('mainPane');
if (menuPane) {
ret = menuPane.performKeyEquivalent(keystring, evt) ;
if (ret) return ret;
}
// Try the keyPane. If it's modal, then try the equivalent there but on
// nobody else.
if (keyPane) {
ret = keyPane.performKeyEquivalent(keystring, evt) ;
if (ret || keyPane.get('isModal')) return ret ;
}
// if not, then try the main pane
if (!ret && mainPane && (mainPane!==keyPane)) {
ret = mainPane.performKeyEquivalent(keystring, evt);
if (ret || mainPane.get('isModal')) return ret ;
}
return ret ;
},
_sc_lastModifiers: null,
/** @private
Modifier key changes are notified with a keydown event in most browsers.
We turn this into a flagsChanged keyboard event. Normally this does not
stop the normal browser behavior.
*/
_sc_handleModifierChanges: function(evt) {
// if the modifier keys have changed, then notify the first responder.
var m;
m = this._sc_lastModifiers = (this._sc_lastModifiers || { alt: false, ctrl: false, shift: false });
var changed = false;
if (evt.altKey !== m.alt) { m.alt = evt.altKey; changed=true; }
if (evt.ctrlKey !== m.ctrl) { m.ctrl = evt.ctrlKey; changed=true; }
if (evt.shiftKey !== m.shift) { m.shift = evt.shiftKey; changed=true;}
evt.modifiers = m; // save on event
return (changed) ? (this.sendEvent('flagsChanged', evt) ? evt.hasCustomEventHandling : true) : true ;
},
/** @private
Determines if the keyDown event is a nonprintable or function key. These
kinds of events are processed as keyboard shortcuts. If no shortcut
handles the event, then it will be sent as a regular keyDown event.
*/
_sc_isFunctionOrNonPrintableKey: function(evt) {
return !!(evt.altKey || evt.ctrlKey || evt.metaKey || ((evt.charCode !== evt.which) && SC.FUNCTION_KEYS[evt.which]));
},
/** @private
Determines if the event simply reflects a modifier key change. These
events may generate a flagsChanged event, but are otherwise ignored.
*/
_sc_isModifierKey: function(evt) {
return !!SC.MODIFIER_KEYS[evt.charCode];
},
// ..........................................................
// MOUSE HANDLING
//
mousewheel: function(evt) {
var surface = this.targetResponderForEvent(evt) ,
handler = this.sendEvent('mouseWheel', evt, surface) ;
return (handler) ? evt.hasCustomEventHandling : true ;
},
_sc_lastHovered: null,
// these methods are used to prevent unnecessary text-selection in IE,
// there could be some more work to improve this behavior and make it
// a bit more useful; right now it's just to prevent bugs when dragging
// and dropping.
_sc_mouseCanDrag: true,
selectstart: function(evt) {
var surface = this.targetResponderForEvent(evt),
result = this.sendEvent('selectStart', evt, surface);
// If the target surface implements mouseDragged, then we want to ignore
// the 'selectstart' event.
if (surface && surface.respondsTo('mouseDragged')) {
return (result !==null ? true: false) && !this._sc_mouseCanDrag;
}
else {
return (result !==null ? true: false);
}
},
drag: function() { return false; },
contextmenu: function(evt) {
var surface = this.targetResponderForEvent(evt) ;
return this.sendEvent('contextMenu', evt, surface);
},
// ..........................................................
// ANIMATION HANDLING
//
webkitAnimationStart: function(evt) {
try {
var surface = this.targetResponderForEvent(evt) ;
this.sendEvent('animationDidStart', evt, surface) ;
} catch (e) {
console.warn('Exception during animationDidStart: %@'.fmt(e)) ;
throw e;
}
return surface ? evt.hasCustomEventHandling : true;
},
webkitAnimationIteration: function(evt) {
try {
var surface = this.targetResponderForEvent(evt) ;
this.sendEvent('animationDidIterate', evt, surface) ;
} catch (e) {
console.warn('Exception during animationDidIterate: %@'.fmt(e)) ;
throw e;
}
return surface ? evt.hasCustomEventHandling : true;
},
webkitAnimationEnd: function(evt) {
try {
var surface = this.targetResponderForEvent(evt) ;
this.sendEvent('animationDidEnd', evt, surface) ;
} catch (e) {
console.warn('Exception during animationDidEnd: %@'.fmt(e)) ;
throw e;
}
return surface ? evt.hasCustomEventHandling : true;
},
/**
Called when the document is ready to begin handling events. Setup event
listeners in this method that you are interested in observing for your
particular platform. Be sure to call arguments.callee.base.apply(this, arguments);.
@returns {void}
*/
awake: function() {
// console.log('SC.Application#awake()');
// handle touch events
this.listenFor('touchstart touchmove touchend touchcancel'.w(), document);
// handle basic events
this.listenFor('keydown keyup beforedeactivate mousedown mouseup click mousemove selectstart contextmenu'.w(), document);
this.listenFor('resize'.w(), window);
// if ((/msie/).test(navigator.userAgent.toLowerCase())) this.listenFor('focusin focusout'.w(), document);
// else {
this.listenFor('focus blur'.w(), window);
// }
// handle animation events
this.listenFor('webkitAnimationStart webkitAnimationIteration webkitAnimationEnd'.w(), document);
// handle special case for keypress- you can't use normal listener to block the backspace key on Mozilla
if (this.keypress) {
if (SC.CAPTURE_BACKSPACE_KEY && SC.browser.mozilla) {
var responder = this ;
document.onkeypress = function(e) {
e = SC.Event.normalizeEvent(e);
return responder.keypress.call(responder, e);
};
// SC.Event.add(window, 'unload', this, function() { document.onkeypress = null; }); // be sure to cleanup memory leaks
// Otherwise, just add a normal event handler.
} else SC.Event.add(document, 'keypress', this, this.keypress);
}
// handle these two events specially in IE
'drag selectstart'.w().forEach(function(keyName) {
var method = this[keyName] ;
if (method) {
// if (SC.browser.msie) {
// var responder = this ;
// document.body['on' + keyName] = function(e) {
// // return method.call(responder, SC.Event.normalizeEvent(e));
// return method.call(responder, SC.Event.normalizeEvent(event || window.event)); // this is IE :(
// };
//
// // be sure to cleanup memory leaks
// SC.Event.add(window, 'unload', this, function() {
// document.body['on' + keyName] = null;
// });
//
// } else {
SC.Event.add(document, keyName, this, method);
// }
}
}, this);
SC.Event.add(document, 'mousewheel', this, this.mousewheel);
// If the browser is identifying itself as a touch-enabled browser, but
// touch events are not present, assume this is a desktop browser doing
// user agent spoofing and simulate touch events automatically.
// if (SC.browser && SC.platform && SC.browser.mobileSafari && !SC.platform.touch) {
// SC.platform.simulateTouchEvents();
// }
// Do some initial set up.
this.computeViewportSize();
this.focus();
this._sc_uiDidChange();
this._sc_perspectiveDidChange();
this._sc_tooltipSurface = SC.View.create({
tooltipBinding: SC.Binding.from('tooltip', this),
computeDesiredWidth: function() {
var tooltip = String(this.get('tooltip')),
ctx = this._measureContext,
frame = this.get('frame');
if (!ctx) {
ctx = this._measureContext = document.createElement('canvas').getContext('2d');
ctx.font = "11pt Helvetica";
}
return ctx.measureText(tooltip).width + 12;
},
willRenderLayers: function(ctx) {
// console.log('rendering tooltip');
var tooltip = String(this.get('tooltip'));
var lingrad = ctx.createLinearGradient(0,0,0,ctx.h);
lingrad.addColorStop(0, 'rgb(252,188,126)');
lingrad.addColorStop(0.9, 'rgb(255,102,0)');
lingrad.addColorStop(1, 'rgb(255,178,128)');
ctx.fillStyle = lingrad;
ctx.fillRect(0,0,ctx.w,ctx.h);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.strokeRect(0,0,ctx.w,ctx.h);
ctx.fillStyle = 'white';
ctx.font = "11pt Helvetica";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.fillText(tooltip, 6, ctx.h/2);
}
});
},
/**
Finds the surface that appears to be targeted by the passed event. This
only works on events with a valid target property.
@param {SC.Event} evt
@returns {SC.Surface} surface instance or null
*/
targetResponderForEvent: function(evt) {
// console.log('SC.Application#targetResponderForEvent()');
var parentNode = evt.target, id, ret, surfaces = SC.surfaces,
fieldEditor = this.get('fieldEditor');
// If the target is the field editor, use that.
if (fieldEditor && fieldEditor._sc_input === parentNode) {
ret = fieldEditor;
// Otherwise, find the nearest surface.
} else {
if (surfaces === null) this.updateSurfacesHash();
while (parentNode && !ret) {
id = parentNode.id;
if (id) ret = surfaces[id];
parentNode = parentNode.parentNode;
}
ret = ret? ret.targetResponderForEvent(evt) : null;
}
sc_assert(ret === null || ret.isResponder, "Error in SC.Application#targetResponderForEvent()");
return ret;
},
// ..........................................................
// KEYBOARD HANDLING
//
/** @private
The keydown event occurs whenever the physically depressed key changes.
This event is used to deliver the flagsChanged event and to with function
keys and keyboard shortcuts.
All actions that might cause an actual insertion of text are handled in
the keypress event.
*/
keydown: function(evt) {
// console.log('SC.Application#keydown(', evt, ')');
if (SC.none(evt)) return true;
var keyCode = evt.keyCode,
isFirefox = SC.isMozilla();
// Fix for IME input (japanese, mandarin).
// If the KeyCode is 229 wait for the keyup and
// trigger a keyDown if it is is enter onKeyup.
if (keyCode===229){
this._IMEInputON = true;
return this.sendEvent('keyDown', evt);
}
// If user presses the escape key while we are in the middle of a
// drag operation, cancel the drag operation and handle the event.
if (keyCode === 27 && this._sc_drag) {
this._sc_drag.cancelDrag();
this._sc_drag = null;
this._sc_mouseDownResponder = null;
return true;
}
// Firefox does NOT handle delete here...
if (isFirefox && (evt.which === 8)) return true ;
// modifier keys are handled separately by the 'flagsChanged' event
// send event for modifier key changes, but only stop processing if this
// is only a modifier change
var ret = this._sc_handleModifierChanges(evt),
target = evt.target || evt.srcElement,
forceBlock = (evt.which === 8) && !SC.allowsBackspaceToPreviousPage && (target === document.body);
if (this._sc_isModifierKey(evt)) return (forceBlock ? false : ret);
// if this is a function or non-printable key, try to use this as a key
// equivalent. Otherwise, send as a keyDown event so that the focused
// responder can do something useful with the event.
ret = true ;
if (this._sc_isFunctionOrNonPrintableKey(evt)) {
// Otherwise, send as keyDown event. If no one was interested in this
// keyDown event (probably the case), just let the browser do its own
// processing.
// Arrow keys are handled in keypress for firefox
if (keyCode>=37 && keyCode<=40 && isFirefox) return true;
ret = this.sendEvent('keyDown', evt) ;
// attempt key equivalent if key not handled
if (!ret) {
ret = !this.attemptKeyEquivalent(evt) ;
} else {
ret = evt.hasCustomEventHandling ;
if (ret) forceBlock = false ; // code asked explicitly to let delete go
}
} else {
ret = this.sendEvent('keyDown', evt);
}
return forceBlock ? false : ret ;
},
/** @private
The keypress event occurs after the user has typed something useful that
the browser would like to insert. Unlike keydown, the input codes here
have been processed to reflect that actual text you might want to insert.
Normally ignore any function or non-printable key events. Otherwise, just
trigger a keyDown.
*/
keypress: function(evt) {
// console.log('SC.Application#keypress(', evt, ')');
var ret,
keyCode = evt.keyCode,
isFirefox = SC.isMozilla();
// delete is handled in keydown() for most browsers
if (isFirefox && (evt.which === 8)) {
//get the keycode and set it for which.
evt.which = keyCode;
ret = this.sendEvent('keyDown', evt);
return ret ? (SC.allowsBackspaceToPreviousPage || evt.hasCustomEventHandling) : true ;
// normal processing. send keyDown for printable keys...
//there is a special case for arrow key repeating of events in FF.
} else {
var isFirefoxArrowKeys = (keyCode >= 37 && keyCode <= 40 && isFirefox),
charCode = evt.charCode;
if ((charCode !== undefined && charCode === 0) && !isFirefoxArrowKeys) return true;
if (isFirefoxArrowKeys) evt.which = keyCode;
return this.sendEvent('keyDown', evt) ? evt.hasCustomEventHandling:true;
}
},
/**
IE's default behavior to blur textfields and other controls can only be
blocked by returning false to this event. However we don't want to block
its default behavior otherwise textfields won't loose focus by clicking
on an empty area as it's expected. If you want to block IE from bluring
another control set blockIEDeactivate to true on the specific surface in
which you want to avoid this. Think of an autocomplete menu, you want to
click on the menu but don't loose focus.
*/
beforedeactivate: function(evt) {
// var toElement = evt.toElement;
// if (toElement && toElement.tagName && toElement.tagName!=="IFRAME") {
// var view = SC.$(toElement).view()[0];
// //The following line is neccesary to allow/block text selection for IE,
// // in combination with the selectstart event.
// if (view && view.get('blocksIEDeactivate')) return false;
// }
return true;
},
// ..........................................................
// MOUSE HANDLING
//
/**
`mouseUp` only gets delivered to the surface that handled the `mouseDown`
event. We also handle `click` and `doubleClick` events here to ensure
consistent delivery. Note that if `_sc_mouseDownResponder` is `null`, no
`mouseUp` event will be sent, but a `click` or `doubleClick` event *will*
be sent as appropriate.
*/
mouseup: function(evt) {
if (this._sc_drag) {
this._sc_drag.tryToPerform('mouseUp', evt);
this._sc_drag = null;
// FIXME: Shouldn't we return at this point?
}
var handler = null, mouseDownSurface = this._sc_mouseDownResponder,
surface = this.targetResponderForEvent(evt);
this._sc_lastMouseUpAt = Date.now(); // Why not evt.timeStamp?
// record click count.
evt.clickCount = this._sc_clickCount;
// Attempt a mouseup call only when there is a target. We don't want a
// mouseup going to anyone unless the