1 /****************************************************************************
  2  Copyright (c) 2008-2010 Ricardo Quesada
  3  Copyright (c) 2011-2012 cocos2d-x.org
  4  Copyright (c) 2013-2014 Chukong Technologies Inc.
  5 
  6  http://www.cocos2d-x.org
  7 
  8  Permission is hereby granted, free of charge, to any person obtaining a copy
  9  of this software and associated documentation files (the "Software"), to deal
 10  in the Software without restriction, including without limitation the rights
 11  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 12  copies of the Software, and to permit persons to whom the Software is
 13  furnished to do so, subject to the following conditions:
 14 
 15  The above copyright notice and this permission notice shall be included in
 16  all copies or substantial portions of the Software.
 17 
 18  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 19  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 20  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 21  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 22  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 23  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 24  THE SOFTWARE.
 25  ****************************************************************************/
 26 
 27 /**
 28  * @constant
 29  * @type Number
 30  */
 31 cc.MENU_STATE_WAITING = 0;
 32 /**
 33  * @constant
 34  * @type Number
 35  */
 36 cc.MENU_STATE_TRACKING_TOUCH = 1;
 37 /**
 38  * @constant
 39  * @type Number
 40  */
 41 cc.MENU_HANDLER_PRIORITY = -128;
 42 /**
 43  * @constant
 44  * @type Number
 45  */
 46 cc.DEFAULT_PADDING = 5;
 47 
 48 /**
 49  *<p> Features and Limitation:<br/>
 50  *  - You can add MenuItem objects in runtime using addChild:<br/>
 51  *  - But the only accepted children are MenuItem objects</p>
 52  * @class
 53  * @extends cc.Layer
 54  * @param {...cc.MenuItem|null} menuItems
 55  * @example
 56  * var layer = new cc.Menu(menuitem1, menuitem2, menuitem3);
 57  */
 58 cc.Menu = cc.Layer.extend(/** @lends cc.Menu# */{
 59     enabled: false,
 60 
 61     _selectedItem: null,
 62     _state: -1,
 63     _touchListener: null,
 64     _className: "Menu",
 65 
 66     /**
 67      * Constructor of cc.Menu override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
 68      * @param {...cc.MenuItem|null} menuItems
 69      */
 70     ctor: function (menuItems) {
 71         cc.Layer.prototype.ctor.call(this);
 72         this._color = cc.color.WHITE;
 73         this.enabled = false;
 74         this._opacity = 255;
 75         this._selectedItem = null;
 76         this._state = -1;
 77 
 78         this._touchListener = cc.EventListener.create({
 79             event: cc.EventListener.TOUCH_ONE_BY_ONE,
 80             swallowTouches: true,
 81             onTouchBegan: this._onTouchBegan,
 82             onTouchMoved: this._onTouchMoved,
 83             onTouchEnded: this._onTouchEnded,
 84             onTouchCancelled: this._onTouchCancelled
 85         });
 86 
 87         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
 88             cc.log("parameters should not be ending with null in Javascript");
 89 
 90         var argc = arguments.length, items;
 91         if (argc === 0) {
 92             items = [];
 93         } else if (argc === 1) {
 94             if (menuItems instanceof Array) {
 95                 items = menuItems;
 96             }
 97             else items = [menuItems];
 98         }
 99         else if (argc > 1) {
100             items = [];
101             for (var i = 0; i < argc; i++) {
102                 if (arguments[i])
103                     items.push(arguments[i]);
104             }
105         }
106         this.initWithArray(items);
107     },
108     /**
109      * <p>
110      *     Event callback that is invoked every time when CCMenu enters the 'stage'.                                   <br/>
111      *     If the CCMenu enters the 'stage' with a transition, this event is called when the transition starts.        <br/>
112      *     During onEnter you can't access a "sister/brother" node.                                                    <br/>
113      *     If you override onEnter, you must call its parent's onEnter function with this._super().
114      * </p>
115      */
116     onEnter: function () {
117         var locListener = this._touchListener;
118         if (!locListener._isRegistered())
119             cc.eventManager.addListener(locListener, this);
120         cc.Node.prototype.onEnter.call(this);
121     },
122 
123     /**
124      * return whether or not the menu will receive events
125      * @return {Boolean}
126      */
127     isEnabled: function () {
128         return this.enabled;
129     },
130 
131     /**
132      * set whether or not the menu will receive events
133      * @param {Boolean} enabled
134      */
135     setEnabled: function (enabled) {
136         this.enabled = enabled;
137     },
138 
139     /**
140      * initializes a cc.Menu with it's items
141      * @param {Array} args
142      * @return {Boolean}
143      */
144     initWithItems: function (args) {
145         var pArray = [];
146         if (args) {
147             for (var i = 0; i < args.length; i++) {
148                 if (args[i])
149                     pArray.push(args[i]);
150             }
151         }
152 
153         return this.initWithArray(pArray);
154     },
155 
156     /**
157      * initializes a cc.Menu with a Array of cc.MenuItem objects
158      * @param {Array} arrayOfItems array Of cc.MenuItem Items
159      * @return {Boolean}
160      */
161     initWithArray: function (arrayOfItems) {
162         if (cc.Layer.prototype.init.call(this)) {
163             this.enabled = true;
164 
165             // menu in the center of the screen
166             var winSize = cc.winSize;
167             this.setPosition(winSize.width / 2, winSize.height / 2);
168             this.setContentSize(winSize);
169             this.setAnchorPoint(0.5, 0.5);
170             this.ignoreAnchorPointForPosition(true);
171 
172             if (arrayOfItems) {
173                 for (var i = 0; i < arrayOfItems.length; i++)
174                     this.addChild(arrayOfItems[i], i);
175             }
176 
177             this._selectedItem = null;
178             this._state = cc.MENU_STATE_WAITING;
179 
180             // enable cascade color and opacity on menus
181             this.cascadeColor = true;
182             this.cascadeOpacity = true;
183 
184             return true;
185         }
186         return false;
187     },
188 
189     /**
190      * add a child for  cc.Menu
191      * @param {cc.Node} child
192      * @param {Number|Null} [zOrder=] zOrder for the child
193      * @param {Number|Null} [tag=] tag for the child
194      */
195     addChild: function (child, zOrder, tag) {
196         if (!(child instanceof cc.MenuItem))
197             throw new Error("cc.Menu.addChild() : Menu only supports MenuItem objects as children");
198         cc.Layer.prototype.addChild.call(this, child, zOrder, tag);
199     },
200 
201     updateAlign: function () {
202         switch (this._align) {
203             case 'vertically':
204                 this.alignItemsVertically();
205                 break;
206             case 'horizontally':
207                 this.alignItemsHorizontally();
208                 break;
209         }
210     },
211 
212     /**
213      * align items vertically with default padding
214      */
215     alignItemsVertically: function () {
216         this.alignItemsVerticallyWithPadding(cc.DEFAULT_PADDING);
217     },
218 
219     /**
220      * align items vertically with specified padding
221      * @param {Number} padding
222      */
223     alignItemsVerticallyWithPadding: function (padding) {
224         this._align = 'vertically';
225         var height = -padding, locChildren = this._children, len, i, locScaleY, locHeight, locChild;
226         if (locChildren && locChildren.length > 0) {
227             for (i = 0, len = locChildren.length; i < len; i++)
228                 height += locChildren[i].height * locChildren[i].scaleY + padding;
229 
230             var y = height / 2.0;
231 
232             for (i = 0, len = locChildren.length; i < len; i++) {
233                 locChild = locChildren[i];
234                 locHeight = locChild.height;
235                 locScaleY = locChild.scaleY;
236                 locChild.setPosition(0, y - locHeight * locScaleY / 2);
237                 y -= locHeight * locScaleY + padding;
238             }
239         }
240     },
241 
242     /**
243      * align items horizontally with default padding
244      */
245     alignItemsHorizontally: function () {
246         this.alignItemsHorizontallyWithPadding(cc.DEFAULT_PADDING);
247     },
248 
249     /**
250      * align items horizontally with specified padding
251      * @param {Number} padding
252      */
253     alignItemsHorizontallyWithPadding: function (padding) {
254         this._align = 'horizontally';
255         var width = -padding, locChildren = this._children, i, len, locScaleX, locWidth, locChild;
256         if (locChildren && locChildren.length > 0) {
257             for (i = 0, len = locChildren.length; i < len; i++)
258                 width += locChildren[i].width * locChildren[i].scaleX + padding;
259 
260             var x = -width / 2.0;
261 
262             for (i = 0, len = locChildren.length; i < len; i++) {
263                 locChild = locChildren[i];
264                 locScaleX = locChild.scaleX;
265                 locWidth = locChildren[i].width;
266                 locChild.setPosition(x + locWidth * locScaleX / 2, 0);
267                 x += locWidth * locScaleX + padding;
268             }
269         }
270     },
271 
272     /**
273      * align items in columns
274      * @example
275      * // Example
276      * menu.alignItemsInColumns(3,2,3)// this will create 3 columns, with 3 items for first column, 2 items for second and 3 for third
277      *
278      * menu.alignItemsInColumns(3,3)//this creates 2 columns, each have 3 items
279      */
280     alignItemsInColumns: function (/*Multiple Arguments*/) {
281         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
282             cc.log("parameters should not be ending with null in Javascript");
283 
284         var rows = [];
285         for (var i = 0; i < arguments.length; i++) {
286             rows.push(arguments[i]);
287         }
288         var height = -5;
289         var row = 0;
290         var rowHeight = 0;
291         var columnsOccupied = 0;
292         var rowColumns, tmp, len;
293         var locChildren = this._children;
294         if (locChildren && locChildren.length > 0) {
295             for (i = 0, len = locChildren.length; i < len; i++) {
296                 if (row >= rows.length)
297                     continue;
298 
299                 rowColumns = rows[row];
300                 // can not have zero columns on a row
301                 if (!rowColumns)
302                     continue;
303 
304                 tmp = locChildren[i].height;
305                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
306 
307                 ++columnsOccupied;
308                 if (columnsOccupied >= rowColumns) {
309                     height += rowHeight + 5;
310 
311                     columnsOccupied = 0;
312                     rowHeight = 0;
313                     ++row;
314                 }
315             }
316         }
317         // check if too many rows/columns for available menu items
318         //cc.assert(!columnsOccupied, "");    //?
319         var winSize = cc.director.getWinSize();
320 
321         row = 0;
322         rowHeight = 0;
323         rowColumns = 0;
324         var w = 0.0;
325         var x = 0.0;
326         var y = (height / 2);
327 
328         if (locChildren && locChildren.length > 0) {
329             for (i = 0, len = locChildren.length; i < len; i++) {
330                 var child = locChildren[i];
331                 if (rowColumns === 0) {
332                     rowColumns = rows[row];
333                     w = winSize.width / (1 + rowColumns);
334                     x = w;
335                 }
336 
337                 tmp = child._getHeight();
338                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
339                 child.setPosition(x - winSize.width / 2, y - tmp / 2);
340 
341                 x += w;
342                 ++columnsOccupied;
343 
344                 if (columnsOccupied >= rowColumns) {
345                     y -= rowHeight + 5;
346                     columnsOccupied = 0;
347                     rowColumns = 0;
348                     rowHeight = 0;
349                     ++row;
350                 }
351             }
352         }
353     },
354     /**
355      * align menu items in rows
356      * @param {Number}
357      * @example
358      * // Example
359      * menu.alignItemsInRows(5,3)//this will align items to 2 rows, first row with 5 items, second row with 3
360      *
361      * menu.alignItemsInRows(4,4,4,4)//this creates 4 rows each have 4 items
362      */
363     alignItemsInRows: function (/*Multiple arguments*/) {
364         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
365             cc.log("parameters should not be ending with null in Javascript");
366         var columns = [], i;
367         for (i = 0; i < arguments.length; i++) {
368             columns.push(arguments[i]);
369         }
370         var columnWidths = [];
371         var columnHeights = [];
372 
373         var width = -10;
374         var columnHeight = -5;
375         var column = 0;
376         var columnWidth = 0;
377         var rowsOccupied = 0;
378         var columnRows, child, len, tmp;
379 
380         var locChildren = this._children;
381         if (locChildren && locChildren.length > 0) {
382             for (i = 0, len = locChildren.length; i < len; i++) {
383                 child = locChildren[i];
384                 // check if too many menu items for the amount of rows/columns
385                 if (column >= columns.length)
386                     continue;
387 
388                 columnRows = columns[column];
389                 // can't have zero rows on a column
390                 if (!columnRows)
391                     continue;
392 
393                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
394                 tmp = child.width;
395                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
396 
397                 columnHeight += (child.height + 5);
398                 ++rowsOccupied;
399 
400                 if (rowsOccupied >= columnRows) {
401                     columnWidths.push(columnWidth);
402                     columnHeights.push(columnHeight);
403                     width += columnWidth + 10;
404 
405                     rowsOccupied = 0;
406                     columnWidth = 0;
407                     columnHeight = -5;
408                     ++column;
409                 }
410             }
411         }
412         // check if too many rows/columns for available menu items.
413         //cc.assert(!rowsOccupied, "");
414         var winSize = cc.director.getWinSize();
415 
416         column = 0;
417         columnWidth = 0;
418         columnRows = 0;
419         var x = -width / 2;
420         var y = 0.0;
421 
422         if (locChildren && locChildren.length > 0) {
423             for (i = 0, len = locChildren.length; i < len; i++) {
424                 child = locChildren[i];
425                 if (columnRows === 0) {
426                     columnRows = columns[column];
427                     y = columnHeights[column];
428                 }
429 
430                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
431                 tmp = child._getWidth();
432                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
433 
434                 child.setPosition(x + columnWidths[column] / 2, y - winSize.height / 2);
435 
436                 y -= child.height + 10;
437                 ++rowsOccupied;
438 
439                 if (rowsOccupied >= columnRows) {
440                     x += columnWidth + 5;
441                     rowsOccupied = 0;
442                     columnRows = 0;
443                     columnWidth = 0;
444                     ++column;
445                 }
446             }
447         }
448     },
449 
450     /**
451      * remove a child from cc.Menu
452      * @param {cc.Node} child the child you want to remove
453      * @param {boolean} cleanup whether to cleanup
454      */
455     removeChild: function (child, cleanup) {
456         if (child == null)
457             return;
458         if (!(child instanceof cc.MenuItem)) {
459             cc.log("cc.Menu.removeChild():Menu only supports MenuItem objects as children");
460             return;
461         }
462 
463         if (this._selectedItem === child)
464             this._selectedItem = null;
465         cc.Node.prototype.removeChild.call(this, child, cleanup);
466     },
467 
468     _onTouchBegan: function (touch, event) {
469         var target = event.getCurrentTarget();
470         if (target._state !== cc.MENU_STATE_WAITING || !target._visible || !target.enabled)
471             return false;
472 
473         for (var c = target.parent; c != null; c = c.parent) {
474             if (!c.isVisible())
475                 return false;
476         }
477 
478         target._selectedItem = target._itemForTouch(touch);
479         if (target._selectedItem) {
480             target._state = cc.MENU_STATE_TRACKING_TOUCH;
481             target._selectedItem.selected();
482             target._selectedItem.setNodeDirty();
483             return true;
484         }
485         return false;
486     },
487 
488     _onTouchEnded: function (touch, event) {
489         var target = event.getCurrentTarget();
490         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
491             cc.log("cc.Menu.onTouchEnded(): invalid state");
492             return;
493         }
494         if (target._selectedItem) {
495             target._selectedItem.unselected();
496             target._selectedItem.setNodeDirty();
497             target._selectedItem.activate();
498         }
499         target._state = cc.MENU_STATE_WAITING;
500     },
501 
502     _onTouchCancelled: function (touch, event) {
503         var target = event.getCurrentTarget();
504         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
505             cc.log("cc.Menu.onTouchCancelled(): invalid state");
506             return;
507         }
508         if (target._selectedItem) {
509             target._selectedItem.unselected();
510             target._selectedItem.setNodeDirty();
511         }
512         target._state = cc.MENU_STATE_WAITING;
513     },
514 
515     _onTouchMoved: function (touch, event) {
516         var target = event.getCurrentTarget();
517         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
518             cc.log("cc.Menu.onTouchMoved(): invalid state");
519             return;
520         }
521         var currentItem = target._itemForTouch(touch);
522         if (currentItem !== target._selectedItem) {
523             if (target._selectedItem) {
524                 target._selectedItem.unselected();
525                 target._selectedItem.setNodeDirty();
526             }
527             target._selectedItem = currentItem;
528             if (target._selectedItem) {
529                 target._selectedItem.selected();
530                 target._selectedItem.setNodeDirty();
531             }
532         }
533     },
534 
535     /**
536      * <p>
537      * callback that is called every time the cc.Menu leaves the 'stage'.                                         <br/>
538      * If the cc.Menu leaves the 'stage' with a transition, this callback is called when the transition finishes. <br/>
539      * During onExit you can't access a sibling node.                                                             <br/>
540      * If you override onExit, you shall call its parent's onExit with this._super().
541      * </p>
542      */
543     onExit: function () {
544         if (this._state === cc.MENU_STATE_TRACKING_TOUCH) {
545             if (this._selectedItem) {
546                 this._selectedItem.unselected();
547                 this._selectedItem = null;
548             }
549             this._state = cc.MENU_STATE_WAITING;
550         }
551         cc.Node.prototype.onExit.call(this);
552     },
553     /**
554      * only use for jsbinding
555      * @param value
556      */
557     setOpacityModifyRGB: function (value) {
558     },
559     /**
560      * only use for jsbinding
561       * @returns {boolean}
562      */
563     isOpacityModifyRGB: function () {
564         return false;
565     },
566 
567     _itemForTouch: function (touch) {
568         var touchLocation = touch.getLocation();
569         var itemChildren = this._children, locItemChild;
570         if (itemChildren && itemChildren.length > 0) {
571             for (var i = itemChildren.length - 1; i >= 0; i--) {
572                 locItemChild = itemChildren[i];
573                 if (locItemChild.isVisible() && locItemChild.isEnabled()) {
574                     var local = locItemChild.convertToNodeSpace(touchLocation);
575                     var r = locItemChild.rect();
576                     r.x = 0;
577                     r.y = 0;
578                     if (cc.rectContainsPoint(r, local))
579                         return locItemChild;
580                 }
581             }
582         }
583         return null;
584     }
585 });
586 
587 var _p = cc.Menu.prototype;
588 
589 // Extended properties
590 /** @expose */
591 _p.enabled;
592 
593 /**
594  * create a new menu
595  * @deprecated  since v3.0, please use new cc.Menu(menuitem1, menuitem2, menuitem3) to create a new menu
596  * @param {...cc.MenuItem|null} menuItems
597  * todo: need to use new
598  * @return {cc.Menu}
599  */
600 cc.Menu.create = function (menuItems) {
601     var argc = arguments.length;
602     if ((argc > 0) && (arguments[argc - 1] == null))
603         cc.log("parameters should not be ending with null in Javascript");
604 
605     var ret;
606     if (argc === 0)
607         ret = new cc.Menu();
608     else if (argc === 1)
609         ret = new cc.Menu(menuItems);
610     else
611         ret = new cc.Menu(Array.prototype.slice.call(arguments, 0));
612     return ret;
613 };
614