1
# ----------------------------------------------------------------------------
3
# Copyright (c) 2008 Daniel Moisset, Ricardo Quesada, Rayentray Tappa, Lucio Torre
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
9
# * Redistributions of source code must retain the above copyright
10
# notice, this list of conditions and the following disclaimer.
11
# * Redistributions in binary form must reproduce the above copyright
12
# notice, this list of conditions and the following disclaimer in
13
# the documentation and/or other materials provided with the
15
# * Neither the name of cocos2d nor the names of its
16
# contributors may be used to endorse or promote products
17
# derived from this software without specific prior written
20
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
# POSSIBILITY OF SUCH DAMAGE.
32
# ----------------------------------------------------------------------------
34
# Ideas borrowed from:
35
# pygext: http://opioid-interactive.com/~shang/projects/pygext/
36
# pyglet astraea: http://www.pyglet.org
37
# Grossini's Hell: http://www.pyweek.org/e/Pywiii/
39
"""A `Layer` that implements a simple menu
44
This module provides a Menu class. Menus can contain regular items
45
(which trigger a function when selected), toggle items (which toggle a flag when selected),
46
or entry items (which lets you enter alphanumeric data).
48
To use a menu in your code, just subclass `Menu` and add the menu to an `Scene` or
52
__docformat__ = 'restructuredtext'
55
from pyglet import font
56
from pyglet.window import key
57
from pyglet.gl import *
58
import pyglet.graphics
61
from director import *
62
from cocosnode import *
64
from sprite import Sprite
67
__all__ = [ 'Menu', # menu class
69
'MenuItem', 'ToggleMenuItem', # menu items classes
70
'MultipleMenuItem', 'EntryMenuItem', 'ImageMenuItem',
72
'verticalMenuLayout', 'fixedPositionMenuLayout', # Different menu layou functions
73
'CENTER', 'LEFT', 'RIGHT', 'TOP', 'BOTTOM', # menu aligment
75
'shake', 'shake_back','zoom_in','zoom_out' # Some useful actions for the menu items
83
CENTER = font.Text.CENTER
85
RIGHT = font.Text.RIGHT
89
BOTTOM = font.Text.BOTTOM
91
def verticalMenuLayout (menu):
92
width, height = director.get_window_size()
93
fo = font.load(menu.font_item['font_name'], menu.font_item['font_size'])
94
fo_height = int( (fo.ascent - fo.descent) * 0.9 )
96
if menu.menu_halign == CENTER:
98
elif menu.menu_halign == RIGHT:
99
pos_x = width - menu.menu_hmargin
100
elif menu.menu_halign == LEFT:
101
pos_x = menu.menu_hmargin
103
raise Exception("Invalid anchor_x value for menu")
105
for idx,i in enumerate( menu.children):
107
if menu.menu_valign == CENTER:
108
pos_y = (height + (len(menu.children) - 2 * idx)
109
* fo_height - menu.title_height) * 0.5
110
elif menu.menu_valign == TOP:
111
pos_y = (height - (idx * fo_height )
112
- menu.title_height - menu.menu_vmargin)
113
elif menu.menu_valign == BOTTOM:
114
pos_y = (0 + fo_height * (len(menu.children) - idx) +
116
item.transform_anchor = (pos_x, pos_y)
117
item.generateWidgets (pos_x, pos_y, menu.font_item,
118
menu.font_item_selected)
120
def fixedPositionMenuLayout (positions):
121
def fixedMenuLayout(menu):
122
width, height = director.get_window_size()
123
for idx,i in enumerate( menu.children):
125
pos_x = positions[idx][0]
126
pos_y = positions[idx][1]
127
item.transform_anchor = (pos_x, pos_y)
128
item.generateWidgets (pos_x, pos_y, menu.font_item,
129
menu.font_item_selected)
130
return fixedMenuLayout
133
"""Abstract base class for menu layers.
138
- override __init__ to set all style attributes,
139
and then call `create_menu()`
140
- Finally you shall add the menu to an `Scene` or another `Layer`
143
is_event_handler = True #: Receives pyglet events
146
activate_sound = None
147
def __init__( self, title = ''):
148
super(Menu, self).__init__()
154
self.title_text = None
156
self.menu_halign = CENTER
157
self.menu_valign = CENTER
159
self.menu_hmargin = 2 # Variable margins for left and right alignment
160
self.menu_vmargin = 2 # Variable margins for top and bottom alignment
163
# Menu default options
164
# Menus can be customized changing these variables
172
'color':(192,192,192,255),
188
'color':(192,192,192,255),
191
self.font_item_selected = {
198
'color':(255,255,255,255),
202
self.title_height = 0
203
self.schedule(lambda dt: None)
206
def _generate_title( self ):
207
width, height = director.get_window_size()
209
self.font_title['x'] = width // 2
210
self.font_title['text'] = self.title
211
self.title_label = pyglet.text.Label( **self.font_title )
212
self.title_label.y = height - self.title_label.content_height //2
214
fo = font.load( self.font_title['font_name'], self.font_title['font_size'] )
215
self.title_height = self.title_label.content_height
217
def _build_items(self, layout_strategy):
218
self.font_item_selected['anchor_x'] = self.menu_halign
219
self.font_item_selected['anchor_y'] = 'center'
221
self.font_item['anchor_x'] = self.menu_halign
222
self.font_item['anchor_y'] = 'center'
224
layout_strategy(self)
225
self.selected_index = 0
226
self.children[ self.selected_index ][1].is_selected = True
228
def _select_item(self, new_idx):
229
if new_idx == self.selected_index:
232
if self.select_sound:
233
self.select_sound.play()
235
self.children[ self.selected_index][1].is_selected = False
236
self.children[ self.selected_index][1].on_unselected()
238
self.children[ new_idx ][1].is_selected = True
239
self.children[ new_idx ][1].on_selected()
241
self.selected_index = new_idx
243
def _activate_item( self ):
244
if self.activate_sound:
245
self.activate_sound.play()
246
self.children[ self.selected_index][1].on_activated()
247
self.children[ self.selected_index ][1].on_key_press( key.ENTER, 0 )
249
def create_menu(self, items, selected_effect=None, unselected_effect=None,
250
activated_effect=None, layout_strategy=verticalMenuLayout):
253
The order of the list important since the
254
first one will be shown first.
259
l.append( MenuItem('Options', self.on_new_game ) )
260
l.append( MenuItem('Quit', self.on_quit ) )
261
self.create_menu( l, zoom_in(), zoom_out() )
265
list of `BaseMenuItem` that will be part of the `Menu`
266
`selected_effect` : function
267
This action will be executed when the `BaseMenuItem` is selected
268
`unselected_effect` : function
269
This action will be executed when the `BaseMenuItem` is unselected
270
`activated_effect` : function
271
this action will executed when the `BaseMenuItem` is activated (pressing Enter or by clicking on it)
275
# calling super.add(). Z is important to mantain order
278
i.activated_effect = activated_effect
279
i.selected_effect = selected_effect
280
i.unselected_effect = unselected_effect
281
i.item_halign = self.menu_halign
282
i.item_valign = self.menu_valign
285
self._generate_title() # If you generate the title after the items
286
# the V position of the items can't consider the title's height
288
self._build_items(layout_strategy)
291
self.title_label.draw()
293
def on_text( self, text ):
296
return self.children[self.selected_index][1].on_text(text)
298
def on_key_press(self, symbol, modifiers):
299
if symbol == key.ESCAPE:
302
elif symbol in (key.ENTER, key.NUM_ENTER):
303
self._activate_item()
305
elif symbol in (key.DOWN, key.UP):
306
if symbol == key.DOWN:
307
new_idx = self.selected_index + 1
308
elif symbol == key.UP:
309
new_idx = self.selected_index - 1
312
new_idx = len(self.children) -1
313
elif new_idx > len(self.children) -1:
315
self._select_item( new_idx )
318
# send the menu item the rest of the keys
319
ret = self.children[self.selected_index][1].on_key_press(symbol, modifiers)
321
# play sound if key was handled
322
if ret and self.activate_sound:
323
self.activate_sound.play()
326
def on_mouse_release( self, x, y, buttons, modifiers ):
327
(x,y) = director.get_virtual_coordinates(x,y)
328
if self.children[ self.selected_index ][1].is_inside_box(x,y):
329
self._activate_item()
331
def on_mouse_motion( self, x, y, dx, dy ):
332
(x,y) = director.get_virtual_coordinates(x,y)
333
for idx,i in enumerate( self.children):
335
if item.is_inside_box( x, y):
336
self._select_item( idx )
340
class BaseMenuItem( CocosNode ):
341
"""An abstract menu item. It triggers a function when it is activated"""
343
selected_effect = None
344
unselected_effect = None
345
activated_effect = None
347
def __init__(self, callback_func, *args, **kwargs):
348
"""Creates a new menu item
351
`callback_func` : function
352
The callback function
355
super( BaseMenuItem, self).__init__()
357
self.callback_func = callback_func
358
self.callback_args = args
359
self.callback_kwargs = kwargs
361
self.is_selected = False
363
self.item_halign = None
364
self.item_valign = None
367
self.item_selected = None
369
def get_item_width (self):
370
""" Returns the width of the item.
371
This method should be implemented by descendents.
375
return self.item.width
377
def get_item_height (self):
378
""" Returns the width of the item.
379
This method should be implemented by descendents.
383
return self.item.height
385
def generateWidgets (self, pos_x, pos_y, font_item, font_item_selected):
386
""" Generate a normal and a selected widget.
387
This method should be implemented by descendents.
389
raise NotImplementedError
391
def get_item_x (self):
392
""" Return the x position of the item.
393
This method should be implemented by descendents.
399
def get_item_y (self):
400
""" Return the y position of the item.
401
This method should be implemented by descendents.
408
"""Returns the box that contains the menu item.
410
:rtype: (x1,x2,y1,y2)
412
width = self.get_item_width ()
413
height = self.get_item_height ()
414
if self.item_halign == CENTER:
416
elif self.item_halign == RIGHT:
418
elif self.item_halign == LEFT:
421
raise Exception("Invalid halign: %s" % str(self.item_halign) )
425
x1 = self.get_item_x() + x_diff
426
y1 = self.get_item_y() + y_diff
427
# x1 += self.parent.x
428
# y1 += self.parent.y
431
# return (x1,y1,x2,y2)
432
return rect.Rect(x1,y1,width,height)
435
raise NotImplementedError
437
def on_key_press(self, symbol, modifiers):
438
if symbol == key.ENTER and self.callback_func:
439
self.callback_func(*self.callback_args, **self.callback_kwargs)
442
def on_text( self, text ):
445
def is_inside_box( self, x, y ):
446
"""Returns whether the point (x,y) is inside the menu item.
450
# (ax,ay,bx,by) = self.get_box()
451
# if( x >= ax and x <= bx and y >= ay and y <= by ):
454
rect = self.get_box()
455
p = self.point_to_local( (x,y) )
456
return rect.contains( p.x, p.y )
459
def on_selected( self ):
460
if self.selected_effect:
462
self.do( self.selected_effect )
464
def on_unselected( self ):
465
if self.unselected_effect:
467
self.do( self.unselected_effect )
469
def on_activated( self ):
470
if self.activated_effect:
472
self.do( self.activated_effect )
474
class MenuItem (BaseMenuItem):
475
"""A menu item that shows a label. """
476
def __init__ (self, label, callback_func, *args, **kwargs):
477
"""Creates a new menu item
481
The label the of the menu item
482
`callback_func` : function
483
The callback function
486
super (MenuItem, self).__init__(callback_func, *args, **kwargs)
488
def get_item_width (self):
489
return self.item.content_width
491
def get_item_height (self):
492
return self.item.content_height
494
def generateWidgets (self, pos_x, pos_y, font_item, font_item_selected):
495
font_item['x'] = int(pos_x)
496
font_item['y'] = int(pos_y)
497
font_item['text'] = self.label
498
self.item = pyglet.text.Label(**font_item )
499
font_item_selected['x'] = int(pos_x)
500
font_item_selected['y'] = int(pos_y)
501
font_item_selected['text'] = self.label
502
self.item_selected = pyglet.text.Label( **font_item_selected )
508
self.item_selected.draw()
514
class ImageMenuItem (BaseMenuItem):
515
""" A menu item that shows a selectable Image """
516
def __init__ (self, name, callback_func, *args, **kwargs):
517
self.image = pyglet.resource.image (name)
518
super (ImageMenuItem, self).__init__(callback_func, *args, **kwargs)
520
def generateWidgets (self, pos_x, pos_y, font_item, font_item_selected):
521
anchors = {'left': 0, 'center': 0.5, 'right': 1, 'top': 1, 'bottom': 0}
522
anchor=(anchors[font_item['anchor_x']] * self.image.width,
523
anchors[font_item['anchor_y']] * self.image.height)
524
self.item = Sprite(self.image, anchor=anchor, opacity=255,
525
color=font_item['color'][:3])
526
self.item.scale = font_item['font_size'] / float(self.item.height )
527
self.item.position = int(pos_x), int(pos_y)
528
self.selected_item = Sprite(self.image, anchor=anchor,
529
color=font_item_selected['color'][:3])
530
self.selected_item.scale = (font_item_selected['font_size'] /
531
float(self.selected_item.height))
532
self.selected_item.position = int(pos_x), int(pos_y)
538
self.selected_item.draw()
544
class MultipleMenuItem( MenuItem ):
545
"""A menu item for switching between multiple values.
549
self.volumes = ['Mute','10','20','30','40','50','60','70','80','90','100']
551
items.append( MultipleMenuItem(
558
def __init__(self, label, callback_func, items, default_item=0):
559
"""Creates a Multiple Menu Item
564
`callback_func` : function
567
List of strings containing the values
568
`default_item` : integer
569
Default item of the list. It is an index of the list. Default: 0
571
self.my_label = label
573
self.idx = default_item
574
if self.idx < 0 or self.idx >= len(self.items):
575
raise Exception("Index out of bounds")
576
super( MultipleMenuItem, self).__init__( self._get_label(), callback_func )
578
def _get_label(self):
579
return self.my_label+self.items[self.idx]
581
def on_key_press(self, symbol, modifiers):
582
if symbol == key.LEFT:
583
self.idx = max(0, self.idx-1)
584
elif symbol in (key.RIGHT, key.ENTER):
585
self.idx = min(len(self.items)-1, self.idx+1)
587
if symbol in (key.LEFT, key.RIGHT, key.ENTER):
588
self.item.text = self._get_label()
589
self.item_selected.text = self._get_label()
590
self.callback_func( self.idx )
593
class ToggleMenuItem( MultipleMenuItem ):
594
'''A menu item for a boolean toggle option.
598
items.append( ToggleMenuItem('Show FPS:', self.on_show_fps, director.show_FPS) )
601
def __init__(self, label, callback_func, value=False ):
602
"""Creates a Toggle Menu Item
607
`callback_func` : function
610
Default value of the item: False is 'OFF', True is 'ON'. Default:False
613
super(ToggleMenuItem, self).__init__( label, callback_func, ['OFF','ON'], int(value) )
615
def on_key_press( self, symbol, mod ):
616
if symbol in (key.LEFT, key.RIGHT, key.ENTER):
620
self.item.text = self._get_label()
621
self.item_selected.text = self._get_label()
622
self.callback_func( int(self.idx) )
625
class EntryMenuItem(MenuItem):
626
"""A menu item for entering a value.
628
When selected, ``self.value`` is toggled, the callback function is
629
called with ``self.value`` as argument."""
631
value = property(lambda self: u''.join(self._value),
632
lambda self, v: setattr(self, '_value', list(v)))
634
def __init__(self, label, callback_func, value, max_length=0 ):
635
"""Creates an Entry Menu Item
640
`callback_func` : function
641
Callback function taking one argument.
643
Default value: any string
644
`max_length` : integer
645
Maximum value length (Defaults to 0 for unbound length)
647
self._value = list(value)
649
super(EntryMenuItem, self).__init__( "%s %s" %(label,value), callback_func )
650
self.max_length = max_length
652
def on_text( self, text ):
653
if self.max_length == 0 or len(self._value) < self.max_length:
654
self._value.append(text)
655
self._calculate_value()
658
def on_key_press(self, symbol, modifiers):
659
if symbol == key.BACKSPACE:
664
self._calculate_value()
667
def _calculate_value( self ):
668
new_text = u"%s %s" % (self._label, self.value)
669
self.item.text = new_text
670
self.item_selected.text = new_text
671
self.callback_func(self.value)
673
class ColorMenuItem( MenuItem ):
674
"""A menu item for selecting a color.
678
colors = [(255, 255, 255), (100, 200, 100), (200, 50, 50)]
680
items.append( ColorMenuItem(
682
self.on_jacket_color,
686
def __init__(self, label, callback_func, items, default_item=0):
687
"""Creates a Color Menu Item
692
`callback_func` : function
695
List of thre-element tuples describing the color choices
696
`default_item` : integer
697
Default item of the list. It is an index of the list. Default: 0
699
self.my_label = label
701
self.idx = default_item
702
if self.idx < 0 or self.idx >= len(self.items):
703
raise Exception("Index out of bounds")
704
super( ColorMenuItem, self).__init__( self._get_label(), callback_func )
706
def _get_label(self):
707
return self.my_label + " "
709
def on_key_press(self, symbol, modifiers):
710
if symbol == key.LEFT:
711
self.idx = max(0, self.idx-1)
712
elif symbol in (key.RIGHT, key.ENTER):
713
self.idx = min(len(self.items)-1, self.idx+1)
715
if symbol in (key.LEFT, key.RIGHT, key.ENTER):
716
self.item.text = self._get_label()
717
self.item_selected.text = self._get_label()
718
self.callback_func( self.idx )
721
def generateWidgets (self, pos_x, pos_y, font_item, font_item_selected):
722
font_item['x'] = int(pos_x)
723
font_item['y'] = int(pos_y)
724
font_item['text'] = self.my_label
725
self.item = pyglet.text.Label(**font_item )
726
self.item.labelWidth=self.item.content_width
727
self.item.text = self.label
728
font_item_selected['x'] = int(pos_x)
729
font_item_selected['y'] = int(pos_y)
730
font_item_selected['text'] = self.my_label
731
self.item_selected = pyglet.text.Label( **font_item_selected )
732
self.item_selected.labelWidth=self.item_selected.content_width
733
self.item_selected.text = self.label
735
def draw(self, *args, **kwargs):
736
super(ColorMenuItem, self).draw()
741
item = self.item_selected
745
x1 = int(item._get_left() + item.labelWidth * 1.05)
746
y1 = int(item.y - item.content_height / 2)
747
y2 = int(item.y + item.content_height / 3)
748
x2 = int(x1 + (y2 - y1) * 2)
749
pyglet.graphics.draw(4, pyglet.graphics.GL_QUADS,
750
('v2f', (x1, y1, x1, y2, x2, y2, x2, y1)),
751
('c3B', self.items[self.idx] * 4))
756
'''Predefined action that performs a slight rotation and then goes back to the original rotation
762
rot = Accelerate(RotateBy( angle, duration ), 2)
763
rot2 = Accelerate(RotateBy( -angle*2, duration), 2)
764
return rot + (rot2 + Reverse(rot2)) * 2 + Reverse(rot)
767
'''Predefined action that rotates to 0 degrees in 0.1 seconds'''
768
return RotateTo(0,0.1)
771
'''Predefined action that scales to 1.5 factor in 0.2 seconds'''
772
return ScaleTo( 1.5, duration=0.2 )
775
'''Predefined action that scales to 1.0 factor in 0.2 seconds'''
776
return ScaleTo( 1.0, duration=0.2 )