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