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 "cc.Menu.addChild() : Menu only supports MenuItem objects as children";
198         cc.Layer.prototype.addChild.call(this, child, zOrder, tag);
199     },
200 
201     /**
202      * align items vertically with default padding
203      */
204     alignItemsVertically: function () {
205         this.alignItemsVerticallyWithPadding(cc.DEFAULT_PADDING);
206     },
207 
208     /**
209      * align items vertically with specified padding
210      * @param {Number} padding
211      */
212     alignItemsVerticallyWithPadding: function (padding) {
213         var height = -padding, locChildren = this._children, len, i, locScaleY, locHeight, locChild;
214         if (locChildren && locChildren.length > 0) {
215             for (i = 0, len = locChildren.length; i < len; i++)
216                 height += locChildren[i].height * locChildren[i].scaleY + padding;
217 
218             var y = height / 2.0;
219 
220             for (i = 0, len = locChildren.length; i < len; i++) {
221                 locChild = locChildren[i];
222                 locHeight = locChild.height;
223                 locScaleY = locChild.scaleY;
224                 locChild.setPosition(0, y - locHeight * locScaleY / 2);
225                 y -= locHeight * locScaleY + padding;
226             }
227         }
228     },
229 
230     /**
231      * align items horizontally with default padding
232      */
233     alignItemsHorizontally: function () {
234         this.alignItemsHorizontallyWithPadding(cc.DEFAULT_PADDING);
235     },
236 
237     /**
238      * align items horizontally with specified padding
239      * @param {Number} padding
240      */
241     alignItemsHorizontallyWithPadding: function (padding) {
242         var width = -padding, locChildren = this._children, i, len, locScaleX, locWidth, locChild;
243         if (locChildren && locChildren.length > 0) {
244             for (i = 0, len = locChildren.length; i < len; i++)
245                 width += locChildren[i].width * locChildren[i].scaleX + padding;
246 
247             var x = -width / 2.0;
248 
249             for (i = 0, len = locChildren.length; i < len; i++) {
250                 locChild = locChildren[i];
251                 locScaleX = locChild.scaleX;
252                 locWidth = locChildren[i].width;
253                 locChild.setPosition(x + locWidth * locScaleX / 2, 0);
254                 x += locWidth * locScaleX + padding;
255             }
256         }
257     },
258 
259     /**
260      * align items in columns
261      * @example
262      * // Example
263      * menu.alignItemsInColumns(3,2,3)// this will create 3 columns, with 3 items for first column, 2 items for second and 3 for third
264      *
265      * menu.alignItemsInColumns(3,3)//this creates 2 columns, each have 3 items
266      */
267     alignItemsInColumns: function (/*Multiple Arguments*/) {
268         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
269             cc.log("parameters should not be ending with null in Javascript");
270 
271         var rows = [];
272         for (var i = 0; i < arguments.length; i++) {
273             rows.push(arguments[i]);
274         }
275         var height = -5;
276         var row = 0;
277         var rowHeight = 0;
278         var columnsOccupied = 0;
279         var rowColumns, tmp, len;
280         var locChildren = this._children;
281         if (locChildren && locChildren.length > 0) {
282             for (i = 0, len = locChildren.length; i < len; i++) {
283                 if (row >= rows.length)
284                     continue;
285 
286                 rowColumns = rows[row];
287                 // can not have zero columns on a row
288                 if (!rowColumns)
289                     continue;
290 
291                 tmp = locChildren[i].height;
292                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
293 
294                 ++columnsOccupied;
295                 if (columnsOccupied >= rowColumns) {
296                     height += rowHeight + 5;
297 
298                     columnsOccupied = 0;
299                     rowHeight = 0;
300                     ++row;
301                 }
302             }
303         }
304         // check if too many rows/columns for available menu items
305         //cc.assert(!columnsOccupied, "");    //?
306         var winSize = cc.director.getWinSize();
307 
308         row = 0;
309         rowHeight = 0;
310         rowColumns = 0;
311         var w = 0.0;
312         var x = 0.0;
313         var y = (height / 2);
314 
315         if (locChildren && locChildren.length > 0) {
316             for (i = 0, len = locChildren.length; i < len; i++) {
317                 var child = locChildren[i];
318                 if (rowColumns == 0) {
319                     rowColumns = rows[row];
320                     w = winSize.width / (1 + rowColumns);
321                     x = w;
322                 }
323 
324                 tmp = child._getHeight();
325                 rowHeight = ((rowHeight >= tmp || isNaN(tmp)) ? rowHeight : tmp);
326                 child.setPosition(x - winSize.width / 2, y - tmp / 2);
327 
328                 x += w;
329                 ++columnsOccupied;
330 
331                 if (columnsOccupied >= rowColumns) {
332                     y -= rowHeight + 5;
333                     columnsOccupied = 0;
334                     rowColumns = 0;
335                     rowHeight = 0;
336                     ++row;
337                 }
338             }
339         }
340     },
341     /**
342      * align menu items in rows
343      * @param {Number}
344      * @example
345      * // Example
346      * menu.alignItemsInRows(5,3)//this will align items to 2 rows, first row with 5 items, second row with 3
347      *
348      * menu.alignItemsInRows(4,4,4,4)//this creates 4 rows each have 4 items
349      */
350     alignItemsInRows: function (/*Multiple arguments*/) {
351         if ((arguments.length > 0) && (arguments[arguments.length - 1] == null))
352             cc.log("parameters should not be ending with null in Javascript");
353         var columns = [], i;
354         for (i = 0; i < arguments.length; i++) {
355             columns.push(arguments[i]);
356         }
357         var columnWidths = [];
358         var columnHeights = [];
359 
360         var width = -10;
361         var columnHeight = -5;
362         var column = 0;
363         var columnWidth = 0;
364         var rowsOccupied = 0;
365         var columnRows, child, len, tmp;
366 
367         var locChildren = this._children;
368         if (locChildren && locChildren.length > 0) {
369             for (i = 0, len = locChildren.length; i < len; i++) {
370                 child = locChildren[i];
371                 // check if too many menu items for the amount of rows/columns
372                 if (column >= columns.length)
373                     continue;
374 
375                 columnRows = columns[column];
376                 // can't have zero rows on a column
377                 if (!columnRows)
378                     continue;
379 
380                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
381                 tmp = child.width;
382                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
383 
384                 columnHeight += (child.height + 5);
385                 ++rowsOccupied;
386 
387                 if (rowsOccupied >= columnRows) {
388                     columnWidths.push(columnWidth);
389                     columnHeights.push(columnHeight);
390                     width += columnWidth + 10;
391 
392                     rowsOccupied = 0;
393                     columnWidth = 0;
394                     columnHeight = -5;
395                     ++column;
396                 }
397             }
398         }
399         // check if too many rows/columns for available menu items.
400         //cc.assert(!rowsOccupied, "");
401         var winSize = cc.director.getWinSize();
402 
403         column = 0;
404         columnWidth = 0;
405         columnRows = 0;
406         var x = -width / 2;
407         var y = 0.0;
408 
409         if (locChildren && locChildren.length > 0) {
410             for (i = 0, len = locChildren.length; i < len; i++) {
411                 child = locChildren[i];
412                 if (columnRows == 0) {
413                     columnRows = columns[column];
414                     y = columnHeights[column];
415                 }
416 
417                 // columnWidth = fmaxf(columnWidth, [item contentSize].width);
418                 tmp = child._getWidth();
419                 columnWidth = ((columnWidth >= tmp || isNaN(tmp)) ? columnWidth : tmp);
420 
421                 child.setPosition(x + columnWidths[column] / 2, y - winSize.height / 2);
422 
423                 y -= child.height + 10;
424                 ++rowsOccupied;
425 
426                 if (rowsOccupied >= columnRows) {
427                     x += columnWidth + 5;
428                     rowsOccupied = 0;
429                     columnRows = 0;
430                     columnWidth = 0;
431                     ++column;
432                 }
433             }
434         }
435     },
436 
437     /**
438      * remove a child from cc.Menu
439      * @param {cc.Node} child the child you want to remove
440      * @param {boolean} cleanup whether to cleanup
441      */
442     removeChild: function (child, cleanup) {
443         if (child == null)
444             return;
445         if (!(child instanceof cc.MenuItem)) {
446             cc.log("cc.Menu.removeChild():Menu only supports MenuItem objects as children");
447             return;
448         }
449 
450         if (this._selectedItem == child)
451             this._selectedItem = null;
452         cc.Node.prototype.removeChild.call(this, child, cleanup);
453     },
454 
455     _onTouchBegan: function (touch, event) {
456         var target = event.getCurrentTarget();
457         if (target._state != cc.MENU_STATE_WAITING || !target._visible || !target.enabled)
458             return false;
459 
460         for (var c = target.parent; c != null; c = c.parent) {
461             if (!c.isVisible())
462                 return false;
463         }
464 
465         target._selectedItem = target._itemForTouch(touch);
466         if (target._selectedItem) {
467             target._state = cc.MENU_STATE_TRACKING_TOUCH;
468             target._selectedItem.selected();
469             target._selectedItem.setNodeDirty();
470             return true;
471         }
472         return false;
473     },
474 
475     _onTouchEnded: function (touch, event) {
476         var target = event.getCurrentTarget();
477         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
478             cc.log("cc.Menu.onTouchEnded(): invalid state");
479             return;
480         }
481         if (target._selectedItem) {
482             target._selectedItem.unselected();
483             target._selectedItem.setNodeDirty();
484             target._selectedItem.activate();
485         }
486         target._state = cc.MENU_STATE_WAITING;
487     },
488 
489     _onTouchCancelled: function (touch, event) {
490         var target = event.getCurrentTarget();
491         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
492             cc.log("cc.Menu.onTouchCancelled(): invalid state");
493             return;
494         }
495         if (this._selectedItem) {
496             target._selectedItem.unselected();
497             target._selectedItem.setNodeDirty();
498         }
499         target._state = cc.MENU_STATE_WAITING;
500     },
501 
502     _onTouchMoved: function (touch, event) {
503         var target = event.getCurrentTarget();
504         if (target._state !== cc.MENU_STATE_TRACKING_TOUCH) {
505             cc.log("cc.Menu.onTouchMoved(): invalid state");
506             return;
507         }
508         var currentItem = target._itemForTouch(touch);
509         if (currentItem != target._selectedItem) {
510             if (target._selectedItem) {
511                 target._selectedItem.unselected();
512                 target._selectedItem.setNodeDirty();
513             }
514             target._selectedItem = currentItem;
515             if (target._selectedItem) {
516                 target._selectedItem.selected();
517                 target._selectedItem.setNodeDirty();
518             }
519         }
520     },
521 
522     /**
523      * <p>
524      * callback that is called every time the cc.Menu leaves the 'stage'.                                         <br/>
525      * If the cc.Menu leaves the 'stage' with a transition, this callback is called when the transition finishes. <br/>
526      * During onExit you can't access a sibling node.                                                             <br/>
527      * If you override onExit, you shall call its parent's onExit with this._super().
528      * </p>
529      */
530     onExit: function () {
531         if (this._state == cc.MENU_STATE_TRACKING_TOUCH) {
532             if (this._selectedItem) {
533                 this._selectedItem.unselected();
534                 this._selectedItem = null;
535             }
536             this._state = cc.MENU_STATE_WAITING;
537         }
538         cc.Node.prototype.onExit.call(this);
539     },
540     /**
541      * only use for jsbinding
542      * @param value
543      */
544     setOpacityModifyRGB: function (value) {
545     },
546     /**
547      * only use for jsbinding
548       * @returns {boolean}
549      */
550     isOpacityModifyRGB: function () {
551         return false;
552     },
553 
554     _itemForTouch: function (touch) {
555         var touchLocation = touch.getLocation();
556         var itemChildren = this._children, locItemChild;
557         if (itemChildren && itemChildren.length > 0) {
558             for (var i = itemChildren.length - 1; i >= 0; i--) {
559                 locItemChild = itemChildren[i];
560                 if (locItemChild.isVisible() && locItemChild.isEnabled()) {
561                     var local = locItemChild.convertToNodeSpace(touchLocation);
562                     var r = locItemChild.rect();
563                     r.x = 0;
564                     r.y = 0;
565                     if (cc.rectContainsPoint(r, local))
566                         return locItemChild;
567                 }
568             }
569         }
570         return null;
571     }
572 });
573 
574 var _p = cc.Menu.prototype;
575 
576 // Extended properties
577 /** @expose */
578 _p.enabled;
579 
580 /**
581  * create a new menu
582  * @deprecated  since v3.0, please use new cc.Menu(menuitem1, menuitem2, menuitem3) to create a new menu
583  * @param {...cc.MenuItem|null} menuItems
584  * todo: need to use new
585  * @return {cc.Menu}
586  */
587 cc.Menu.create = function (menuItems) {
588     var argc = arguments.length;
589     if ((argc > 0) && (arguments[argc - 1] == null))
590         cc.log("parameters should not be ending with null in Javascript");
591 
592     var ret;
593     if (argc == 0)
594         ret = new cc.Menu();
595     else if (argc == 1)
596         ret = new cc.Menu(menuItems);
597     else
598         ret = new cc.Menu(Array.prototype.slice.call(arguments, 0));
599     return ret;
600 };
601