Commit before breaking everything
[matches/honours.git] / research / transmission_spectroscopy / simulator / pgu-0.18 / build / lib / pgu / gui / theme.py
1 # theme.py
2
3 """
4 """
5 import os, re
6 import pygame
7
8 try:
9     from configparser import ConfigParser
10 except:
11     from ConfigParser import ConfigParser
12
13 from .const import *
14 from . import widget
15 from . import surface
16 from .errors import StyleError
17 from .basic import parse_color, is_color
18
19 __file__ = os.path.abspath(__file__)
20
21 class Theme:
22     """Theme interface.
23     
24     If you wish to create your own theme, create a class with this interface, and 
25     pass it to gui.App via gui.App(theme=MyTheme()).
26     
27     """
28
29     # Image extensions automatically recognized by the theme class
30     image_extensions = (".gif", ".jpg", ".bmp", ".png", ".tga")
31
32     def __init__(self,dirs='default'):
33         """Theme constructor.
34
35         Keyword arguments:
36             dirs -- Name of the theme dir to load a theme from.  May be an 
37                 absolute path to a theme, if pgu is not installed, or if you 
38                 created your own theme.  May include several dirs in a list if 
39                 data is spread across several themes.
40         
41         Example:
42             theme = gui.Theme("default")
43             theme = gui.Theme(["mytheme","mytheme2"])
44
45         """
46         self.config = {}
47         self._loaded = []
48         self.cache = {}
49         self._preload(dirs)
50         pygame.font.init()
51     
52     def _preload(self,ds):
53         if not isinstance(ds, list):
54             ds = [ds]
55         for d in ds:
56             if d not in self._loaded:
57                 self._load(d)
58             self._loaded.append(d)
59     
60     def _load(self, name):
61         #theme_dir = themes[name]
62         
63         #try to load the local dir, or absolute path
64         dnames = [name]
65         
66         #if the package isn't installed and people are just
67         #trying out the scripts or examples
68         dnames.append(os.path.join(os.path.dirname(__file__),"..","..","data","themes",name))
69         
70         #if the package is installed, and the package is installed
71         #in /usr/lib/python2.3/site-packages/pgu/
72         #or c:\python23\lib\site-packages\pgu\
73         #the data is in ... lib/../share/ ...
74         dnames.append(os.path.join(os.path.dirname(__file__),"..","..","..","..","share","pgu","themes",name))
75         dnames.append(os.path.join(os.path.dirname(__file__),"..","..","..","..","..","share","pgu","themes",name))
76         dnames.append(os.path.join(os.path.dirname(__file__),"..","..","share","pgu","themes",name)) 
77         for dname in dnames:
78             if os.path.isdir(dname): break
79
80         if not os.path.isdir(dname): 
81             raise Exception('could not find theme '+name)
82
83         # Normalize the path to make it look nicer (gets rid of the ..'s)
84         dname = os.path.normpath(dname)
85
86         # Try parsing the theme in the custom txt file format
87         fname = os.path.join(dname,"config.txt")
88         if os.path.isfile(fname):
89             try:
90                 f = open(fname)
91                 for line in f.readlines():
92                     args = line.strip().split()
93
94                     if len(args) < 3:
95                         continue
96
97                     pcls = ""
98                     (cls, attr, vals) = (args[0], args[1], args[2:])
99                     if (":" in cls):
100                         (cls, pcls) = cls.split(":")
101
102                     self.config[cls, pcls, attr] = (dname, vals)
103             finally:
104                 f.close()
105             return
106
107         # Try parsing the theme data as an ini file
108         fname = os.path.join(dname,"style.ini")
109         if os.path.isfile(fname):
110             cfg = ConfigParser()
111             f = open(fname,'r')
112             cfg.readfp(f)
113             for section in cfg.sections():
114                 cls = section
115                 pcls = ''
116                 if cls.find(":")>=0:
117                     cls,pcls = cls.split(":")
118                 for attr in cfg.options(section):
119                     vals = cfg.get(section,attr).strip().split()
120                     self.config[cls,pcls,attr] = (dname, vals)
121             return
122
123         # The folder probably doesn't contain a theme
124         raise IOError("Cannot load theme: missing style.ini or config.txt")
125
126     def _get(self, cls, pcls, attr):
127         key = (cls, pcls, attr)
128         if not key in self.config:
129             return
130
131         if key in self.cache:
132             # This property is already in the cache
133             return self.cache[key]
134
135         (dname, vals) = self.config[key]
136
137         if (os.path.splitext(vals[0].lower())[1] in self.image_extensions):
138             # This is an image attribute
139             v = pygame.image.load(os.path.join(dname, vals[0]))
140
141         elif (attr == "color" or attr == "background"):
142             # This is a color value
143             v = parse_color(vals[0])
144
145         elif (attr == "font"):
146             # This is a font value
147             name = vals[0]
148             size = int(vals[1])
149             if (name.endswith(".ttf")):
150                 # Load the font from a file
151                 v = pygame.font.Font(os.path.join(dname, name), size)
152             else:
153                 # Must be a system font
154                 v = pygame.font.SysFont(name, size)
155
156         else:
157             try:
158                 v = int(vals[0])
159             except:
160                 v = vals[0]
161         self.cache[key] = v
162         return v
163
164     # TODO - obsolete, use 'getstyle' below instead
165     def get(self,cls,pcls,attr):
166         try:
167             return self.getstyle(cls, pcls, attr)
168         except StyleError:
169             return 0
170
171     # Returns the style information, given the class, sub-class and attribute names. 
172     # This raises a StylError if the style isn't found.
173     def getstyle(self, cls, pcls, attr):
174         """Interface method -- get the value of a style attribute.
175         
176         Arguments:
177             cls -- class, for example "checkbox", "button", etc.
178             pcls -- pseudo class, for example "hover", "down", etc.
179             attr -- attribute, for example "image", "background", "font", "color", etc.
180         
181         This method is called from gui.style
182
183         """
184
185         if not self._loaded: 
186             # Load the default theme
187             self._preload("default")
188
189         o = (cls, pcls, attr)
190         
191         v = self._get(cls, pcls, attr)
192         if v: 
193             return v
194         
195         v = self._get(cls, "", attr)
196         if v: 
197             return v
198         
199         v = self._get("default", "", attr)
200         if v: 
201             return v
202         
203         # The style doesn't exist
204         self.cache[o] = 0
205         raise StyleError("Style not defined: '%s', '%s', '%s'" % o)
206
207     # Draws a box around the surface in the given style
208     def box(self, style, surf):
209         c = (0, 0, 0)
210         if style.border_color != 0:
211             c = style.border_color
212         w,h = surf.get_size()
213
214         surf.fill(c,(0,0,w,style.border_top))
215         surf.fill(c,(0,h-style.border_bottom,w,style.border_bottom))
216         surf.fill(c,(0,0,style.border_left,h))
217         surf.fill(c,(w-style.border_right,0,style.border_right,h))
218
219     def getspacing(self,w):
220         # return the top, right, bottom, left spacing around the widget
221         if not hasattr(w,'_spacing'): #HACK: assume spacing doesn't change re pcls
222             s = w.style
223             xt = s.margin_top+s.border_top+s.padding_top
224             xr = s.padding_right+s.border_right+s.margin_right
225             xb = s.padding_bottom+s.border_bottom+s.margin_bottom
226             xl = s.margin_left+s.border_left+s.padding_left
227             w._spacing = xt,xr,xb,xl
228         return w._spacing
229
230         
231     def resize(self,w,func):
232         # Returns the rectangle expanded in each direction
233         def expand_rect(rect, left, top, right, bottom):
234             return pygame.Rect(rect.x - left, 
235                                rect.y - top, 
236                                rect.w + left + right, 
237                                rect.h + top + bottom)
238
239         def theme_resize(width=None,height=None):
240             s = w.style
241             
242             pt,pr,pb,pl = (s.padding_top,s.padding_right,
243                            s.padding_bottom,s.padding_left)
244             bt,br,bb,bl = (s.border_top,s.border_right,
245                            s.border_bottom,s.border_left)
246             mt,mr,mb,ml = (s.margin_top,s.margin_right,
247                            s.margin_bottom,s.margin_left)
248             # Calculate the total space on each side
249             top = pt+bt+mt
250             right = pr+br+mr
251             bottom = pb+bb+mb
252             left = pl+bl+ml
253             ttw = left+right
254             tth = top+bottom
255             
256             tilew,tileh = None,None
257             if width != None: tilew = width-ttw
258             if height != None: tileh = height-tth
259             tilew,tileh = func(tilew,tileh)
260
261             if width == None: width = tilew
262             if height == None: height = tileh
263             
264             #if the widget hasn't respected the style.width,
265             #style height, we'll add in the space for it...
266             width = max(width-ttw, tilew, w.style.width)
267             height = max(height-tth, tileh, w.style.height)
268             
269             #width = max(tilew,w.style.width-tw)
270             #height = max(tileh,w.style.height-th)
271
272             r = pygame.Rect(left,top,width,height)
273             
274             w._rect_padding = expand_rect(r, pl, pt, pr, pb)
275             w._rect_border = expand_rect(w._rect_padding, bl, bt, br, bb)
276             w._rect_margin = expand_rect(w._rect_border, ml, mt, mr, mb)
277
278             # align it within it's zone of power.   
279             rect = pygame.Rect(left, top, tilew, tileh)
280             dx = width-rect.w
281             dy = height-rect.h
282             rect.x += (w.style.align+1)*dx/2
283             rect.y += (w.style.valign+1)*dy/2
284
285             w._rect_content = rect
286
287             return (w._rect_margin.w, w._rect_margin.h)
288         return theme_resize
289
290
291     def paint(self,w,func):
292         # The function that renders the widget according to the theme, then calls the 
293         # widget's own paint function.
294         def theme_paint(s):
295 #             if w.disabled:
296 #                 if not hasattr(w,'_disabled_bkgr'):
297 #                     w._disabled_bkgr = s.convert()
298 #                 orig = s
299 #                 s = w._disabled_bkgr.convert()
300
301 #             if not hasattr(w,'_theme_paint_bkgr'):
302 #                 w._theme_paint_bkgr = s.convert()
303 #             else:
304 #                 s.blit(w._theme_paint_bkgr,(0,0))
305 #             
306 #             if w.disabled:
307 #                 orig = s
308 #                 s = w._theme_paint_bkgr.convert()
309
310             if w.disabled:
311                 if (not (hasattr(w,'_theme_bkgr') and 
312                          w._theme_bkgr.get_width() == s.get_width() and 
313                          w._theme_bkgr.get_height() == s.get_height())):
314                     w._theme_bkgr = s.copy()
315                 orig = s
316                 s = w._theme_bkgr
317                 s.fill((0,0,0,0))
318                 s.blit(orig,(0,0))
319                 
320             if w.background:
321                 w.background.paint(surface.subsurface(s,w._rect_border))
322
323             self.box(w.style, surface.subsurface(s,w._rect_border))
324             r = func(surface.subsurface(s,w._rect_content))
325             
326             if w.disabled:
327                 s.set_alpha(128)
328                 orig.blit(s,(0,0))
329             
330             w._painted = True
331             return r
332         return theme_paint
333     
334     def event(self,w,func):
335         def theme_event(e):
336             rect = w._rect_content
337             if (not rect):
338                 # This should never be the case, but it sometimes happens that _rect_content isn't
339                 # set before a mouse event is received. In this case we'll ignore the event.
340                 return func(e)
341
342             if e.type == MOUSEBUTTONUP or e.type == MOUSEBUTTONDOWN:
343                 sub = pygame.event.Event(e.type,{
344                     'button':e.button,
345                     'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
346             elif e.type == CLICK:
347                 sub = pygame.event.Event(e.type,{
348                     'button':e.button,
349                     'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
350             elif e.type == MOUSEMOTION:
351                 sub = pygame.event.Event(e.type,{
352                     'buttons':e.buttons,
353                     'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y),
354                     'rel':e.rel})
355             else:
356                 sub = e
357             return func(sub)
358
359         return theme_event
360     
361     def update(self,w,func):
362         def theme_update(s):
363             if w.disabled: return []
364             r = func(surface.subsurface(s,w._rect_content))
365             if type(r) == list:
366                 dx,dy = w._rect_content.topleft
367                 for rr in r:
368                     rr.x,rr.y = rr.x+dx,rr.y+dy
369             return r
370         return theme_update
371         
372     def open(self,w,func):
373         def theme_open(widget=None,x=None,y=None):
374             if not hasattr(w,'_rect_content'):
375                 # HACK: so that container.open won't resize again!
376                 w.rect.w,w.rect.h = w.resize()
377             rect = w._rect_content
378             ##print w.__class__.__name__, rect
379             if x != None: x += rect.x
380             if y != None: y += rect.y
381             return func(widget,x,y)
382         return theme_open
383
384     def decorate(self,widget,level):
385         """Interface method -- decorate a widget.
386         
387         The theme system is given the opportunity to decorate a widget 
388         methods at the end of the Widget initializer.
389
390         Arguments:
391             widget -- the widget to be decorated
392             level -- the amount of decoration to do, False for none, True for 
393                 normal amount, 'app' for special treatment of App objects.
394         
395         """        
396
397         w = widget
398         if level == False: return
399         
400         if type(w.style.background) != int:
401             w.background = Background(w,self)    
402         
403         if level == 'app': return
404         
405         for k,v in list(w.style.__dict__.items()):
406             if k in ('border','margin','padding'):
407                 for kk in ('top','bottom','left','right'):
408                     setattr(w.style,'%s_%s'%(k,kk),v)
409
410         w.paint = self.paint(w,w.paint)
411         w.event = self.event(w,w.event)
412         w.update = self.update(w,w.update)
413         w.resize = self.resize(w,w.resize)
414         w.open = self.open(w,w.open)
415
416     def render(self,surf,box,r,size=None,offset=None):
417         """Renders a box using an image.
418
419         Arguments:
420             surf -- the target pygame surface
421             box -- pygame surface or color
422             r -- pygame rect describing the size of the image to render
423
424         If 'box' is a surface, it is interpreted as a 3x3 grid of tiles. The 
425         corner tiles are rendered in the corners of the box. The side tiles 
426         are used to fill the top, bottom and sides of the box. The centre tile 
427         is used to fill the interior of the box.
428         """
429
430         if box == 0: return
431
432         if is_color(box):
433             surf.fill(box,r)
434             return
435         
436         x,y,w,h=r.x,r.y,r.w,r.h
437
438         if (size and offset):
439             pass
440 #        destx = x
441 #        desty = y
442
443         # Calculate the size of each tile
444         tilew, tileh = int(box.get_width()/3), int(box.get_height()/3)
445         xx, yy = x+w, y+h
446         src = pygame.rect.Rect(0, 0, tilew, tileh)
447         dest = pygame.rect.Rect(0, 0, tilew, tileh)
448
449         # Render the interior of the box
450         surf.set_clip(pygame.Rect(x+tilew, y+tileh, w-tilew*2, h-tileh*2))
451         src.x,src.y = tilew,tileh
452         for dest.y in range(y+tileh,yy-tileh,tileh):
453             for dest.x in range(x+tilew,xx-tilew,tilew): 
454                 surf.blit(box,dest,src)
455
456         # Render the top side of the box
457         surf.set_clip(pygame.Rect(x+tilew,y,w-tilew*2,tileh))
458         src.x,src.y,dest.y = tilew,0,y
459         for dest.x in range(x+tilew, xx-tilew*2+tilew, tilew): 
460             surf.blit(box,dest,src)
461         
462         # Render the bottom side
463         surf.set_clip(pygame.Rect(x+tilew,yy-tileh,w-tilew*2,tileh))
464         src.x,src.y,dest.y = tilew,tileh*2,yy-tileh
465         for dest.x in range(x+tilew,xx-tilew*2+tilew,tilew): 
466             surf.blit(box,dest,src)
467
468         # Render the left side
469         surf.set_clip(pygame.Rect(x,y+tileh,xx,h-tileh*2))
470         src.y,src.x,dest.x = tileh,0,x
471         for dest.y in range(y+tileh,yy-tileh*2+tileh,tileh): 
472             surf.blit(box,dest,src)
473
474         # Render the right side
475         surf.set_clip(pygame.Rect(xx-tilew,y+tileh,xx,h-tileh*2))
476         src.y,src.x,dest.x=tileh,tilew*2,xx-tilew
477         for dest.y in range(y+tileh,yy-tileh*2+tileh,tileh): 
478             surf.blit(box,dest,src)
479
480         # Render the upper-left corner
481         surf.set_clip()
482         src.x,src.y,dest.x,dest.y = 0,0,x,y
483         surf.blit(box,dest,src)
484         
485         # Render the upper-right corner
486         src.x,src.y,dest.x,dest.y = tilew*2,0,xx-tilew,y
487         surf.blit(box,dest,src)
488         
489         # Render the lower-left corner
490         src.x,src.y,dest.x,dest.y = 0,tileh*2,x,yy-tileh
491         surf.blit(box,dest,src)
492         
493         # Render the lower-right corner
494         src.x,src.y,dest.x,dest.y = tilew*2,tileh*2,xx-tilew,yy-tileh
495         surf.blit(box,dest,src)
496
497
498 class Background(widget.Widget):
499     def __init__(self,value,theme,**params):
500         params['decorate'] = False
501         widget.Widget.__init__(self,**params)
502         self.value = value
503         self.theme = theme
504     
505     def paint(self, s, size=None, offset=None):
506         r = pygame.Rect(0,0,s.get_width(),s.get_height())
507         v = self.value.style.background
508         self.theme.render(s,v,r, size=size, offset=offset)
509

UCC git Repository :: git.ucc.asn.au