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     _color: null,
 62     _opacity: 0,
 63     _selectedItem: null,
 64     _state: -1,
 65     _touchListener: null,
 66     _className: "Menu",
 67 
 68     /**
 69      * Constructor of cc.Menu override it to extend the construction behavior, remember to call "this._super()" in the extended "ctor" function.
 70      * @param {...cc.MenuItem|null} menuItems}
 71      */
 72     ctor: function (menuItems) {
 73         cc.Layer.prototype.ctor.call(this);
 74         this._color = cc.color.WHITE;
 75         this.enabled = false;
 76         this._opacity = 255;
 77         this._selectedItem = null;
 78         this._state = -1;
 79 
 80         this._touchListener = cc.EventListener.create({
 81             event: cc.EventListener.TOUCH_ONE_BY_ONE,
 82             swallowTouches: true,
 83             onTouchBegan: this._onTouchBegan,
 84             onTouchMoved: this._onTouchMoved,
 85             onTouchEnded: this._onTouchEnded,
 86             onTouchCancelled: this._onTouchCancelled
 87         });
 88 
 89         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
 90             cc.log("parameters should not be ending with null in Javascript");
 91 
 92         var argc = arguments.length, items;
 93         if (argc == 0) {
 94             items = [];
 95         } else if (argc == 1) {
 96             if (menuItems instanceof Array) {
 97                 items = menuItems;
 98             }
 99             else items = [menuItems];
100         }
101         else if (argc > 1) {
102             items = [];
103             for (var i = 0; i < argc; i++) {
104                 if (arguments[i])
105                     items.push(arguments[i]);
106             }
107         }
108         this.initWithArray(items);
109     },
110     /**
111      * <p>
112      *     Event callback that is invoked every time when CCMenu enters the 'stage'.                                   <br/>
113      *     If the CCMenu enters the 'stage' with a transition, this event is called when the transition starts.        <br/>
114      *     During onEnter you can't access a "sister/brother" node.                                                    <br/>
115      *     If you override onEnter, you must call its parent's onEnter function with this._super().
116      * </p>
117      */
118     onEnter: function () {
119         var locListener = this._touchListener;
120         if (!locListener._isRegistered())
121             cc.eventManager.addListener(locListener, this);
122         cc.Node.prototype.onEnter.call(this);
123     },
124 
125     /**
126      * return the color for cc.Menu
127      * @return {cc.Color}
128      */
129     getColor: function () {
130         var locColor = this._color;
131         return cc.color(locColor.r, locColor.g, locColor.b, locColor.a);
132     },
133 
134     /**
135      * set the color for cc.Menu
136      * @param {cc.Color} color
137      */
138     setColor: function (color) {
139         var locColor = this._color;
140         locColor.r = color.r;
141         locColor.g = color.g;
142         locColor.b = color.b;
143 
144         var locChildren = this._children;
145         if (locChildren && locChildren.length > 0) {
146             for (var i = 0; i < locChildren.length; i++) {
147                 locChildren[i].setColor(color);
148             }
149         }
150 
151         if (color.a !== undefined && !color.a_undefined) {
152             this.setOpacity(color.a);
153         }
154     },
155 
156     /**
157      * return the opacity for this menu
158      * @return {Number}
159      */
160     getOpacity: function () {
161         return this._opacity;
162     },
163 
164     /**
165      * set the opacity for this menu
166      * @param {Number} opa
167      */
168     setOpacity: function (opa) {
169         this._opacity = opa;
170         var locChildren = this._children;
171         if (locChildren && locChildren.length > 0) {
172             for (var i = 0; i < locChildren.length; i++)
173                 locChildren[i].setOpacity(opa);
174         }
175         this._color.a = opa;
176     },
177 
178     /**
179      * return whether or not the menu will receive events
180      * @return {Boolean}
181      */
182     isEnabled: function () {
183         return this.enabled;
184     },
185 
186     /**
187      * set whether or not the menu will receive events
188      * @param {Boolean} enabled
189      */
190     setEnabled: function (enabled) {
191         this.enabled = enabled;
192     },
193 
194     /**
195      * initializes a cc.Menu with it's items
196      * @param {Array} args
197      * @return {Boolean}
198      */
199     initWithItems: function (args) {
200         var pArray = [];
201         if (args) {
202             for (var i = 0; i < args.length; i++) {
203                 if (args[i])
204                     pArray.push(args[i]);
205             }
206         }
207 
208         return this.initWithArray(pArray);
209     },
210 
211     /**
212      * initializes a cc.Menu with a Array of cc.MenuItem objects
213      * @param {Array} array Of cc.MenuItem Items
214      * @return {Boolean}
215      */
216     initWithArray: function (arrayOfItems) {
217         if (cc.Layer.prototype.init.call(this)) {
218             this.enabled = true;
219 
220             // menu in the center of the screen
221             var winSize = cc.winSize;
222             this.setPosition(winSize.width / 2, winSize.height / 2);
223             this.setContentSize(winSize);
224             this.setAnchorPoint(0.5, 0.5);
225             this.ignoreAnchorPointForPosition(true);
226 
227             if (arrayOfItems) {
228                 for (var i = 0; i < arrayOfItems.length; i++)
229                     this.addChild(arrayOfItems[i], i);
230             }
231 
232             this._selectedItem = null;
233             this._state = cc.MENU_STATE_WAITING;
234 
235             // enable cascade color and opacity on menus
236             this.cascadeColor = true;
237             this.cascadeOpacity = true;
238 
239             return true;
240         }
241         return false;
242     },
243 
244     /**
245      * add a child for  cc.Menu
246      * @param {cc.Node} child
247      * @param {Number|Null} [zOrder=] zOrder for the child
248      * @param {Number|Null} [tag=] tag for the child
249      */
250     addChild: function (child, zOrder, tag) {
251         if (!(child instanceof cc.MenuItem))
252             throw "cc.Menu.addChild() : Menu only supports MenuItem objects as children";
253         cc.Layer.prototype.addChild.call(this, child, zOrder, tag);
254     },
255 
256     /**
257      * align items vertically with default padding
258      */
259     alignItemsVertically: function () {
260         this.alignItemsVerticallyWithPadding(cc.DEFAULT_PADDING);
261     },
262 
263     /**
264      * align items vertically with specified padding
265      * @param {Number} padding
266      */
267     alignItemsVerticallyWithPadding: function (padding) {
268         var height = -padding, locChildren = this._children, len, i, locScaleY, locHeight, locChild;
269         if (locChildren && locChildren.length > 0) {
270             for (i = 0, len = locChildren.length; i < len; i++)
271                 height += locChildren[i].height * locChildren[i].scaleY + padding;
272 
273             var y = height / 2.0;
274 
275             for (i = 0, len = locChildren.length; i < len; i++) {
276                 locChild = locChildren[i];
277                 locHeight = locChild.height;
278                 locScaleY = locChild.scaleY;
279                 locChild.setPosition(0, y - locHeight * locScaleY / 2);
280                 y -= locHeight * locScaleY + padding;
281             }
282         }
283     },
284 
285     /**
286      * align items horizontally with default padding
287      */
288     alignItemsHorizontally: function () {
289         this.alignItemsHorizontallyWithPadding(cc.DEFAULT_PADDING);
290     },
291 
292     /**
293      * align items horizontally with specified padding
294      * @param {Number} padding
295      */
296     alignItemsHorizontallyWithPadding: function (padding) {
297         var width = -padding, locChildren = this._children, i, len, locScaleX, locWidth, locChild;
298         if (locChildren && locChildren.length > 0) {
299             for (i = 0, len = locChildren.length; i < len; i++)
300                 width += locChildren[i].width * locChildren[i].scaleX + padding;
301 
302             var x = -width / 2.0;
303 
304             for (i = 0, len = locChildren.length; i < len; i++) {
305                 locChild = locChildren[i];
306                 locScaleX = locChild.scaleX;
307                 locWidth = locChildren[i].width;
308                 locChild.setPosition(x + locWidth * locScaleX / 2, 0);
309                 x += locWidth * locScaleX + padding;
310             }
311         }
312     },
313 
314     /**
315      * align items in columns
316      * @example
317      * // Example
318      * menu.alignItemsInColumns(3,2,3)// this will create 3 columns, with 3 items for first column, 2 items for second and 3 for third
319      *
320      * menu.alignItemsInColumns(3,3)//this creates 2 columns, each have 3 items
321      */
322     alignItemsInColumns: function (/*Multiple Arguments*/) {
323         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
324             cc.log("parameters should not be ending with null in Javascript");
325 
326         var rows = [];
327         for (var i = 0; i < arguments.length; i++) {
328             rows.push(arguments[i]);
329         }
330         var height = -5;
331         var row = 0;
332         var rowHeight = 0;
333         var columnsOccupied = 0;
334         var rowColumns, tmp, len;
335         var locChildren = this._children;
336         if (locChildren && locChildren.length > 0) {
337             for (i = 0, len = locChildren.length; i < len; i++) {
338                 if (row >= rows.length)
339                     continue;
340 
341                 rowColumns = rows[row];
342                 // can not have zero columns on a row
343                 if (!rowColumns)
344                     continue;
345 
346                 tmp = locChildren[i].height;
347                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
348 
349                 ++columnsOccupied;
350                 if (columnsOccupied >= rowColumns) {
351                     height += rowHeight + 5;
352 
353                     columnsOccupied = 0;
354                     rowHeight = 0;
355                     ++row;
356                 }
357             }
358         }
359         // check if too many rows/columns for available menu items
360         //cc.assert(!columnsOccupied, "");    //?
361         var winSize = cc.director.getWinSize();
362 
363         row = 0;
364         rowHeight = 0;
365         rowColumns = 0;
366         var w = 0.0;
367         var x = 0.0;
368         var y = (height / 2);
369 
370         if (locChildren && locChildren.length > 0) {
371             for (i = 0, len = locChildren.length; i < len; i++) {
372                 var child = locChildren[i];
373                 if (rowColumns == 0) {
374                     rowColumns = rows[row];
375                     w = winSize.width / (1 + rowColumns);
376                     x = w;
377                 }
378 
379                 tmp = child._getHeight();
380                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
381                 child.setPosition(x - winSize.width / 2, y - tmp / 2);
382 
383                 x += w;
384                 ++columnsOccupied;
385 
386                 if (columnsOccupied >= rowColumns) {
387                     y -= rowHeight + 5;
388                     columnsOccupied = 0;
389                     rowColumns = 0;
390                     rowHeight = 0;
391                     ++row;
392                 }
393             }
394         }
395     },
396     /**
397      * align menu items in rows
398      * @param {Number}
399      * @example
400      * // Example
401      * menu.alignItemsInRows(5,3)//this will align items to 2 rows, first row with 5 items, second row with 3
402      *
403      * menu.alignItemsInRows(4,4,4,4)//this creates 4 rows each have 4 items
404      */
405     alignItemsInRows: function (/*Multiple arguments*/) {
406         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
407             cc.log("parameters should not be ending with null in Javascript");
408         var columns = [], i;
409         for (i = 0; i < arguments.length; i++) {
410             columns.push(arguments[i]);
411         }
412         var columnWidths = [];
413         var columnHeights = [];
414 
415         var width = -10;
416         var columnHeight = -5;
417         var column = 0;
418         var columnWidth = 0;
419         var rowsOccupied = 0;
420         var columnRows, child, len, tmp;
421 
422         var locChildren = this._children;
423         if (locChildren && locChildren.length > 0) {
424             for (i = 0, len = locChildren.length; i < len; i++) {
425                 child = locChildren[i];
426                 // check if too many menu items for the amount of rows/columns
427                 if (column >= columns.length)
428                     continue;
429 
430                 columnRows = columns[column];
431                 // can't have zero rows on a column
432                 if (!columnRows)
433                     continue;
434 
435                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
436                 tmp = child.width;
437                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
438 
439                 columnHeight += (child.height + 5);
440                 ++rowsOccupied;
441 
442                 if (rowsOccupied >= columnRows) {
443                     columnWidths.push(columnWidth);
444                     columnHeights.push(columnHeight);
445                     width += columnWidth + 10;
446 
447                     rowsOccupied = 0;
448                     columnWidth = 0;
449                     columnHeight = -5;
450                     ++column;
451                 }
452             }
453         }
454         // check if too many rows/columns for available menu items.
455         //cc.assert(!rowsOccupied, "");
456         var winSize = cc.director.getWinSize();
457 
458         column = 0;
459         columnWidth = 0;
460         columnRows = 0;
461         var x = -width / 2;
462         var y = 0.0;
463 
464         if (locChildren && locChildren.length > 0) {
465             for (i = 0, len = locChildren.length; i < len; i++) {
466                 child = locChildren[i];
467                 if (columnRows == 0) {
468                     columnRows = columns[column];
469                     y = columnHeights[column];
470                 }
471 
472                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
473                 tmp = child._getWidth();
474                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
475 
476                 child.setPosition(x + columnWidths[column] / 2, y - winSize.height / 2);
477 
478                 y -= child.height + 10;
479                 ++rowsOccupied;
480 
481                 if (rowsOccupied >= columnRows) {
482                     x += columnWidth + 5;
483                     rowsOccupied = 0;
484                     columnRows = 0;
485                     columnWidth = 0;
486                     ++column;
487                 }
488             }
489         }
490     },
491 
492     /**
493      * remove a child from cc.Menu
494      * @param {cc.Node} child the child you want to remove
495      * @param {boolean} cleanup whether to cleanup
496      */
497     removeChild: function (child, cleanup) {
498         if (child == null)
499             return;
500         if (!(child instanceof cc.MenuItem)) {
501             cc.log("cc.Menu.removeChild():Menu only supports MenuItem objects as children");
502             return;
503         }
504 
505         if (this._selectedItem == child)
506             this._selectedItem = null;
507         cc.Node.prototype.removeChild.call(this, child, cleanup);
508     },
509 
510     _onTouchBegan: function (touch, event) {
511         var target = event.getCurrentTarget();
512         if (target._state != cc.MENU_STATE_WAITING || !target._visible || !target.enabled)
513             return false;
514 
515         for (var c = target.parent; c != null; c = c.parent) {
516             if (!c.isVisible())
517                 return false;
518         }
519 
520         target._selectedItem = target._itemForTouch(touch);
521         if (target._selectedItem) {
522             target._state = cc.MENU_STATE_TRACKING_TOUCH;
523             target._selectedItem.selected();
524             return true;
525         }
526         return false;
527     },
528 
529     _onTouchEnded: function (touch, event) {
530         var target = event.getCurrentTarget();
531         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
532             cc.log("cc.Menu.onTouchEnded(): invalid state");
533             return;
534         }
535         if (target._selectedItem) {
536             target._selectedItem.unselected();
537             target._selectedItem.activate();
538         }
539         target._state = cc.MENU_STATE_WAITING;
540     },
541 
542     _onTouchCancelled: function (touch, event) {
543         var target = event.getCurrentTarget();
544         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
545             cc.log("cc.Menu.onTouchCancelled(): invalid state");
546             return;
547         }
548         if (this._selectedItem)
549             target._selectedItem.unselected();
550         target._state = cc.MENU_STATE_WAITING;
551     },
552 
553     _onTouchMoved: function (touch, event) {
554         var target = event.getCurrentTarget();
555         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
556             cc.log("cc.Menu.onTouchMoved(): invalid state");
557             return;
558         }
559         var currentItem = target._itemForTouch(touch);
560         if (currentItem != target._selectedItem) {
561             if (target._selectedItem)
562                 target._selectedItem.unselected();
563             target._selectedItem = currentItem;
564             if (target._selectedItem)
565                 target._selectedItem.selected();
566         }
567     },
568 
569     /**
570      * <p>
571      * callback that is called every time the cc.Menu leaves the 'stage'.                                         <br/>
572      * If the cc.Menu leaves the 'stage' with a transition, this callback is called when the transition finishes. <br/>
573      * During onExit you can't access a sibling node.                                                             <br/>
574      * If you override onExit, you shall call its parent's onExit with this._super().
575      * </p>
576      */
577     onExit: function () {
578         if (this._state == cc.MENU_STATE_TRACKING_TOUCH) {
579             if (this._selectedItem) {
580                 this._selectedItem.unselected();
581                 this._selectedItem = null;
582             }
583             this._state = cc.MENU_STATE_WAITING;
584         }
585         cc.Node.prototype.onExit.call(this);
586     },
587     /**
588      * only use for jsbinding
589      * @param value
590      */
591     setOpacityModifyRGB: function (value) {
592     },
593     /**
594      * only use for jsbinding
595       * @returns {boolean}
596      */
597     isOpacityModifyRGB: function () {
598         return false;
599     },
600 
601     _itemForTouch: function (touch) {
602         var touchLocation = touch.getLocation();
603         var itemChildren = this._children, locItemChild;
604         if (itemChildren && itemChildren.length > 0) {
605             for (var i = 0; i < itemChildren.length; i++) {
606                 locItemChild = itemChildren[i];
607                 if (locItemChild.isVisible() && locItemChild.isEnabled()) {
608                     var local = locItemChild.convertToNodeSpace(touchLocation);
609                     var r = locItemChild.rect();
610                     r.x = 0;
611                     r.y = 0;
612                     if (cc.rectContainsPoint(r, local))
613                         return locItemChild;
614                 }
615             }
616         }
617         return null;
618     }
619 });
620 
621 var _p = cc.Menu.prototype;
622 
623 // Extended properties
624 /** @expose */
625 _p.enabled;
626 
627 /**
628  * create a new menu
629  * @deprecated  since v3.0, please use new cc.Menu(menuitem1, menuitem2, menuitem3) to create a new menu
630  * @param {...cc.MenuItem|null} menuItems
631  * @return {cc.Menu}
632  * @example
633  * // Example
634  * //there is no limit on how many menu item you can pass in
635  * var myMenu = cc.Menu.create(menuitem1, menuitem2, menuitem3);
636  */
637 cc.Menu.create = function (menuItems) {
638     var argc = arguments.length;
639     if ((argc > 0) && (arguments[argc - 1] == null))
640         cc.log("parameters should not be ending with null in Javascript");
641 
642     var ret;
643     if (argc == 0)
644         ret = new cc.Menu();
645     else if (argc == 1)
646         ret = new cc.Menu(menuItems);
647     else
648         ret = new cc.Menu(Array.prototype.slice.call(arguments, 0));
649     return ret;
650 };
651