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