Source

pdw / Source / MenuClass.py

The default branch has multiple heads

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
#! /usr/bin/python

## \file  menu.py
#  \brief General Menu Class
#  \author Scott Barlow
#  \date 2009
#  \version 1.0.3
#
#  This is a menu class written for pygame/Python.  The menu is designed to work
#  with a program using a finite state machine (but it could also be easily
#  modified to have the 'buttons' return functions).  The menu 'buttons' contain
#  a 'state' (a state could really be anything you want) and this 'state' is
#  what is returned when the user selects/presses the button.  The program
#  controlling the menu can then act on this returned state as required.  This
#  helps to write non-blocking code.
#
#  The menu can have text buttons, image buttons (that get highlighted on all
#  sides to detect which is selected), or any combination of the two.
#
#  The menu is flexible and can be dynamically changed.  The 'buttons' will
#  auto-magically update themselves the next time they are drawn to the screen
#  (via the update method, which calls the draw method).  The draw method should
#  not be called itself.  'Buttons' can be added or removed at any time.
#
#  The menu can be positioned by the top left corner (a rectangle containing all
#  buttons is what gets moved).  It can be changed to center the entire menu
#  (i.e. center that containing rectangle) on that same position coordinate.  Or
#  the user can center the entire menu on the self.draw_surface.  Note that if
#  the pygame screen is given to the menu, then the entire window will be
#  available to be drawn to.  But if the user gives the menu another pygame
#  surface, then that surface itself will need to be blitted to the pygame
#  screen at some point.  Furthermore, the user can align the buttons to align
#  on the left, tobe centerd, or to align themselves on the right.  Also, they
#  can be aligned vertically on the top, center, or bottom.
#
#  The user can dynamically change the colors of the font/highlights, the
#  padding between buttons (left/right and top/bottom), the thickness of the
#  highlight around image buttons, and the orientation of the menu (if the
#  'buttons' will be stacked top to bottom ('vertical') or left to right
#  ('horizontal').
#
#  The best way to figure out the menu is to tinker around with it.  Run the
#  example programs, change attributes, and play with the menu.
#
#
#       Copyright 2009 Scott Barlow
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 3 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA or see <http://www.gnu.org/licenses/>.
#
#
#  Changelog
#     V1.0.0 - Initial Release
#     V1.0.1 - Added get_current_image method
#     V1.0.2 - Fixed a bug in the set_font method (to update the rect of the
#              text buttons when the font is changed
#     V1.0.3 - Added self.refresh_whole_surface_on_load functionality
#


#-------------------------------------------------------------------------------
#---[ Imports ]-----------------------------------------------------------------
#-------------------------------------------------------------------------------
import pygame


#-------------------------------------------------------------------------------
#---[ Defines ]-----------------------------------------------------------------
#-------------------------------------------------------------------------------
## RGB color for Black
BLACK = (0, 0, 0)

## RGB color for White
WHITE = (255, 255, 255)

## RGB color for Red
RED = (255, 0, 0)

## RGB color for Green
GREEN = (0, 255, 0)

## RGB color for Blue
BLUE = (0, 0, 255)

## This is a user event that should be sent whenever the game state is changed
#  (at the main game loop level)
EVENT_CHANGE_STATE = pygame.USEREVENT + 1

