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