goog.provide('lime.Node');
goog.require('goog.events.EventTarget');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Size');
goog.require('goog.math.Vec2');
goog.require('lime');
goog.require('lime.DirtyObject');
goog.require('lime.Renderer.CANVAS');
goog.require('lime.Renderer.DOM');
/**
* Node. Abstract drawable object in lime.
* @constructor
* @implements lime.DirtyObject
* @extends goog.events.EventTarget
*/
lime.Node = function() {
goog.events.EventTarget.call(this);
this.children_ = [];
this.parent_ = null;
/** type {Object.<number, number>} */
this.transitionsAdd_ = {};
this.transitionsActive_ = {};
this.transitionsActiveSet_ = {};
/** type {Object.<number, number>} */
this.transitionsClear_ = {};
/**
* Allow translate3d and other css optimizations
* @type {boolean}
*/
this.allow3DCSSTransform_ = true;
/**
* Node has been added to DOM tree
* @type {boolean}
*/
this.inTree_ = false;
this.director_ = null;
this.scene_ = null;
/**
* Hash of active event handlers
* @type {Object}
* @private
*/
this.eventHandlers_ = {};
this.setScale(1);
this.setPosition(0, 0);
this.setSize(0, 0);
this.quality_ = 1.0;
this.setAnchorPoint(0.5, 0.5);
this.setRotation(0);
this.setAutoResize(lime.AutoResize.NONE);
this.opacity_ = 1;
this.setMask(null);
this.setRenderer(this.supportedRenderers[0].getType());
this.setDirty(lime.Dirty.LAYOUT);
};
goog.inherits(lime.Node, goog.events.EventTarget);
/**
* Supported renderers for Node
* @type {Array.<lime.Renderer>}
*/
lime.Node.prototype.supportedRenderers = [
lime.Renderer.DOM,
lime.Renderer.CANVAS
];
/**
* Set renderer for the node. Renderer defines what lower
* level technology will be used for drawing node on screen
* @param {lime.Renderer} value Renderer object.
* @return {lime.Node} Node itself.
*/
lime.Node.prototype.setRenderer = function(value) {
if (!this.renderer || this.renderer.getType() != value) {
var index = -1;
for (var i = 0; i < this.supportedRenderers.length; i++) {
if (this.supportedRenderers[i].getType() == value) {
index = i;
break;
}
}
if (index == -1) return this; //not supported
this.renderer = this.supportedRenderers[i];
this.setDirty(lime.Dirty.LAYOUT);
for (var i = 0, child; child = this.children_[i]; i++) {
child.setRenderer(value);
}
}
return this;
};
/**
* Does node require DOM element for drawing?
* @return {boolean} True if DOM is required.
*/
lime.Node.prototype.needsDomElement = function() {
return !(this.parent_ &&
this.parent_.renderer.getType() == lime.Renderer.CANVAS);
};
/**
* Return deepest element in DOM tree that is used
* for drawing the Node.
* @return {Element} Deepest DOM element.
*/
lime.Node.prototype.getDeepestDomElement = function() {
return this.getDeepestParentWithDom().domElement;
};
/**
* Return deepest parent node that requires DOM element
* for drawing on screen.
* @return {lime.Node} Deepest parent.
*/
lime.Node.prototype.getDeepestParentWithDom = function() {
if (this.needsDomElement()) {
this.updateDomElement();
return this;
} else {
if (this.parent_)
return this.parent_.getDeepestParentWithDom();
}
return null;
};
/**
* Return array of parent nodes until scene object
* @private
* @return {Array.<lime.Node>} Array of parents.
*/
lime.Node.prototype.getParentStack_ = function() {
if (!this.parent_ || this instanceof lime.Scene) return [];
var r = this.parent_.children_.indexOf(this);
var a = this.parent_.getParentStack_();
a.push(r);
return a;
};
/**
* Compare two node positions in the tree
* @param {lime.Node} n1 First node.
* @param {lime.Node} n2 Second node.
* @return {number} Comparison result.
*/
lime.Node.compareNode = function(n1, n2) {
if (n1 == n2) return 0;
var s1 = n1.getParentStack_();
var s2 = n2.getParentStack_();
var i = 0;
while (true) {
if (s1.length <= i) return 1;
if (s2.length <= i) return -1;
if (s1[i] == s2[i]) {
i++;
} else {
return s1[i] > s2[i] ? -1 : 1;
}
}
};
/**
* @type {boolean}
* @private
* @override
*/
lime.Node.prototype.customEvent_ = false;
/**
* Return a bitmask of dirty values that need to be updated before next drawing
* The bitmask parts are values of lime.Dirty enum
* @return {number} Dirty propertiest bitmask.
*/
lime.Node.prototype.getDirty = function() {
return this.dirty_;
};
/**
* Sets a dirty value true. This means that object needs that
* kind of updates before next draw.
* @param {number} value Values to be added to the bitmask.
* @param {number=} opt_pass Pass number (0-1).
* @param {boolean=} opt_nextframe Register for next frame.
* @return {lime.Node} Node itself.
*/
lime.Node.prototype.setDirty = function(value, opt_pass, opt_nextframe) {
if (value && !this.dirty_) {
lime.setObjectDirty(this, opt_pass, opt_nextframe);
}
var old = this.dirty_;
this.dirty_ |= value;
if (value == lime.Dirty.LAYOUT) {
for (var i = 0, child; child = this.children_[i]; i++) {
if (child instanceof lime.Node)
child.setDirty(lime.Dirty.LAYOUT);
}
}
if (!goog.isDef(this.dirty_) || !value) {
this.dirty_ = 0;
lime.clearObjectDirty(this, opt_pass, opt_nextframe);
}
if (value && this.maskTarget_) {
this.mSet = false;
this.maskTarget_.setDirty(~0);
}
return this;
};
/**
* Returns scale vector for the element. 1,1 means no scale.
* @return {goog.math.Vec2} scale vector.
*/
lime.Node.prototype.getScale = function() {
return this.scale_;
};
/**
* Sets new scale vector for element. This function also accepts
* 2 numbers or 1 number that would be coverted to vector before use
* @param {(goog.math.Vec2|number)} value New scale vector.
* @param {number=} opt_y Optionaly set scale using x,y.
* @return {lime.Node} Node itself.
*/
lime.Node.prototype.setScale = function(value, opt_y) {
if (arguments.length == 1 && goog.isNumber(value)) {
this.scale_ = new goog.math.Vec2(value, value);
} else if (arguments.length == 2) {
this.scale_ = new goog.math.Vec2(arguments[0], arguments[1]);
} else {
this.scale_ = value;
}
if (this.transitionsActive_[lime.Transition.SCALE]) return this;
return this.setDirty(lime.Dirty.SCALE);
};
/**
* Returns element's position coordinate
* @return {!goog.math.Coordinate} Current position coordinate.
*/
lime.Node.prototype.getPosition = function() {
return this.position_;
};
/**
* Sets new position for element. Also accepts 2 numbers(x and y value)
* @param {(goog.math.Coordinate|number)} value Position coordinate.
* @param {number=} opt_y Optionaly set position using x,y.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setPosition = function(value, opt_y) {
if (arguments.length == 2) {
this.position_ = new goog.math.Coordinate(arguments[0], arguments[1]);
} else {
this.position_ = value;
}
if (this.transitionsActive_[lime.Transition.POSITION]) return this;
return this.setDirty(lime.Dirty.POSITION);
};
/**
* Returns element used as a mask for current element
* @return {lime.Node} Mask node.
*/
lime.Node.prototype.getMask = function() {
return this.mask_;
};
/**
* Sets element as a mask for current element
* @param {lime.Node} value Mask node.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setMask = function(value) {
if (value == this.mask_) return this;
if (this.mask_) {
this.mask_.releaseDependencies();
delete this.mask_.maskTarget_;
}
this.mask_ = value;
if (this.mask_) {
this.mask_.setupDependencies();
this.mask_.maskTarget_ = this;
}
return this.setDirty(lime.Dirty.CONTENT);
};
/**
* Returns anchor point for the element.
* @return {goog.math.Vec2} Anchorpoint vector.
*/
lime.Node.prototype.getAnchorPoint = function() {
return this.anchorPoint_;
};
/**
* Sets elements anchor point to new value. Anchor point is used
* when positioning the element to position coordinate. [0,0] means
* top left corner, [1,1] bottom right, [.5,.5] means that element
* is position by the center. You can also pass in 2 numbers.
* @param {(goog.math.Vec2|number)} value AnchorPoint vector.
* @param {number=} opt_y Optionaly set anchorpoint with x,y.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setAnchorPoint = function(value, opt_y) {
if (arguments.length == 2) {
this.anchorPoint_ = new goog.math.Vec2(arguments[0], arguments[1]);
} else {
this.anchorPoint_ = value;
}
return this.setDirty(lime.Dirty.POSITION);
};
/**
* Returns rotation angle for element in degrees
* @return {number} Rotation angle.
*/
lime.Node.prototype.getRotation = function() {
return (this.rotation_ = this.rotation_ % 360);
};
/**
* Rotates element to specific angle in degrees
* @param {number} value New rotation angle.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setRotation = function(value) {
this.rotation_ = value;
if (this.transitionsActive_[lime.Transition.ROTATION]) return this;
return this.setDirty(lime.Dirty.POSITION);
};
/**
* Returns true if element currently not visible
* @return {boolean} True if node is hidden.
*/
lime.Node.prototype.getHidden = function() {
return this.hidden_;
};
/**
* Sets if element is visible or not
* @param {boolean} value Hide(true) or show(false).
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setHidden = function(value) {
this.hidden_ = value;
this.setDirty(lime.Dirty.VISIBILITY);
this.autoHide_ = 0;
return this;
};
/**
* Returns elements dimension
* @return {goog.math.Size} Current element dimensions.
*/
lime.Node.prototype.getSize = function() {
return this.size_;
};
/**
* Sets dimensions for element. This funciton also
* accepts 2 numbers: width,height
* @param {(goog.math.Size|number)} value Elements new size.
* @param {number=} opt_height Optionaly use widht,height as parameter.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setSize = function(value, opt_height) {
var oldSize = this.size_,
newval,
scale;
if (arguments.length == 2) {
newval = new goog.math.Size(arguments[0], arguments[1]);
} else {
newval = value;
}
//todo:clear this mess
var ap2 = this.getAnchorPoint();
if (oldSize && this.children_.length) {
for (var i = 0; i < this.children_.length; i++) {
var c = this.children_[i];
if (c.getAutoResize) {
var ar = c.getAutoResize();
if (ar == lime.AutoResize.NONE) continue;
var b = c.getBoundingBox();
var fixed = oldSize.width;
var c1 = b.left + ap2.x * oldSize.width;
var c2 = b.right - b.left;
var c3 = fixed - b.right - ap2.x * oldSize.width;
if (ar & lime.AutoResize.LEFT) fixed -= c1;
if (ar & lime.AutoResize.WIDTH) fixed -= c2;
if (ar & lime.AutoResize.RIGHT) fixed -= c3;
if (fixed != oldSize.width) {
scale = (newval.width - fixed) /
(oldSize.width - fixed);
if (ar & lime.AutoResize.LEFT) c1 *= scale;
if (ar & lime.AutoResize.WIDTH) c2 *= scale;
}
fixed = oldSize.height;
var r1 = b.top + ap2.y * oldSize.height;
var r2 = b.bottom - b.top;
var r3 = fixed - b.bottom - ap2.y * oldSize.height;
if (ar & lime.AutoResize.TOP) fixed -= r1;
if (ar & lime.AutoResize.HEIGHT) fixed -= r2;
if (ar & lime.AutoResize.BOTTOM) fixed -= r3;
if (fixed != oldSize.height) {
scale = (newval.height - fixed) /
(oldSize.height - fixed);
if (ar & lime.AutoResize.TOP) r1 *= scale;
if (ar & lime.AutoResize.HEIGHT) r2 *= scale;
}
var ap = c.getAnchorPoint();
c.setSize(c2, r2);
c.setPosition(c1 + ap.x * c2 - ap2.x * newval.width,
r1 + ap.y * r2 - ap2.y * newval.height);
}
}
}
this.size_ = newval;
return this.setDirty(lime.Dirty.SCALE);
};
/**
* Returns elements quality value.
* @return {number} Quality value.
*/
lime.Node.prototype.getQuality = function() {
return this.quality_;
};
/**
* Sets quality value used while drawing. Not all rendermodes can draw
* more effectively on lower quality. 1.0 full quality, 0.5 half quality.
* Setting this walue larger than 1.0 almost never does anything good.
* @param {number} value New quality value.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setQuality = function(value) {
if (this.quality_ != value) {
this.quality_ = value;
this.setDirty(lime.Dirty.SCALE);
this.calcRelativeQuality();
}
return this;
};
/**
* Return cumulative quality value relative to screen full quality.
* @return {number} Quality value.
*/
lime.Node.prototype.getRelativeQuality = function() {
if (!this.relativeQuality_)
this.calcRelativeQuality();
return this.relativeQuality_;
}
/**
* Calculates relative quality change from the
* parent objects quality
*/
lime.Node.prototype.calcRelativeQuality = function() {
var rq = goog.isDef(this.relativeQuality_) ?
this.relativeQuality_ : this.quality_;
if (this.parent_ && this.parent_.relativeQuality_)
rq = this.quality_ * this.parent_.relativeQuality_;
if (rq != this.relativeQuality_) {
this.relativeQuality_ = rq;
for (var i = 0, child; child = this.children_[i]; i++) {
if (child instanceof lime.Node)
child.calcRelativeQuality();
}
this.setDirty(lime.Dirty.SCALE);
}
};
/**
* Returns autoresize rules bitmask.
* @return {number} Autoresize bitmask.
*/
lime.Node.prototype.getAutoResize = function() {
return this.autoResize_;
};
/**
* Sets new autoresixe rules. NOT IMPLEMENTED!
* @param {number} value New autoresize bitmask.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setAutoResize = function(value) {
this.autoResize_ = value;
return this.setDirty(lime.Dirty.ALL);
};
/**
* Sets css 3d transforms rule (for DOM renderer).
* XXX: This hack is required to fix a mobile Safari rendering bug.
* It should only be used on nodes that are affected by the bug!
* 3D acceleration in css transforms is enabled by default and applied
* for iOS and Playbook.
*
* @param {boolean} value allow(true) or disable(false).
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setAllow3DCSSTransforms = function(value) {
this.allow3DCSSTransform_ = value;
return this;
};
/**
* Returns css 3d transforms flag
* @return {boolean}
*/
lime.Node.prototype.getCSS3DTransformsAllowed = function() {
return this.allow3DCSSTransform_;
};
/**
* Convert screen coordinate to node coordinate space
* @param {goog.math.Coordinate} coord Screen coordinate.
* @return {goog.math.Coordinate} Local coordinate.
*/
lime.Node.prototype.screenToLocal = function(coord) {
if (!this.inTree_) return coord;
var newcoord = this.getParent().screenToLocal(coord);
return this.parentToLocal(newcoord);
};
/**
* Covert coordinate form parent node space to
* current node space
* @param {goog.math.Coordinate} coord Parent coordinate.
* @return {goog.math.Coordinate} Local coordinate.
*/
lime.Node.prototype.parentToLocal = function(coord) {
if (!this.getParent()) return null;
coord.x -= this.position_.x;
coord.y -= this.position_.y;
coord.x /= this.scale_.x;
coord.y /= this.scale_.y;
if (this.rotation_ != 0) {
var c2 = coord.clone(),
rot = this.rotation_ * Math.PI / 180,
cos = Math.cos(rot),
sin = Math.sin(rot);
coord.x = cos * c2.x - sin * c2.y;
coord.y = cos * c2.y + sin * c2.x;
}
return coord;
};
/**
* Convert coordinate in node space to screen coordinate
* @param {goog.math.Coordinate} coord Local coordinate.
* @return {goog.math.Coordinate} Screen coordinate.
*/
lime.Node.prototype.localToScreen = function(coord) {
if (!this.inTree_) return coord;
return this.getParent().localToScreen(this.localToParent(coord));
};
/**
* Convert coordinate from current node space to
* parent node space
* @param {goog.math.Coordinate} coord Local coordinate.
* @return {goog.math.Coordinate} Parent coordinate.
*/
lime.Node.prototype.localToParent = function(coord) {
if (!this.getParent()) return coord;
var newcoord = coord.clone();
if (this.rotation_ != 0) {
var rot = -this.rotation_ * Math.PI / 180,
cos = Math.cos(rot),
sin = Math.sin(rot);
newcoord.x = cos * coord.x - sin * coord.y;
newcoord.y = cos * coord.y + sin * coord.x;
}
newcoord.x *= this.scale_.x;
newcoord.y *= this.scale_.y;
newcoord.x += this.position_.x;
newcoord.y += this.position_.y;
return newcoord;
};
/**
* Convert coordinate in node space to other nodes coordinate space
* @param {goog.math.Coordinate} coord Local coordinate.
* @param {lime.Node} node Node for new coordinate space.
* @return {goog.math.Coordinate} Coordinate in node space.
*/
lime.Node.prototype.localToNode = function(coord, node) {
// Todo: this can be optimized.
// maybe with goog.dom.findCommonAncestor but probably even more
if (!this.inTree_) return coord;
return node.screenToLocal(this.localToScreen(coord));
};
/**
* Returns the opacity value of the Node. 0.0=100% trasparent, 1.0=100% opaque
* @return {number} Opacity value.
*/
lime.Node.prototype.getOpacity = function() {
return this.opacity_;
};
/**
* Sets the opacity value of the Node object
* @param {number} value New opacity value(0-1).
* @return {lime.Node} The node object itself.
*/
lime.Node.prototype.setOpacity = function(value) {
this.opacity_ = value;
var hidden = this.getHidden();
if (this.opacity_ == 0 && !hidden) {
this.setHidden(true);
this.autoHide_ = 1;
} else if (this.opacity_ != 0 && hidden && this.autoHide_) {
this.setHidden(false);
}
if (goog.isDef(this.transitionsActive_[lime.Transition.OPACITY])) {
return this;
}
this.setDirty(lime.Dirty.ALPHA);
return this;
};
/**
* Create DOM element to render the node
*/
lime.Node.prototype.createDomElement = function() {
var newTagName =
this.renderer.getType() == lime.Renderer.CANVAS ? 'canvas' : 'div';
var create = function() {
this.domElement = this.rootElement =
this.containerElement = goog.dom.createDom(newTagName);
if (this.domClassName)
goog.dom.classes.add(this.domElement, this.domClassName);
this.dirty_ |= ~0;
};
if (this.domElement) {
var curtag = this.domElement.tagName.toLowerCase();
if (curtag != newTagName) {
var oldEl = this.rootElement;
create.call(this);
if (oldEl.parentNode)
oldEl.parentNode.replaceChild(this.rootElement, oldEl);
//return true;
}
} else {
create.call(this);
//return true;
}
};
/**
* Update DOM element connected to the node
*/
lime.Node.prototype.updateDomElement = function() {
if (this.needsDomElement()) {
this.createDomElement();
} else {
this.removeDomElement();
}
//return false;
};
/**
* Remove DOM element connected to teh node
*/
lime.Node.prototype.removeDomElement = function() {
if (this.rootElement) {
goog.dom.removeNode(this.rootElement);
delete this.domElement;
delete this.rootElement;
delete this.containerElement;
//return true;
}
};
/**
* Update node's layout (tree relations)
*/
lime.Node.prototype.updateLayout = function() {
// debugger;
this.dirty_ &= ~lime.Dirty.LAYOUT;
//var didupdate = this.updateDomElement();
this.updateDomElement();
if (this.parent_ && (this.parent_.dirty_ & lime.Dirty.LAYOUT)) {
this.parent_.updateLayout();
return;
}
if (this.needsDomElement()) {
for (var i = 0, child; child = this.children_[i]; i++) {
if (child instanceof lime.Node)
child.updateLayout();
}
this.renderer.updateLayout.call(this);
}
};
/**
* Update modified dirty parameters to visible elements properties
* @param {number=} opt_pass Pass number.
*/
lime.Node.prototype.update = function(opt_pass) {
// if (!this.renderer) return;
var property,
value;
var pass = opt_pass || 0;
var uid = goog.getUid(this);
if (this.dirty_ & lime.Dirty.LAYOUT) {
this.updateLayout();
}
var do_draw = this.renderer.getType() == lime.Renderer.DOM || pass;
if (do_draw) {
//clear transitions in the queue
for (var i in this.transitionsClear_) {
delete this.transitionsActive_[i];
delete this.transitionsActiveSet_[i];
property = lime.Node.getPropertyForTransition(parseInt(i, 10));
lime.style.clearTransition(this.domElement, property);
if (this.domElement != this.containerElement) {
lime.style.clearTransition(this.continerElement, property);
}
}
// predraw is a check that elements are correctly drawn before the
// transition. if not then transition is started in the next frame not now.
var only_predraw = 0;
for (i in this.transitionsAdd_) {
value = this.transitionsAdd_[i];
// 3rd is an "already_activated" flag
if (!value[3]) {
value[3] = 1;
if (i == lime.Transition.POSITION &&
this.positionDrawn_ != this.position_) {
this.setDirty(lime.Dirty.POSITION, 0, true);
only_predraw = 1;
}
if (i == lime.Transition.SCALE &&
this.scaleDrawn_ != this.scale_) {
this.setDirty(lime.Dirty.SCALE, 0, true);
only_predraw = 1;
}
if (i == lime.Transition.OPACITY &&
this.opacityDrawn_ != this.opacity_) {
this.setDirty(lime.Dirty.ALPHA, 0, true);
only_predraw = 1;
}
if (i == lime.Transition.ROTATION &&
this.rotationDrawn_ != this.rotation_) {
this.setDirty(lime.Dirty.ROTATION, 0, true);
only_predraw = 1;
}
}
}
// activate the transitions
if (!only_predraw)
for (i in this.transitionsAdd_) {
value = this.transitionsAdd_[i];
property = lime.Node.getPropertyForTransition(parseInt(i, 10));
if (this.renderer.getType() == lime.Renderer.DOM || property != 'opacity') {
this.transitionsActive_[i] = value[0];
lime.style.setTransition(this.domElement,
property, value[1], value[2]);
if (this.domElement != this.containerElement &&
property == lime.style.transformProperty) {
lime.style.setTransition(this.containerElement,
property, value[1], value[2]);
}
}
delete this.transitionsAdd_[i];
}
// cache last drawn values to for predraw check
this.positionDrawn_ = this.position_;
this.scaleDrawn_ = this.scale_;
this.opacityDrawn_ = this.opacity_;
this.rotationDrawn_ = this.rotation_;
this.transitionsClear_ = {};
}
if (pass) {
this.renderer.drawCanvas.call(this);
} else {
if (this.renderer.getType() == lime.Renderer.CANVAS) {
var parent = this.getDeepestParentWithDom();
parent.redraw_ = 1;
if (parent == this && this.dirty_ == lime.Dirty.POSITION && !this.mask_) {
parent.redraw_ = 0;
}
lime.setObjectDirty(this.getDeepestParentWithDom(), 1);
}
// dom draw happens here
this.renderer.update.call(this);
}
// set flags that transitions have been draw.
if (do_draw)
for (i in this.transitionsActive_) {
if (this.transitionsActive_[i]) {
this.transitionsActiveSet_[i] = true;
}
}
if (this.dependencies_) {
for (var i = 0; i < this.dependencies_.length; i++) {
this.dependencies_[i].setDirty(lime.Dirty.ALL);
}
}
//clear dirty
this.setDirty(0, pass);
};
/**
* Return CSS property name for transition constant
* @param {number} transition Transition constant.
* @return {string} Property name.
*/
lime.Node.getPropertyForTransition = function(transition) {
return transition == lime.Transition.OPACITY ?
'opacity' : lime.style.transformProperty;
};
/**
* Return the parent object. Returns null in not in tree
* @return {lime.Node} Parent node.
*/
lime.Node.prototype.getParent = function() {
return this.parent_ ? this.parent_ : null;
};
/**
* Append element to the end of childrens array
* @param {lime.Node|Element|Node} child Child node.
* @param {number=} opt_pos Position of new child.
* @return {lime.Node} obejct itself.
*/
lime.Node.prototype.appendChild = function(child, opt_pos) {
if (child instanceof lime.Node && child.getParent()) {
child.getParent().removeChild(child);
} else if (child.parentNode) {
goog.dom.removeNode( /** @type {Node} */ (child));
}
child.parent_ = this;
if (opt_pos == undefined) {
this.children_.push(child);
} else {
goog.array.insertAt(this.children_, child, opt_pos);
}
if (this.renderer.getType() != lime.Renderer.DOM) {
child.setRenderer(this.renderer.getType());
}
if (child instanceof lime.Node) {
child.calcRelativeQuality();
if (this.inTree_) child.wasAddedToTree();
}
return this.setDirty(lime.Dirty.LAYOUT);
};
/**
* Return number of childnodes current element has.
* @return {number} Number of children.
*/
lime.Node.prototype.getNumberOfChildren = function() {
return this.children_.length;
}
/**
* Return the child at defined index.
* @param {number} index Child index.
* @return {lime.Node|Element|null} Child element.
*/
lime.Node.prototype.getChildAt = function(index) {
if (index >= 0 && this.getNumberOfChildren() > index) {
return this.children_[index];
} else {
return null;
}
};
/**
* Return the index position of a child.
* @param {lime.Node|Element} child Child to search.
* @return {number} Index number.
*/
lime.Node.prototype.getChildIndex = function(child) {
return this.children_.indexOf(child);
};
/**
* Remove element from the childrens array
* @param {lime.Node|Element} child Child node.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.removeChild = function(child) {
return this.removeChildAt(this.getChildIndex(child));
};
/**
* Remove element at a given index from the childrens array
* @param {number} index Index of element to remove.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.removeChildAt = function(index) {
if (index >= 0 && this.getNumberOfChildren() > index) {
var child = this.getChildAt(index);
if (child.maskTarget_) {
child.maskTarget_.setMask(null);
}
if (child instanceof lime.Node) {
if (this.inTree_)
child.wasRemovedFromTree();
child.removeDomElement();
child.parent_ = null;
} else
goog.dom.removeNode(child);
this.children_.splice(index, 1);
return this.setDirty(lime.Dirty.LAYOUT);
}
return this;
};
/**
* Removes all children of a node.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.removeAllChildren = function() {
while (this.getNumberOfChildren()) {
this.removeChildAt(0);
}
return this;
};
/**
* Move a child to another index in the childrens array.
* @param {lime.Node} child Child node.
* @param {number} index New index for the child.
* @return {lime.Node} object itself.
*/
lime.Node.prototype.setChildIndex = function(child, index) {
var oldindex = this.getChildIndex(child);
if (oldindex != -1 && oldindex != index) {
this.children_.splice(oldindex, 1);
goog.array.insertAt(this.children_, child, index);
if (this.getDirector())
this.getDirector().eventDispatcher.updateDispatchOrder(child);
return this.setDirty(lime.Dirty.LAYOUT);
}
return this;
};
/**
* @inheritDoc
*/
lime.Node.prototype.addEventListener = function(type, handler,
opt_capture, opt_handlerScope) {
// Bypass all mouse events on touchscreen devices
if (lime.userAgent.SUPPORTS_TOUCH &&
type.substring(0, 5) == 'mouse') return;
// First element defines if events are registered with DOM 1=yes/0=no
// Second element defines how many listeners have been set
if (!goog.isDef(this.eventHandlers_[type])) {
this.eventHandlers_[type] = [0, 0];
}
if (this.inTree_ && this.eventHandlers_[type][0] == 0) {
this.eventHandlers_[type][0] = 1;
this.getDirector().eventDispatcher.register(this, type);
}
this.eventHandlers_[type][1]++;
};
/**
* @inheritDoc
*/
lime.Node.prototype.removeEventListener = function(
type, handler, opt_capture, opt_handlerScope) {
// Bypass all mouse events on touchscreen devices
if (lime.userAgent.SUPPORTS_TOUCH &&
type.substring(0, 5) == 'mouse') return;
if (this.inTree_ && this.eventHandlers_[type][1] == 1) {
this.eventHandlers_[type][0] = 0;
this.getDirector().eventDispatcher.release(this, type);
}
this.eventHandlers_[type][1]--;
if (!this.eventHandlers_[type][1]) delete this.eventHandlers_[type];
};
/**
* Return the Director instance related to the node.
* Returns null if no director connection.
* @return {lime.Director} Current director.
*/
lime.Node.prototype.getDirector = function() {
if (!this.inTree_) return null;
return this.director_;
};
/**
* Returns the Scene instance related to the node.
* Returns null if no scene connection.
* @return {lime.Scene} Current scene.
*/
lime.Node.prototype.getScene = function() {
if (!this.inTree_) return null;
return this.scene_;
};
/**
* Handle removing Node from DOM tree
*/
lime.Node.prototype.wasRemovedFromTree = function() {
var child;
if (!this.dependencySet_) {
this.removeDependency(this.getParent());
}
// Call remove for all children
for (var i = 0; child = this.children_[i]; i++) {
if (child instanceof lime.Node) {
child.wasRemovedFromTree();
}
}
// Unregister Event listeners
for (var type in this.eventHandlers_) {
this.eventHandlers_[type][0] = 0;
if (!this.getDirector()) debugger;
this.getDirector().eventDispatcher.release(this, type);
}
this.getDirector().eventDispatcher.updateDispatchOrder(this);
this.inTree_ = false;
this.director_ = null;
this.scene_ = null;
};
/**
* Handle adding Node to the DOM tree
*/
lime.Node.prototype.wasAddedToTree = function() {
this.inTree_ = true;
this.director_ = this.parent_.getDirector();
this.scene_ = this.parent_.getScene();
// Notify all children
for (var i = 0, child; child = this.children_[i]; i++) {
if (child instanceof lime.Node) {
child.wasAddedToTree();
}
}
// Register Event Listeners
for (var type in this.eventHandlers_) {
this.eventHandlers_[type][0] = 1;
this.getDirector().eventDispatcher.register(this, type);
}
if (this.dependencySet_) {
this.setupDependencies();
}
this.getDirector().eventDispatcher.updateDispatchOrder(this);
};
lime.Node.prototype.setupDependencies = function() {
this.dependencySet_ = true;
if (this.inTree_) {
this.addDependency(this.getParent());
}
}
lime.Node.prototype.addDependency = function(other) {
if (!other.dependencies_) other.dependencies_ = [];
goog.array.insert(other.dependencies_, this);
if (!other && !(other.getParent() instanceof lime.Scene)) {
this.addDependency(other.getParent());
}
}
lime.Node.prototype.removeDependency = function(other) {
if (!other || !other.dependencies_) return;
goog.array.remove(other.dependencies_, this);
this.removeDependency(other.getParent());
}
lime.Node.prototype.releaseDependencies = function() {
delete this.dependencySet_;
this.removeDependency(this.getParent());
}
/**
* Returns a bounding box for the element in its own coordinate space
* @return {goog.math.Box} Contents frame in node space.
*/
lime.Node.prototype.getFrame = function() {
var s = this.getSize(),
a = this.getAnchorPoint();
return new goog.math.Box(-s.height * a.y, //top
s.width * (1 - a.x), //right
s.height * (1 - a.y), //bottom
-s.width * a.x //left
);
};
/**
* Returns bounding box for element in parents coordinate space.
* @param {goog.math.Box} opt_frame Optional frame box for element.
* @return {goog.math.Box} Bounding box.
*/
lime.Node.prototype.getBoundingBox = function(opt_frame) {
var frame = opt_frame || this.getFrame();
var tl = new goog.math.Coordinate(frame.left, frame.top);
var tr = new goog.math.Coordinate(frame.right, frame.top);
var bl = new goog.math.Coordinate(frame.left, frame.bottom);
var br = new goog.math.Coordinate(frame.right, frame.bottom);
tl = this.localToParent(tl);
tr = this.localToParent(tr);
bl = this.localToParent(bl);
br = this.localToParent(br);
return new goog.math.Box(
Math.floor(Math.min(tl.y, tr.y, bl.y, br.y)),
Math.ceil(Math.max(tl.x, tr.x, bl.x, br.x)),
Math.ceil(Math.max(tl.y, tr.y, bl.y, br.y)),
Math.floor(Math.min(tl.x, tr.x, bl.x, br.x)));
};
/**
* Return box containing current element and all its children
* @return {goog.math.Box} Bounding box.
*/
lime.Node.prototype.measureContents = function() {
var frame = this.getFrame();
if (frame.left == frame.right && this.children_.length) {
frame = this.children_[0].getBoundingBox(
this.children_[0].measureContents());
}
for (var i = 0, child; child = this.children_[i]; i++) {
if (child.isMask != 1)
frame.expandToInclude(child.getBoundingBox(child.measureContents()));
}
return frame;
};
/**
* Register transition property. Use through animations.
* @param {number} property Transition property constant.
* @param {*} value New value.
* @param {number} duration Transition duration.
* @param {Array.<*>} ease Easing function.
*/
lime.Node.prototype.addTransition = function(property, value, duration, ease) {
this.transitionsAdd_[property] = [value, duration, ease, 0];
};
/**
* Clear previously set transition
* @param {number} property Transition property.
*/
lime.Node.prototype.clearTransition = function(property) {
this.transitionsClear_[property] = 1;
};
/**
* Checks if event should fire on element based on the position.
* Before returning true this function should set the position property
* of the event to the hit position in elements coordinate space
* @param {lime.events.Event} e Event object.
* @return {boolean} If node should handle the event.
*/
lime.Node.prototype.hitTest = function(e) {
var coord = this.screenToLocal(e.screenPosition);
if (this.getFrame().contains(coord)) {
e.position = coord;
return true;
}
return false;
};
/**
* Add Node to action targets list and start the animation
* @param {lime.animation.Animation} action Animation to run.
*/
lime.Node.prototype.runAction = function(action) {
action.addTarget(this);
action.play();
};