1#file: widget.py 2#Copyright (C) 2008 FunnyMan3595 3#This file is part of Endgame: Singularity. 4 5#Endgame: Singularity is free software; you can redistribute it and/or modify 6#it under the terms of the GNU General Public License as published by 7#the Free Software Foundation; either version 2 of the License, or 8#(at your option) any later version. 9 10#Endgame: Singularity is distributed in the hope that it will be useful, 11#but WITHOUT ANY WARRANTY; without even the implied warranty of 12#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13#GNU General Public License for more details. 14 15#You should have received a copy of the GNU General Public License 16#along with Endgame: Singularity; if not, write to the Free Software 17#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18 19#This file contains the widget class. 20 21from __future__ import absolute_import 22 23import pygame 24from numpy import array 25from inspect import getmembers 26 27from singularity.code import g 28from singularity.code.graphics import g as gg, constants 29 30 31def unmask(widget): 32 """Causes the widget to exist above its parent's fade mask. The widget's 33 children will still be masked, unless they are unmasked themselves.""" 34 unmask_all(widget) 35 widget.mask_children = True 36 37def unmask_all(widget): 38 """Causes the widget to exist above its parent's fade mask. The widget's 39 children will not be masked.""" 40 widget.self_mask = True 41 widget.do_mask = lambda: None 42 43def call_on_change(data_member, call_me, *args, **kwargs): 44 """Creates a data member that calls a function when changed.""" 45 def get(self): 46 return getattr(self, data_member) 47 48 def set(self, my_value): 49 if data_member in self.__dict__: 50 change = (my_value != self.__dict__[data_member]) 51 else: 52 change = True 53 54 if change: 55 setattr(self, data_member, my_value) 56 call_me(self, *args, **kwargs) 57 58 return property(get, set) 59 60def set_on_change(data_member, set_me, set_value = True): 61 """Creates a data member that sets another data member to a given value 62 when changed.""" 63 return call_on_change(data_member, setattr, set_me, set_value) 64 65def causes_rebuild(data_member): 66 """Creates a data member that sets needs_rebuild to True when changed.""" 67 return set_on_change(data_member, "needs_rebuild") 68 69def causes_redraw(data_member): 70 """Creates a data member that sets needs_redraw to True when changed.""" 71 return set_on_change(data_member, "needs_redraw") 72 73def causes_resize(data_member): 74 """Creates a data member that sets needs_resize to True when changed.""" 75 return set_on_change(data_member, "needs_resize") 76 77def causes_reposition(data_member): 78 """Creates a data member that sets needs_reposition to True when changed.""" 79 return set_on_change(data_member, "needs_reposition") 80 81def causes_update(data_member): 82 """Creates a data member that sets needs_update to True when changed.""" 83 return set_on_change(data_member, "needs_update") 84 85def propogate_need(data_member): 86 """Creates a function that can be passed to call_on_change. When the 87 data member changes to True, needs_update is set, and the True value 88 is passed to all descendants.""" 89 def do_propogate(self): 90 if getattr(self, data_member, False): 91 self.needs_update = True 92 93 if hasattr(self, "children"): 94 descendants = self.children[:] 95 while descendants: 96 child = descendants.pop() 97 # Propogate to this child and its descendants, if needed. 98 if not getattr(child, data_member, False): 99 setattr(child, data_member, True) 100 child._needs_update = True 101 if hasattr(child, "children"): 102 descendants += child.children 103 104 return do_propogate 105 106# Previous attempt was to hide the raw value by resolving 107# the value before returning it. However, there are legitimate 108# reason to access the raw value. So, we need two property. 109# Using a wrapper is not worth the trouble. 110# I choose to let the unresolved value the default because 111# in majority of it's what we want, and in other case, 112# you must handle reconfig anyways. 113class auto_reconfig(object): 114 115 __slots__ = ["data_member", "reconfig_datamember", "reconfig_func"] # Avoid __dict__. 116 117 def __init__(self, data_member, reconfig_prefix, reconfig_func): 118 self.data_member = data_member 119 self.reconfig_datamember = reconfig_prefix + data_member 120 self.reconfig_func = reconfig_func 121 122 def __get__(self, obj, objtype=None): 123 if obj is None: 124 return self 125 return getattr(obj, self.data_member) 126 127 def __set__(self, obj, my_value): 128 new_value = self.reconfig_func(my_value) 129 130 setattr(obj, self.reconfig_datamember, new_value) 131 setattr(obj, self.data_member, my_value) 132 133 def reconfig(self, obj): 134 updated_value = self.reconfig_func(getattr(obj, self.data_member)) 135 setattr(obj, self.reconfig_datamember, updated_value) 136 137 138# In debug mode, this list tracks which widgets (i.e. rects) where highlighted "last" 139# during a redraw, so they can be "re-updated" without the highlight 140debug_mode_undo_drawing_highlight = [] 141 142 143class Widget(object): 144 """A Widget is a GUI element. It can have one parent and any number of 145 children.""" 146 147 needs_redraw = call_on_change("_needs_redraw", 148 propogate_need("_needs_redraw")) 149 150 needs_resize = call_on_change("_needs_resize", 151 propogate_need("_needs_resize")) 152 153 needs_reposition = call_on_change("_needs_reposition", 154 propogate_need("_needs_reposition")) 155 156 needs_rebuild = causes_update("_needs_rebuild") 157 158 def _propogate_update(self): 159 if self._needs_update: 160 if hasattr(self, "parent"): 161 target = self.parent 162 while target and not target._needs_update: 163 target._needs_update = True 164 target = target.parent 165 166 needs_update = call_on_change("_needs_update", _propogate_update) 167 168 needs_reconfig = call_on_change("_needs_reconfig", 169 propogate_need("_needs_reconfig")) 170 171 pos = causes_reposition("_pos") 172 size = causes_resize("_size") 173 anchor = causes_reposition("_anchor") 174 visible = causes_redraw("_visible") 175 176 def __init__(self, parent, pos, size, anchor = constants.TOP_LEFT): 177 self.parent = parent 178 self.children = [] 179 180 self.pos = pos 181 self.size = size 182 self.anchor = anchor 183 184 # "It's a widget!" 185 self.add_hooks() 186 187 self.is_above_mask = False 188 self.self_mask = False 189 self.mask_children = False 190 self.visible = True 191 192 self.needs_rebuild = True 193 self.collision_rect = None 194 195 # Set automatically by other properties. 196 #self.needs_redraw = True 197 #self.needs_full_redraw = True 198 self.needs_reconfig = True 199 200 @property 201 def parent(self): 202 return self._parent 203 204 @parent.setter 205 def parent(self, parent): 206 if hasattr(self, 'children'): 207 self.remove_hooks() 208 209 if (hasattr(self, '_parent') and self._parent is not None): 210 try: 211 self._parent.children.remove(self) 212 except ValueError: 213 pass # Wasn't there to start with. 214 215 self._parent = parent 216 217 if self.parent is not None: 218 self.parent.children.append(self) 219 self.parent.needs_rebuild = True 220 self.parent.needs_resize = True 221 self.parent.needs_reposition = True 222 self.parent.needs_redraw = True 223 224 if hasattr(self, 'children'): 225 self.add_hooks() 226 227 def add_hooks(self): 228 if self.parent is not None: 229 # Won't trigger on the call from __init__, since there are no 230 # children yet, but add_hooks may be explicitly called elsewhere to 231 # undo remove_hooks. 232 for child in self.children: 233 child.add_hooks() 234 235 def remove_hooks(self): 236 # Localize the children list to avoid index corruption and O(N^2) time. 237 children = self.children 238 239 # Recurse to the children. 240 for child in children: 241 child.remove_hooks() 242 243 def _parent_size(self): 244 if self.parent == None: 245 return gg.real_screen_size 246 else: 247 return self.parent.real_size 248 249 def _calc_size(self): 250 """Internal method. Calculates and returns the real size of this 251 widget. 252 253 Override to create a dynamically-sized widget.""" 254 parent_size = self._parent_size() 255 size = list(self.size) 256 for i in range(2): 257 if size[i] > 0: 258 size[i] = int(size[i] * gg.real_screen_size[i]) 259 elif size[i] < 0: 260 size[i] = int( (-size[i]) * parent_size[i] ) 261 262 return tuple(size) 263 264 def get_real_size(self): 265 """Returns the real size of this widget. 266 267 To implement a dynamically-sized widget, override _calc_size, which 268 will be called whenever the widget is resized, and set needs_resize 269 when appropriate.""" 270 return self._real_size 271 272 real_size = property(get_real_size) 273 274 def get_real_pos(self): 275 """Returns the real position of this widget on its parent.""" 276 vanchor, hanchor = self.anchor 277 parent_size = self._parent_size() 278 my_size = self.real_size 279 280 if self.pos[0] >= 0: 281 hpos = int(self.pos[0] * gg.real_screen_size[0]) 282 else: 283 hpos = - int(self.pos[0] * parent_size[0]) 284 285 if hanchor == constants.LEFT: 286 pass 287 elif hanchor == constants.CENTER: 288 hpos -= my_size[0] // 2 289 elif hanchor == constants.RIGHT: 290 hpos -= my_size[0] 291 292 if self.pos[1] >= 0: 293 vpos = int(self.pos[1] * gg.real_screen_size[1]) 294 else: 295 vpos = - int(self.pos[1] * parent_size[1]) 296 297 if vanchor == constants.TOP: 298 pass 299 elif vanchor == constants.MID: 300 vpos -= my_size[1] // 2 301 elif vanchor == constants.BOTTOM: 302 vpos -= my_size[1] 303 304 return (hpos, vpos) 305 306 real_pos = property(get_real_pos) 307 308 def _make_collision_rect(self): 309 """Creates and returns a collision rect for this widget.""" 310 pos = array(self.real_pos) 311 if self.parent: 312 pos += self.parent.collision_rect[:2] 313 314 return pygame.Rect(pos, self.real_size) 315 316 def is_over(self, position): 317 if not getattr(self, "collision_rect", None): 318 return False 319 320 if position != (0,0): 321 return self.collision_rect.collidepoint(position) 322 else: 323 return False 324 325 def remake_surfaces(self): 326 """Recreates the surfaces that this widget will draw on.""" 327 size = self.real_size 328 pos = self.real_pos 329 330 if self.parent != None: 331 try: 332 self.surface = self.parent.surface.subsurface(pos + size) 333 except ValueError: 334 print("Warning: %r can't fit on its parent." % self) 335 print(pos, size, self.parent.real_pos, self.parent.real_size) 336 337 wanted_rect = pos + size 338 available_rect = self.parent.surface.get_rect() 339 compromise = available_rect.clip(wanted_rect) 340 341 self.surface = self.parent.surface.subsurface(compromise) 342 else: 343 # Recreate using the abstracted screen size, NOT the real one 344 # g.set_screen() will calculate the proper g.real_screen_size 345 if gg.screen_surface is None: 346 # Ensure that the screen is initialized 347 gg.set_mode() 348 # We draw on a copy of the surface. This is to avoid crashes 349 # during draggable resizing (event.VIDEORESIZE) where the 350 # screen size might change behind our backs while drawing 351 # (event.VIDEORESIZE tells us that the screen has been updated 352 # and we should catch up and not the other way around) 353 self.surface = gg.screen_surface.copy() 354 self.surface.fill( (0,0,0,255) ) 355 356 gg.fade_mask = pygame.Surface(size, 0, gg.ALPHA) 357 gg.fade_mask.fill( (0,0,0,175) ) 358 359 def prepare_for_redraw(self): 360 # First, we handle config changes. 361 if self.needs_reconfig: 362 self.reconfig() 363 self.needs_reconfig = False 364 365 # Then any substance changes. 366 if self.needs_rebuild: 367 self.rebuild() 368 self.needs_rebuild = False 369 370 # Then size changes. 371 if self.needs_resize: 372 self.resize() 373 self.needs_resize = False 374 self.needs_reposition = True 375 self.needs_redraw = True 376 377 # Then position changes. 378 if self.needs_reposition: 379 self.needs_reposition = False 380 self.reposition() 381 382 # And finally we recurse to our descendants. 383 for child in self.children: 384 if child.visible: 385 child.prepare_for_redraw() 386 387 def maybe_update(self): 388 if self.needs_update: 389 self.update() 390 391 def update(self): 392 # First we prepare everything for its redraw (if needed). 393 self.prepare_for_redraw() 394 395 _, updated_rect = self._update() 396 397 # Oh, and if this is the top-level widget, we should update the display. 398 if not self.parent and gg.screen_surface: 399 root_surface = self.surface 400 if g.debug and updated_rect: 401 # In debug mode, draw red boxes to represent widgets that were updated. 402 global debug_mode_undo_drawing_highlight 403 try: 404 # If the theme defines a color for this purpose, we will use it 405 widget_highlight_color = gg.resolve_color_alias('debug_mode_highlight_redrawn_widget') 406 except KeyError: 407 # ... and for every thing else, there is the color red. 408 widget_highlight_color = 0xff, 0, 0, 0 409 root_surface = self.surface.copy() 410 n_updated_rect = [] 411 for rect in updated_rect: 412 n_updated_rect.append(pygame.draw.rect(root_surface, widget_highlight_color, rect, 1)) 413 updated_rect.extend(debug_mode_undo_drawing_highlight) 414 debug_mode_undo_drawing_highlight = n_updated_rect 415 416 gg.screen_surface.blits(((root_surface, r, r) for r in updated_rect), doreturn=0) 417 pygame.display.update(updated_rect) 418 419 def _update(self): 420 redrew_self = self.needs_redraw 421 update_full_rect = redrew_self 422 affected_rects = [] 423 if self.needs_redraw: 424 self.redraw() 425 426 # Then we update any children below our fade mask. 427 check_mask = [] 428 above_mask = [] 429 for child in self.children: 430 if child.needs_update and child.visible: 431 if child.is_above_mask: 432 above_mask.append(child) 433 else: 434 # update_full_rect = True # We do not bother tracking this case 435 child_mask, child_rects = child._update() 436 check_mask.extend(child_mask) 437 if child_rects and not update_full_rect: 438 affected_rects.extend(child_rects) 439 440 # Next, we handle the fade mask. 441 if getattr(self, "faded", False): 442 while check_mask: 443 child = check_mask.pop() 444 if not child.self_mask: 445 # update_full_rect = True # We do not bother tracking this case 446 child_rect = child.surface.blit(gg.fade_mask, (0,0)) 447 if not update_full_rect: 448 affected_rects.append(child_rect) 449 elif child.mask_children: 450 check_mask += child.children 451 452 # And finally we update any children above the fade mask. 453 for child in above_mask: 454 _, child_rects = child._update() 455 if child_rects and not update_full_rect: 456 affected_rects.extend(child_rects) 457 458 # Update complete. 459 self.needs_update = False 460 461 # Any descendants we didn't check for masking get passed upwards. 462 if redrew_self: 463 # If we redrew this widget, we tell our parent to consider it 464 # instead. The parent will recurse down to any descendants if 465 # needed, and redraw already propogated down to them. 466 check_mask = [self] 467 468 if update_full_rect: 469 size = self.real_size 470 pos = self.real_pos 471 472 affected_rects = [self.collision_rect] 473 474 return check_mask, affected_rects 475 476 def reconfig(self): 477 # Find reconfig property and update them. 478 clazz = self.__class__ 479 for prop_name, prop in getmembers(clazz): 480 if (isinstance(prop, auto_reconfig)): 481 prop.reconfig(self) 482 483 def rebuild(self): 484 pass 485 486 def resize(self): 487 self._real_size = self._calc_size() 488 489 def reposition(self): 490 old_rect = self.collision_rect 491 self.collision_rect = self._make_collision_rect() 492 493 if not self.parent: 494 self.remake_surfaces() 495 self.needs_redraw = True 496 elif ( (getattr(self, "surface", None) is None) 497 or (old_rect is None) 498 or (self.surface.get_parent() is not self.parent.surface) 499 or (not self.collision_rect.contains(old_rect)) 500 ): 501 self.remake_surfaces() 502 self.parent.needs_redraw = True 503 elif self.collision_rect != old_rect: 504 self.remake_surfaces() 505 self.needs_redraw = True 506 507 def redraw(self): 508 self.needs_redraw = False 509 if self.parent is None: 510 self.surface.fill((0,0,0,255)) 511 512 def add_handler(self, *args, **kwargs): 513 """Handler pass-through.""" 514 if self.parent: 515 self.parent.add_handler(*args, **kwargs) 516 517 def remove_handler(self, *args, **kwargs): 518 """Handler pass-through.""" 519 if self.parent: 520 self.parent.remove_handler(*args, **kwargs) 521 522 def add_key_handler(self, *args, **kwargs): 523 """Handler pass-through.""" 524 if self.parent: 525 self.parent.add_key_handler(*args, **kwargs) 526 527 def remove_key_handler(self, *args, **kwargs): 528 """Handler pass-through.""" 529 if self.parent: 530 self.parent.remove_key_handler(*args, **kwargs) 531 532 def add_focus_widget(self, *args, **kwargs): 533 """Focus pass-through.""" 534 if self.parent: 535 self.parent.add_focus_widget(*args, **kwargs) 536 537 def remove_focus_widget(self, *args, **kwargs): 538 """Focus pass-through.""" 539 if self.parent: 540 self.parent.remove_focus_widget(*args, **kwargs) 541 542 def took_focus(self, *args, **kwargs): 543 """Focus pass-through.""" 544 if self.parent: 545 self.parent.took_focus(*args, **kwargs) 546 547 def clear_focus(self, *args, **kwargs): 548 """Focus pass-through.""" 549 if self.parent: 550 self.parent.clear_focus(*args, **kwargs) 551 552 553class BorderedWidget(Widget): 554 borders = causes_redraw("__borders") 555 556 border_color = auto_reconfig("_border_color", "resolved", gg.resolve_color_alias) 557 background_color = auto_reconfig("_background_color", "resolved", gg.resolve_color_alias) 558 resolved_border_color = causes_redraw("_resolved_border_color") 559 resolved_background_color = causes_redraw("_resolved_background_color") 560 561 def __init__(self, parent, *args, **kwargs): 562 self.borders = kwargs.pop("borders", ()) 563 self.border_color = kwargs.pop("border_color", "widget_border") 564 self.background_color = kwargs.pop("background_color", "widget_background") 565 566 super(BorderedWidget, self).__init__(parent, *args, **kwargs) 567 568 def rebuild(self): 569 super(BorderedWidget, self).rebuild() 570 if self.parent and self.resolved_background_color == gg.colors["clear"]: 571 self.parent.needs_redraw = True 572 573 def reposition(self): 574 super(BorderedWidget, self).reposition() 575 if self.parent and self.resolved_background_color == gg.colors["clear"]: 576 self.parent.needs_redraw = True 577 578 def redraw(self): 579 super(BorderedWidget, self).redraw() 580 581 # TODO: Transparency do not work correctly. 582 # First: fill cannot use alpha channel with current surface. 583 # Second: Transparency needs the parent redraw to work correctly. 584 # It make transparency unusable with some widget. 585 586 # Fill the background. 587 if self.resolved_background_color != gg.colors["clear"]: 588 self.surface.fill( self.resolved_background_color ) 589 590 self.draw_borders() 591 592 def draw_borders(self): 593 my_size = self.real_size 594 595 for edge in self.borders: 596 if edge == constants.TOP: 597 self.surface.fill(self.resolved_border_color, (0, 0, my_size[0], 1) ) 598 elif edge == constants.LEFT: 599 self.surface.fill(self.resolved_border_color, (0, 0, 1, my_size[1]) ) 600 elif edge == constants.RIGHT: 601 self.surface.fill(self.resolved_border_color, 602 (my_size[0]-1, 0) + my_size) 603 elif edge == constants.BOTTOM: 604 self.surface.fill(self.resolved_border_color, 605 (0, my_size[1]-1) + my_size) 606 607 608class FocusWidget(Widget): 609 has_focus = causes_redraw("_has_focus") 610 def __init__(self, *args, **kwargs): 611 super(FocusWidget, self).__init__(*args, **kwargs) 612 self.has_focus = False 613 614 self.add_handler(constants.CLICK, self.handle_click, 0) 615 616 def add_hooks(self): 617 super(FocusWidget, self).add_hooks() 618 if self.parent is not None: 619 self.parent.add_focus_widget(self) 620 621 def remove_hooks(self): 622 super(FocusWidget, self).remove_hooks() 623 if self.parent is not None: 624 self.parent.remove_focus_widget(self) 625 626 def handle_click(self, event): 627 if not self.is_over(event.pos): 628 self.clear_focus(self) 629