9 from configparser import ConfigParser
11 from ConfigParser import ConfigParser
16 from .errors import StyleError
17 from .basic import parse_color, is_color
19 __file__ = os.path.abspath(__file__)
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()).
29 # Image extensions automatically recognized by the theme class
30 image_extensions = (".gif", ".jpg", ".bmp", ".png", ".tga")
32 def __init__(self,dirs='default'):
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.
42 theme = gui.Theme("default")
43 theme = gui.Theme(["mytheme","mytheme2"])
52 def _preload(self,ds):
53 if not isinstance(ds, list):
56 if d not in self._loaded:
58 self._loaded.append(d)
60 def _load(self, name):
61 #theme_dir = themes[name]
63 #try to load the local dir, or absolute path
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))
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))
78 if os.path.isdir(dname): break
80 if not os.path.isdir(dname):
81 raise Exception('could not find theme '+name)
83 # Normalize the path to make it look nicer (gets rid of the ..'s)
84 dname = os.path.normpath(dname)
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):
91 for line in f.readlines():
92 args = line.strip().split()
98 (cls, attr, vals) = (args[0], args[1], args[2:])
100 (cls, pcls) = cls.split(":")
102 self.config[cls, pcls, attr] = (dname, vals)
107 # Try parsing the theme data as an ini file
108 fname = os.path.join(dname,"style.ini")
109 if os.path.isfile(fname):
113 for section in cfg.sections():
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)
123 # The folder probably doesn't contain a theme
124 raise IOError("Cannot load theme: missing style.ini or config.txt")
126 def _get(self, cls, pcls, attr):
127 key = (cls, pcls, attr)
128 if not key in self.config:
131 if key in self.cache:
132 # This property is already in the cache
133 return self.cache[key]
135 (dname, vals) = self.config[key]
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]))
141 elif (attr == "color" or attr == "background"):
142 # This is a color value
143 v = parse_color(vals[0])
145 elif (attr == "font"):
146 # This is a font value
149 if (name.endswith(".ttf")):
150 # Load the font from a file
151 v = pygame.font.Font(os.path.join(dname, name), size)
153 # Must be a system font
154 v = pygame.font.SysFont(name, size)
164 # TODO - obsolete, use 'getstyle' below instead
165 def get(self,cls,pcls,attr):
167 return self.getstyle(cls, pcls, attr)
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.
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.
181 This method is called from gui.style
186 # Load the default theme
187 self._preload("default")
189 o = (cls, pcls, attr)
191 v = self._get(cls, pcls, attr)
195 v = self._get(cls, "", attr)
199 v = self._get("default", "", attr)
203 # The style doesn't exist
205 raise StyleError("Style not defined: '%s', '%s', '%s'" % o)
207 # Draws a box around the surface in the given style
208 def box(self, style, surf):
210 if style.border_color != 0:
211 c = style.border_color
212 w,h = surf.get_size()
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))
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
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
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,
236 rect.w + left + right,
237 rect.h + top + bottom)
239 def theme_resize(width=None,height=None):
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
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)
261 if width == None: width = tilew
262 if height == None: height = tileh
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)
269 #width = max(tilew,w.style.width-tw)
270 #height = max(tileh,w.style.height-th)
272 r = pygame.Rect(left,top,width,height)
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)
278 # align it within it's zone of power.
279 rect = pygame.Rect(left, top, tilew, tileh)
282 rect.x += (w.style.align+1)*dx/2
283 rect.y += (w.style.valign+1)*dy/2
285 w._rect_content = rect
287 return (w._rect_margin.w, w._rect_margin.h)
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.
296 # if not hasattr(w,'_disabled_bkgr'):
297 # w._disabled_bkgr = s.convert()
299 # s = w._disabled_bkgr.convert()
301 # if not hasattr(w,'_theme_paint_bkgr'):
302 # w._theme_paint_bkgr = s.convert()
304 # s.blit(w._theme_paint_bkgr,(0,0))
308 # s = w._theme_paint_bkgr.convert()
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()
321 w.background.paint(surface.subsurface(s,w._rect_border))
323 self.box(w.style, surface.subsurface(s,w._rect_border))
324 r = func(surface.subsurface(s,w._rect_content))
334 def event(self,w,func):
336 rect = w._rect_content
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.
342 if e.type == MOUSEBUTTONUP or e.type == MOUSEBUTTONDOWN:
343 sub = pygame.event.Event(e.type,{
345 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
346 elif e.type == CLICK:
347 sub = pygame.event.Event(e.type,{
349 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y)})
350 elif e.type == MOUSEMOTION:
351 sub = pygame.event.Event(e.type,{
353 'pos':(e.pos[0]-rect.x,e.pos[1]-rect.y),
361 def update(self,w,func):
363 if w.disabled: return []
364 r = func(surface.subsurface(s,w._rect_content))
366 dx,dy = w._rect_content.topleft
368 rr.x,rr.y = rr.x+dx,rr.y+dy
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)
384 def decorate(self,widget,level):
385 """Interface method -- decorate a widget.
387 The theme system is given the opportunity to decorate a widget
388 methods at the end of the Widget initializer.
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.
398 if level == False: return
400 if type(w.style.background) != int:
401 w.background = Background(w,self)
403 if level == 'app': return
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)
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)
416 def render(self,surf,box,r,size=None,offset=None):
417 """Renders a box using an image.
420 surf -- the target pygame surface
421 box -- pygame surface or color
422 r -- pygame rect describing the size of the image to render
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.
436 x,y,w,h=r.x,r.y,r.w,r.h
438 if (size and offset):
443 # Calculate the size of each tile
444 tilew, tileh = int(box.get_width()/3), int(box.get_height()/3)
446 src = pygame.rect.Rect(0, 0, tilew, tileh)
447 dest = pygame.rect.Rect(0, 0, tilew, tileh)
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)
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)
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)
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)
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)
480 # Render the upper-left corner
482 src.x,src.y,dest.x,dest.y = 0,0,x,y
483 surf.blit(box,dest,src)
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)
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)
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)
498 class Background(widget.Widget):
499 def __init__(self,value,theme,**params):
500 params['decorate'] = False
501 widget.Widget.__init__(self,**params)
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)