#-------------------------------------------------------------------------------
#---[ cMenu Class ]-------------------------------------------------------------
#-------------------------------------------------------------------------------
## This class is used to display and control a menu
#
class cMenu:
   ## ---[ __init__ ]-----------------------------------------------------------
   #  @param   self        The class itself, Python standard
   #  @param   x           The x location to shift the buttons by when the
   #                       button is drawn to the surface in the update method
   #  @param   y           The y location to shift the buttons by when the
   #                       button is drawn to the surface in the update method
   #  @param   h_pad       Number the extra pixels to pad the buttons by (on the
   #                       left/right)
   #  @param   v_pad       Number the extra pixels to pad the buttons by (on the
   #                       top/bottom).
   #  @param   orientation Should be 'vertical' or 'horizontal'.  The buttons
   #                       will be put vertically or horizontally until 'number'
   #                       (the next argument) of buttons have been created at
   #                       which point it will start a new row or column
   #  @param   number      The number of buttons to put vertically or
   #                       horizontally before starting a new row or column
   #  @param   background  The background to use for the buttons (what will show
   #                       up behind the buttons).  This is often the
   #                       surface that they will be blitted to via the update
   #                       method
   #  @param   buttonList  This is a list of buttons to be added (though
   #                       more buttons can be added via another method).  The
   #                       elements of the list should be tuples of 3 parts as
   #                       shown: ('text', state, image) where text is the text
   #                       that will be shown for the button, state is what will
   #                       be returned when the button is pressed (enter is
   #                       hit), and image is None if the button is just going
   #                       to display text, or else is an image itself if the
   #                       button will be displayed as an image instead of text.
   #
   #  Initialize the class
   #
   def __init__(self, x, y, h_pad, v_pad, orientation, number, background,
                buttonList):
      ## menu items
      self.menu_items = []                      # List of menu items
      self.font = pygame.font.Font(None, 32)    # Font to use

      self.x = x                                # Top left corner (of surface)
      self.y = y                                # relative to the screen/window
      self.change_number = number               # See description above
      self.orientation = orientation            # See description above
      self.horizontal_padding = h_pad           # See description above
      self.vertical_padding = v_pad             # See description above

      self.selection = 0                        # The currently selected button
      self.u_color = WHITE                      # Color for unselected text
      self.s_color = RED                        # Color for selected text
      self.image_highlight_color = BLUE         # Color for the image highlights
      self.image_highlight_offset = 2           # Addition padding around image
                                                # buttons only for the highlight

      self.background = background.copy()       # The unedited background image
      self.draw_surface = background            # Surface to draw to
      self.centered = False                     # True if the menu is centered
      self.centeredOnScreen = False             # True if the menu is centered
      self.update_buttons = True                # True if the positions of the
                                                # buttons need to be updated
      self.refresh_whole_surface_on_load = False# When the menu is first
                                                # displayed (when the event
                                                # EVENT_CHANGE_STATE is given to
                                                # the update method), the entire
                                                # self.draw_surface will be
                                                # updated

      # This dictionary contains the alignment orientation of the buttons
      # related to each other.  It shifts the button within the bounds of
      # 'max_width' and 'max_height' in the self.position_buttons() method.
      self.alignment = {'vertical'  :'top',
                        'horizontal':'left'}

      # Now add any buttons that were sent in
      self.add_buttons(buttonList)
   
      # Hack to allow menu default > 0; just set this before calling menu
      self.initialSelection = 0 

      # Hacked to work with the mouse! See mouse events in update()
      self.mousePos = [0,0]


   ## ---[ redraw_all ]---------------------------------------------------------
   def redraw_all(self):
      for button in self.menu_items:
         button['redraw'] = True

   ## ---[ get_current_text ]---------------------------------------------------
   def get_current_text(self):
      return self.menu_items[self.selection]['text']

   ## ---[ get_current_image ]--------------------------------------------------
   def get_current_image(self):
      return self.menu_items[self.selection]['b_image']

   ## ---[ set_unselected_color ]-----------------------------------------------
   def set_unselected_color(self, new_color):
      self.u_color = new_color
      self.update_buttons = True

   ## ---[ set_selected_color ]-------------------------------------------------
   def set_selected_color(self, new_color):
      self.s_color = new_color
      self.update_buttons = True

   ## ---[ set_image_highlight_color ]------------------------------------------
   def set_image_highlight_color(self, new_color):
      self.image_highlight_color = new_color
      self.update_buttons = True

   ## ---[ set_image_highlight_thickness ]--------------------------------------
   def set_image_highlight_thickness(self, new_thick):
      old_th = self.image_highlight_offset
      # We need to update the width of the button images now (the images
      # themselves will be updated before the next refresh/re-draw).  Note that
      # we only change the rect on the image buttons since we only highlight the
      # image buttons (not the text buttons)
      for button in self.menu_items:
         if button['b_image'] != None:
            button['rect'][2] = button['rect'][2] - 2 * old_th + 2 * new_thick
            button['rect'][3] = button['rect'][3] - 2 * old_th + 2 * new_thick
      self.image_highlight_offset = new_thick
      self.update_buttons = True

   ## ---[ set_padding ]--------------------------------------------------------
   def set_padding(self, h_pad, v_pad):
      self.horizontal_padding = h_pad
      self.vertical_padding   = v_pad
      self.update_buttons = True

   ## ---[ set_orientation ]----------------------------------------------------
   def set_orientation(self, new_orientation):
      if new_orientation == 'vertical' or new_orientation == 'horizontal':
         self.orientation = new_orientation
         self.update_buttons = True
      else:
         print 'WARNING:  cMenu.set_orientation:  Invalid argument '\
               'new_orientation (value: %d)' % new_orientation

   ## ---[ set_change_number ]--------------------------------------------------
   def set_change_number(self, new_change_number):
      self.change_number = new_change_number
      self.update_buttons = True

   ## ---[ set_refresh_whole_screen_on_load ]-----------------------------------
   def set_refresh_whole_surface_on_load(self, new_val = True):
      self.refresh_whole_surface_on_load = new_val # Should be True or False

   ## ---[ set_font ]-----------------------------------------------------------
   def set_font(self, font):
      self.font = font

      # We need to update the width and height of the text buttons since we
      # calculated their width and height based on the font
      for button in self.menu_items:
         if button['b_image'] == None:
            width, height = self.font.size(button['text'])
            button['rect'][2] = width
            button['rect'][3] = height

      self.update_buttons = True

   ## ---[ set_alignment ]------------------------------------------------------
   #  @param   self     The class itself, Python standard
   #  @param   v_align  The way to align the text vertically within its 'cell'
   #  @param   h_align  The way to align the text horizontally within its 'cell'
   #
   #  This method sets the alignment of the buttons within their 'cell' (i.e. it
   #  sets the alignment of the button (based on it's width and height) within
   #  the max_width and max_height values calculated in the
   #  self.position_buttons() method).  The self.position_buttons() method is
   #  also where the alignment occurs.  The valid alignments are:
   #     left
   #     center
   #     right
   #
   def set_alignment(self, v_align, h_align):
      if v_align in ['top', 'center', 'bottom']:
         self.alignment['vertical'] = v_align
      if h_align in ['left', 'center', 'right']:
         self.alignment['horizontal'] = h_align
      self.update_buttons = True


   ## ---[ set_position ]-------------------------------------------------------
   #  @param   self   The class itself, Python standard
   #  @param   x      The x (horizontal location)
   #  @param   y      The y (vertical location)
   #
   #  This method sets the x and y locations for the menu.  By default, this
   #  sets the position of the menu with respect to the top left corner of the
   #  self.draw_surface.  If 'centered' is true, then this is the location of
   #  the center of the menu.
   #
   def set_position(self, x, y):
      self.x = x
      self.y = y
      self.update_buttons = True


   ## ---[ set_center ]---------------------------------------------------------
   #  @param   self           The class itself, Python standard
   #  @param   centered       A boolean, centers the menu if it is True, default
   #                          value is True
   #  @param   centeredOnScreen  If this is true, then the menu will be centered
   #                             on the entire self.draw_surface surface.
   #
   #  When passed a value of True, this centers the menu at the self.x and
   #  self.y locations.  If False is passed to it, then this makes the top left
   #  corner of the menu start at the x and y location with respect to the
   #  self.draw_surface.  If centerScreen is True, then self.centered is set to
   #  true, regardless of the value passed in
   #
   def set_center(self, centered, centeredOnScreen):
      if centeredOnScreen:
         self.centeredOnScreen = centeredOnScreen
         self.centered = False
      else:
         self.centeredOnScreen = False
         self.centered = centered
      self.update_buttons = True


   ## ---[ add_buttons ]--------------------------------------------------------
   #  @param   self         The class itself, Python standard
   #  @param   buttonList   List of menu buttons to be added
   #
   #  Used to add button(s) to the menu
   #
   def add_buttons(self, buttonList):
      for button in buttonList:
         self.menu_items.append(self.create_button(button))
      self.update_buttons = True


   ## ---[ remove_buttons ]-----------------------------------------------------
   #  @param   self         The class itself, Python standard
   #  @param   indexList    List of indexes to be removed
   #
   #  Used to remove button(s) from the menu
   #
   def remove_buttons(self, indexList):
      old_contained_rect = self.contained_rect
      for index in indexList:
         if len(self.menu_items) > 1:
            self.menu_items.pop(index)
      self.update_buttons = True
      return old_contained_rect


   ## ---[ update_button_locations ]--------------------------------------------
   #  @param   self         The class itself, Python standard
   #
   #  This method is just used to update the location of the buttons when the
   #  a change is made
   #
   def update_button_locations(self):
      self.position_buttons()
      self.set_button_images()
      self.update_buttons = False


   ## ---[ create_button ]------------------------------------------------------
   #  @param   self         The class itself, Python standard
   #  @param   button_info  A list with the button text, the next state to
   #                        return, and the image (if applicable)
   #
   #  Create the button dictionary for a new button.  Note that this button is
   #  useless until the set_button_images method is called which is where the
   #  button images are created and assigned.  The reason it is not done here
   #  is becuase we need to know the location of the button on the background
   #  which is not assigned until position_buttons() is called.  Since position
   #  buttons depends on the width and height of each button, we just calculate
   #  those here, then we set the location of the buttons via the
   #  position_buttons() method, then we make the actual images via the
   #  set_button_images() function
   #
   def create_button(self, button_info):
      # If this button is not an image, set the width and height based on the
      # text
      if button_info[2] == None:
         width, height = self.font.size(button_info[0])
         button_rect = pygame.Rect((0, 0), (width, height))
      # Else this button is a graphic button, so create the width and height
      # based on the image provided
      else:
         width, height = button_info[2].get_size()
         offset = (self.image_highlight_offset, self.image_highlight_offset)
         new_width  = width  + 2 * offset[0]  # Make room for the highlight on
         new_height = height + 2 * offset[1]  # all sides
         button_rect = pygame.Rect((0, 0), (new_width, new_height))

      set_redraw = True     # When the button is created, it needs to be drawn
      set_selected = False  # When the button is created, it is not selected

      new_button = {'text'    : button_info[0],
                    'state'   : button_info[1],
                    'selected': set_selected,
                    'rect'    : button_rect,
                    'offset'  : (0, 0),
                    'redraw'  : set_redraw,
                    'b_image' : button_info[2],  # base image
                    's_image' : None,            # image when selected and not
                    'u_image' : None}            # selected (created in
                                                 # set_button_images)

      return new_button


   ## ---[ set_button_images ]--------------------------------------------------
   #  @param   self         The class itself, Python standard
   #
   #  Create the button images to be displayed - adjusted for the location of
   #  the button over the background image
   #
   def set_button_images(self):
      for button in self.menu_items:
         # If this button is not an image, create the selected and unselected
         # images based on the text
         if button['b_image'] == None:
            r = self.font.render
            width  = button['rect'][2]
            height = button['rect'][3]
            rect = pygame.Rect(button['offset'], (width, height))

            # For each of the text button (selected and unselected), create a
            # surface of the required size (already calculated before), blit
            # the background image to the surface, then render the text and blit
            # that text onto the same surface.
            selected_image = pygame.Surface((width, height), -1)
            selected_image.blit(self.background, (0, 0), rect)
            text_image   = r(button['text'], True, self.s_color)
            selected_image.blit(text_image, (0, 0))

            unselected_image = pygame.Surface((width, height), -1)
            unselected_image.blit(self.background, (0, 0), rect)
            text_image   = r(button['text'], True, self.u_color)
            unselected_image.blit(text_image, (0, 0))

         # Else this button is a graphic button, so create the selected and
         # unselected images based on the image provided
         else:
            orig_width, orig_height = button['b_image'].get_size()
            new_width  = button['rect'][2]
            new_height = button['rect'][3]
            offset = (self.image_highlight_offset, self.image_highlight_offset)

            # Selected image! --------------------------------------------------
            # Create the surface, fill the surface with the highlight color,
            # then blit the background image to the surface (inside of the
            # highlight area), and then blit the actual button base image over
            # the background
            selected_image = pygame.Surface((new_width, new_height), -1)
            selected_image.fill(self.image_highlight_color)
            rect = pygame.Rect((button['offset'][0] + offset[0],
                                button['offset'][1] + offset[1]),
                               (orig_width, orig_height))
            selected_image.blit(self.background, offset, rect)
            selected_image.blit(button['b_image'], offset)

            # Unselected image! ------------------------------------------------
            # Create the surface, blit the background image onto the surface (to
            # make sure effects go away when the button is no longer selected),
            # and then blit the actual button base image over the background
            unselected_image = pygame.Surface((new_width, new_height), -1)
            rect = pygame.Rect(button['offset'], (new_width, new_height))
            unselected_image.blit(self.background, (0, 0), rect)
            unselected_image.blit(button['b_image'], offset)

         button['s_image'] = selected_image
         button['u_image'] = unselected_image


   ## ---[ position_buttons ]---------------------------------------------------
   #  @param   self    The class itself, Python standard
   #
   #  Sets the positions for the buttons
   #
   def position_buttons(self):
      width = 0
      height = 0
      max_width = 0
      max_height = 0
      counter = 0
      x_loc = self.x
      y_loc = self.y

      # Get the maximum width and height of the surfaces
      for button in self.menu_items:
         width  = button['rect'][2]
         height = button['rect'][3]
         max_width = max(width, max_width)
         max_height = max(height, max_height)

      # Position the button in relation to each other
      for button in self.menu_items:
         # Find the offsets for the alignment of the buttons (left, center, or
         # right
         # Vertical Alignment
         if self.alignment['vertical'] == 'top':
            offset_height = 0
         elif self.alignment['vertical'] == 'center':
            offset_height = (max_height - button['rect'][3])/2
         elif self.alignment['vertical'] == 'bottom':
            offset_height = (max_height - button['rect'][3])
         else:
            offset_height = 0
            print 'WARNING:  cMenu.position_buttons:  Vertical Alignment '\
                  '(value: %s) not recognized!  Left alignment will be used'\
                                                    % self.alignment['vertical']

         # Horizontal Alignment
         if self.alignment['horizontal'] == 'left':
            offset_width = 0
         elif self.alignment['horizontal'] == 'center':
            offset_width = (max_width - button['rect'][2])/2
         elif self.alignment['horizontal'] == 'right':
            offset_width = (max_width - button['rect'][2])
         else:
            offset_width = 0
            print 'WARNING:  cMenu.position_buttons:  Horizontal Alignment '\
                  '(value: %s) not recognized!  Left alignment will be used'\
                                                  % self.alignment['horizontal']

         # Move the button location slightly based on the alignment offsets
         x_loc += offset_width
         y_loc += offset_height

         # Assign the location of the button
         button['offset'] = (x_loc, y_loc)

         # Take the alignment offsets away after the button position has been
         # assigned so that the new button can start fresh again
         x_loc -= offset_width
         y_loc -= offset_height

         # Add the width/height to the position based on the orientation of the
         # menu.  Add in the padding.
         if self.orientation == 'vertical':
            y_loc += max_height + self.vertical_padding
         else:
            x_loc += max_width + self.horizontal_padding
         counter += 1

         # If we have reached the self.change_number of buttons, then it is time
         # to start a new row or column
         if counter == self.change_number:
            counter = 0
            if self.orientation == 'vertical':
               x_loc += max_width + self.horizontal_padding
               y_loc = self.y
            else:
               y_loc += max_height + self.vertical_padding
               x_loc = self.x

      # Find the smallest Rect that will contain all of the buttons
      self.contained_rect = self.menu_items[0]['rect'].move(button['offset'])
      for button in self.menu_items:
         temp_rect = button['rect'].move(button['offset'])
         self.contained_rect.union_ip(temp_rect)

      # We shift the buttons around on the screen if they are supposed to be
      # centered (on the surface itself or at (x, y).  We do it here instead of
      # at the beginning of this function becuase we need to know what the
      # self.contained_rect is to know the correct amount to shift them.
      if self.centeredOnScreen:
         shift_x = self.x - (self.draw_surface.get_rect()[2] -
                             self.contained_rect[2]) / 2
         shift_y = self.y - (self.draw_surface.get_rect()[3] -
                             self.contained_rect[3]) / 2
      elif self.centered:
         shift_x = (self.contained_rect[2]) / 2
         shift_y = (self.contained_rect[3]) / 2
      if self.centeredOnScreen or self.centered:
         # Move the buttons to make them centered
         for button in self.menu_items:
            button['offset'] = (button['offset'][0] - shift_x,
                                button['offset'][1] - shift_y)

         # Re-find the smallest Rect that will contain all of the buttons
         self.contained_rect = self.menu_items[0]['rect'].move(button['offset'])
         for button in self.menu_items:
            temp_rect = button['rect'].move(button['offset'])
            self.contained_rect.union_ip(temp_rect)


   ## ---[ update ]-------------------------------------------------------------
   #  @param   self      The class itself, Python standard
   #  @param   e         The last event
   #  @param   c_state   The current state of the game from where this is called
   #  @return            A list of rectangles of where the screen changed
   #  @return            The new state for the game
   #
   #  Update the menu surface, redraw it to the stored surface self.draw_surface
   #
   def update(self, e, c_state):
      redraw_full_menu = False

      self.selection_prev = self.selection
      o = self.orientation
      s = self.selection
      n = self.change_number

      if e.type == pygame.MOUSEBUTTONDOWN:
        rectangle_list = self.draw_buttons()
        mouseSelected = 0
        for r in rectangle_list:
            mouseSelected  += 1
            mouseX = self.mousePos[0]
            mouseY = self.mousePos[1]
            if r[0] < mouseX and r[0]+r[2] > mouseX and r[1] < mouseY and r[1]+r[3] > mouseY:
                self.selection = mouseSelected - 1
      elif e.type == pygame.MOUSEMOTION:
        self.mousePos = e.pos
      elif e.type == pygame.MOUSEBUTTONUP:
        rectangle_list = self.draw_buttons()
        mouseSelected = 0
        for r in rectangle_list:
            mouseSelected  += 1
            mouseX = self.mousePos[0]
            mouseY = self.mousePos[1]
            if r[0] < mouseX and r[0]+r[2] > mouseX and r[1] < mouseY and r[1]+r[3] > mouseY:
                if self.selection == mouseSelected - 1:
                    return [None], self.menu_items[self.selection]['state']
                else:
                    return rectangle_list, c_state
        
        
      elif e.type == pygame.KEYDOWN:    
          if e.key == pygame.K_DOWN:
             if (o == 'vertical') and ((s + 1) % n != 0):
                self.selection += 1
             elif o == 'horizontal':
                self.selection += n
          elif e.key == pygame.K_UP:
             if (o == 'vertical') and ((s) % n != 0):
                self.selection -= 1
             elif o == 'horizontal':
                self.selection -= n
          elif e.key == pygame.K_RIGHT:
             if o == 'vertical':
                self.selection += n
             elif (o == 'horizontal') and ((s + 1) % n != 0):
                self.selection += 1
          elif e.key == pygame.K_LEFT:
             if o == 'vertical':
                self.selection -= n
             elif (o == 'horizontal') and ((s) % n != 0):
                self.selection -= 1
# We don't want this in prod, but nice feature for demo!
##          elif e.key == pygame.K_r:
##             original_contained_rect = self.remove_buttons([s])
##             if self.selection -1 >= 0:
##                self.selection -= 1
##                self.selection_prev -= 1
##             redraw_full_menu = True
          elif e.key == pygame.K_RETURN:
             return [None], self.menu_items[s]['state']

          if self.selection >= len(self.menu_items) or self.selection < 0:
             self.selection = self.selection_prev

      # If this is an EVENT_CHANGE_STATE, then this is the first time that we
      # have entered this menu, so lets set it up
      if e.type == EVENT_CHANGE_STATE:
         # Hack to start with default choice in middle of menu...
         #self.selection = 0
         self.selection = self.initialSelection
        
         self.menu_items[self.selection_prev]['selected'] = False
         self.menu_items[self.selection]['selected'] = True
         self.redraw_all()
         rectangle_list = self.draw_buttons()
         if self.refresh_whole_surface_on_load:
            rectangle_list = pygame.Rect((0, 0), self.draw_surface.get_size())
            return [rectangle_list], c_state
         else:           
            return [self.contained_rect], c_state

      elif redraw_full_menu:
         self.menu_items[self.selection_prev]['selected'] = False
         self.menu_items[self.selection]['selected'] = True
         self.redraw_all()
         rectangle_list = self.draw_buttons(original_contained_rect)
         return rectangle_list, c_state

      elif self.selection != self.selection_prev:
         self.menu_items[self.selection_prev]['selected'] = False
         self.menu_items[self.selection]['selected'] = True
         rectangle_list = self.draw_buttons()
         return rectangle_list, c_state

      # If no updates were made, return defaults
      return [None], c_state


   ## ---[ draw_buttons ]-------------------------------------------------------
   #  @param   self          The class itself, Python standard
   #  @param   redraw_rect   If this pygame.Rect is provided, then the entire
   #                         background will be drawn to the surface in the area
   #                         of this rect before the buttons are drawn
   #  @return                A list of rectangles of where the screen changed
   #
   #  Draw the buttons to the self.draw_surface and return a list of Rect's that
   #  indicate where on the surface changes were made
   #
   def draw_buttons(self, redraw_rect = None):
      rect_list = []

      # If buttons have been changed (added button(s), deleted button(s),
      # changed colors, etc, etc), then we need to update the button locations
      # and images
      if self.update_buttons:
         self.update_button_locations()

         # Print a warning if the buttons are partially/completely off the
         # surface
         if not self.draw_surface.get_rect().contains(self.contained_rect):
            print 'WARNING:  cMenu.draw_buttons:  Some buttons are partially '\
                  'or completely off of the self.draw_surface!'

      # If a rect was provided, redraw the background surface to the area of the
      # rect before we draw the buttons
      if redraw_rect != None:
         offset = (redraw_rect[0], redraw_rect[1])
         drawn_rect = self.draw_surface.blit(self.background,
                                             offset,
                                             redraw_rect)
         rect_list.append(drawn_rect)

      # Cycle through the buttons, only draw the ones that need to be redrawn
      for button in self.menu_items:
         if button['redraw']:
            if button['selected']:
               image = button['s_image']
            else:
               image = button['u_image']

            drawn_rect = self.draw_surface.blit(image,
                                                button['offset'],
                                                button['rect'])
            rect_list.append(drawn_rect)

      return rect_list


#---[ END OF FILE ]-------------------------------------------------------------