Let ChoiceBox font and calculated size be set from the aliasses.
[openblackhole/openblackhole-enigma2.git] / skin.py
1 from Tools.Profile import profile
2 profile("LOAD:ElementTree")
3 import xml.etree.cElementTree
4 import os
5
6 profile("LOAD:enigma_skin")
7 from enigma import eSize, ePoint, gFont, eWindow, eLabel, ePixmap, eWindowStyleManager, \
8         addFont, gRGB, eWindowStyleSkinned
9 from Components.config import ConfigSubsection, ConfigText, config
10 from Components.Converter.Converter import Converter
11 from Components.Sources.Source import Source, ObsoleteSource
12 from Tools.Directories import resolveFilename, SCOPE_SKIN, SCOPE_SKIN_IMAGE, SCOPE_FONTS, SCOPE_CURRENT_SKIN, SCOPE_CONFIG, fileExists
13 from Tools.Import import my_import
14 from Tools.LoadPixmap import LoadPixmap
15
16 colorNames = {}
17 # Predefined fonts, typically used in built-in screens and for components like
18 # the movie list and so.
19 fonts = {
20         "Body": ("Regular", 18, 22, 16),
21         "ChoiceList": ("Regular", 20, 24, 18),
22 }
23
24 def dump(x, i=0):
25         print " " * i + str(x)
26         try:
27                 for n in x.childNodes:
28                         dump(n, i + 1)
29         except:
30                 None
31
32 class SkinError(Exception):
33         def __init__(self, message):
34                 self.msg = message
35
36         def __str__(self):
37                 return "{%s}: %s. Please contact the skin's author!" % (config.skin.primary_skin.value, self.msg)
38
39 dom_skins = [ ]
40
41 def addSkin(name, scope = SCOPE_SKIN):
42         # read the skin
43         filename = resolveFilename(scope, name)
44         if fileExists(filename):
45                 mpath = os.path.dirname(filename) + "/"
46                 dom_skins.append((mpath, xml.etree.cElementTree.parse(filename).getroot()))
47                 return True
48         return False
49
50 # we do our best to always select the "right" value
51 # skins are loaded in order of priority: skin with
52 # highest priority is loaded last, usually the user-provided
53 # skin.
54
55 # currently, loadSingleSkinData (colors, bordersets etc.)
56 # are applied one-after-each, in order of ascending priority.
57 # the dom_skin will keep all screens in descending priority,
58 # so the first screen found will be used.
59
60 # example: loadSkin("nemesis_greenline/skin.xml")
61 config.skin = ConfigSubsection()
62 DEFAULT_SKIN = "PLi-HD/skin.xml"
63 # on SD hardware, PLi-HD will not be available
64 if not fileExists(resolveFilename(SCOPE_SKIN, DEFAULT_SKIN)):
65         # in that case, fallback to Magic (which is an SD skin)
66         DEFAULT_SKIN = "Magic/skin.xml"
67 config.skin.primary_skin = ConfigText(default=DEFAULT_SKIN)
68
69 profile("LoadSkin")
70 try:
71         addSkin('skin_user.xml', SCOPE_CONFIG)
72 except (SkinError, IOError, AssertionError), err:
73         print "not loading user skin: ", err
74
75 # Only one of these is present, compliments of AM_CONDITIONAL
76 display_skin_id = 1
77 addSkin('skin_display.xml')
78 if addSkin('skin_display96.xml'):
79         # Color OLED
80         display_skin_id = 2
81 addSkin('skin_text.xml')
82
83 addSkin('skin_subtitles.xml')
84
85 try:
86         if not addSkin(config.skin.primary_skin.value):
87                 raise SkinError, "primary skin not found"
88 except Exception, err:
89         print "SKIN ERROR:", err
90         skin = DEFAULT_SKIN
91         if config.skin.primary_skin.value == skin:
92                 skin = 'skin.xml'
93         print "defaulting to standard skin...", skin
94         config.skin.primary_skin.value = skin
95         addSkin(skin)
96         del skin
97
98 addSkin('skin_default.xml')
99 profile("LoadSkinDefaultDone")
100
101 def parseCoordinate(s, e, size=0, font=None):
102         s = s.strip()
103         if s == "center":
104                 val = (e - size)/2
105         elif s == '*':
106                 return None
107         else:
108                 if s[0] is 'e':
109                         val = e
110                         s = s[1:]
111                 elif s[0] is 'c':
112                         val = e/2
113                         s = s[1:]
114                 else:
115                         val = 0;
116                 if s:
117                         if s[-1] is '%':
118                                 val += e * int(s[:-1]) / 100
119                         elif s[-1] is 'w':
120                                 val += fonts[font][3] * int(s[:-1]) / 24;
121                         elif s[-1] is 'h':
122                                 val += fonts[font][2] * int(s[:-1]) / 24;
123                         else:
124                                 val += int(s)
125         if val < 0:
126                 val = 0
127         return val
128                 
129
130 def getParentSize(object, desktop):
131         size = eSize()
132         if object:
133                 parent = object.getParent()
134                 # For some widgets (e.g. ScrollLabel) the skin attributes are applied to
135                 # a child widget, instead of to the widget itself. In that case, the parent
136                 # we have here is not the real parent, but it is the main widget.
137                 # We have to go one level higher to get the actual parent.
138                 # We can detect this because the 'parent' will not have a size yet
139                 # (the main widget's size will be calculated internally, as soon as the child
140                 # widget has parsed the skin attributes)
141                 if parent and parent.size().isEmpty():
142                         parent = parent.getParent()
143                 if parent:
144                         size = parent.size()
145                 elif desktop:
146                         #widget has no parent, use desktop size instead for relative coordinates
147                         size = desktop.size()
148         return size
149
150 def parsePosition(s, scale, object = None, desktop = None, size = None):
151         x, y = s.split(',')
152         parentsize = eSize()
153         if object and (x[0] in ('c', 'e') or y[0] in ('c', 'e')):
154                 parentsize = getParentSize(object, desktop)
155         xval = parseCoordinate(x, parentsize.width(), size and size.width())
156         yval = parseCoordinate(y, parentsize.height(), size and size.height())
157         return ePoint(xval * scale[0][0] / scale[0][1], yval * scale[1][0] / scale[1][1])
158
159 def parseSize(s, scale, object = None, desktop = None):
160         x, y = s.split(',')
161         parentsize = eSize()
162         if object and (x[0] in ('c', 'e') or y[0] in ('c', 'e')):
163                 parentsize = getParentSize(object, desktop)
164         xval = parseCoordinate(x, parentsize.width())
165         yval = parseCoordinate(y, parentsize.height())
166         return eSize(xval * scale[0][0] / scale[0][1], yval * scale[1][0] / scale[1][1])
167
168 def parseFont(s, scale):
169         try:
170                 f = fonts[s]
171                 name = f[0]
172                 size = f[1]
173         except:
174                 name, size = s.split(';')
175         return gFont(name, int(size) * scale[0][0] / scale[0][1])
176
177 def parseColor(s):
178         if s[0] != '#':
179                 try:
180                         return colorNames[s]
181                 except:
182                         raise SkinError("color '%s' must be #aarrggbb or valid named color" % (s))
183         return gRGB(int(s[1:], 0x10))
184
185 def collectAttributes(skinAttributes, node, context, skin_path_prefix=None, ignore=(), filenames=frozenset(("pixmap", "pointer", "seek_pointer", "backgroundPixmap", "selectionPixmap", "sliderPixmap", "scrollbarbackgroundPixmap"))):
186         # walk all attributes
187         size = None
188         pos = None
189         font = None
190         for attrib, value in node.items():
191                 if attrib not in ignore:
192                         if attrib in filenames:
193                                 value = resolveFilename(SCOPE_SKIN_IMAGE, value, path_prefix=skin_path_prefix)
194                         # Bit of a hack this, really. When a window has a flag (e.g. wfNoBorder)
195                         # it needs to be set at least before the size is set, in order for the
196                         # window dimensions to be calculated correctly in all situations.
197                         # If wfNoBorder is applied after the size has been set, the window will fail to clear the title area.
198                         # Similar situation for a scrollbar in a listbox; when the scrollbar setting is applied after
199                         # the size, a scrollbar will not be shown until the selection moves for the first time
200                         if attrib == 'size':
201                                 size = value.encode("utf-8")
202                         elif attrib == 'position':
203                                 pos = value.encode("utf-8")
204                         elif attrib == 'font':
205                                 font = value.encode("utf-8")
206                                 skinAttributes.append((attrib, font))
207                         else:
208                                 skinAttributes.append((attrib, value.encode("utf-8")))
209         if pos is not None:
210                 pos, size = context.parse(pos, size, font)
211                 skinAttributes.append(('position', pos))
212         if size is not None:
213                 skinAttributes.append(('size', size))
214
215
216 def loadPixmap(path, desktop):
217         cached = False
218         option = path.find("#")
219         if option != -1:
220                 options = path[option+1:].split(',')
221                 path = path[:option]
222                 cached = "cached" in options
223         ptr = LoadPixmap(path, desktop, cached)
224         if ptr is None:
225                 raise SkinError("pixmap file %s not found!" % (path))
226         return ptr
227
228 class AttributeParser:
229         def __init__(self, guiObject, desktop, scale = ((1,1),(1,1))):
230                 self.guiObject = guiObject
231                 self.desktop = desktop
232                 self.scale = scale
233         def applyOne(self, attrib, value):
234                 try:
235                         getattr(self, attrib)(value)
236                 except AttributeError:
237                         print "[Skin] Attribute not implemented:", attrib, "value:", value
238                 except SkinError, ex:
239                         print "[Skin] Error:", ex
240         def applyAll(self, attrs):
241                 for attrib, value in attrs:
242                         try:
243                                 getattr(self, attrib)(value)
244                         except AttributeError:
245                                 print "[Skin] Attribute not implemented:", attrib, "value:", value
246                         except SkinError, ex:
247                                 print "[Skin] Error:", ex
248         def position(self, value):
249                 if isinstance(value, tuple):
250                         self.guiObject.move(ePoint(*value))
251                 else: 
252                         self.guiObject.move(parsePosition(value, self.scale, self.guiObject, self.desktop, self.guiObject.csize()))
253         def size(self, value):
254                 if isinstance(value, tuple):
255                         self.guiObject.resize(eSize(*value))
256                 else:
257                         self.guiObject.resize(parseSize(value, self.scale, self.guiObject, self.desktop))
258         def title(self, value):
259                 self.guiObject.setTitle(_(value))
260         def text(self, value):
261                 self.guiObject.setText(_(value))
262         def font(self, value):
263                 self.guiObject.setFont(parseFont(value, self.scale))
264         def zPosition(self, value):
265                 self.guiObject.setZPosition(int(value))
266         def itemHeight(self, value):
267                 self.guiObject.setItemHeight(int(value))
268         def pixmap(self, value):
269                 ptr = loadPixmap(value, self.desktop)
270                 self.guiObject.setPixmap(ptr)
271         def backgroundPixmap(self, value):
272                 ptr = loadPixmap(value, self.desktop)
273                 self.guiObject.setBackgroundPicture(ptr)
274         def selectionPixmap(self, value):
275                 ptr = loadPixmap(value, self.desktop)
276                 self.guiObject.setSelectionPicture(ptr)
277         def sliderPixmap(self, value):
278                 ptr = loadPixmap(value, self.desktop)
279                 self.guiObject.setSliderPicture(ptr)
280         def scrollbarbackgroundPixmap(self, value):
281                 ptr = loadPixmap(value, self.desktop)
282                 self.guiObject.setScrollbarBackgroundPicture(ptr)
283         def alphatest(self, value):
284                 self.guiObject.setAlphatest(
285                         { "on": 1,
286                           "off": 0,
287                           "blend": 2,
288                         }[value])
289         def scale(self, value):
290                 self.guiObject.setScale(1)
291         def orientation(self, value): # used by eSlider
292                 try:
293                         self.guiObject.setOrientation(*
294                                 { "orVertical": (self.guiObject.orVertical, False),
295                                         "orTopToBottom": (self.guiObject.orVertical, False),
296                                         "orBottomToTop": (self.guiObject.orVertical, True),
297                                         "orHorizontal": (self.guiObject.orHorizontal, False),
298                                         "orLeftToRight": (self.guiObject.orHorizontal, False),
299                                         "orRightToLeft": (self.guiObject.orHorizontal, True),
300                                 }[value])
301                 except KeyError:
302                         print "oprientation must be either orVertical or orHorizontal!"
303         def valign(self, value):
304                 try:
305                         self.guiObject.setVAlign(
306                                 { "top": self.guiObject.alignTop,
307                                         "center": self.guiObject.alignCenter,
308                                         "bottom": self.guiObject.alignBottom
309                                 }[value])
310                 except KeyError:
311                         print "valign must be either top, center or bottom!"
312         def halign(self, value):
313                 try:
314                         self.guiObject.setHAlign(
315                                 { "left": self.guiObject.alignLeft,
316                                         "center": self.guiObject.alignCenter,
317                                         "right": self.guiObject.alignRight,
318                                         "block": self.guiObject.alignBlock
319                                 }[value])
320                 except KeyError:
321                         print "halign must be either left, center, right or block!"
322         def textOffset(self, value):
323                 x, y = value.split(',')
324                 self.guiObject.setTextOffset(ePoint(int(x) * self.scale[0][0] / self.scale[0][1], int(y) * self.scale[1][0] / self.scale[1][1]))
325         def flags(self, value):
326                 flags = value.split(',')
327                 for f in flags:
328                         try:
329                                 fv = eWindow.__dict__[f]
330                                 self.guiObject.setFlag(fv)
331                         except KeyError:
332                                 print "illegal flag %s!" % f
333         def backgroundColor(self, value):
334                 self.guiObject.setBackgroundColor(parseColor(value))
335         def backgroundColorSelected(self, value):
336                 self.guiObject.setBackgroundColorSelected(parseColor(value))
337         def foregroundColor(self, value):
338                 self.guiObject.setForegroundColor(parseColor(value))
339         def foregroundColorSelected(self, value):
340                 self.guiObject.setForegroundColorSelected(parseColor(value))
341         def shadowColor(self, value):
342                 self.guiObject.setShadowColor(parseColor(value))
343         def selectionDisabled(self, value):
344                 self.guiObject.setSelectionEnable(0)
345         def transparent(self, value):
346                 self.guiObject.setTransparent(int(value))
347         def borderColor(self, value):
348                 self.guiObject.setBorderColor(parseColor(value))
349         def borderWidth(self, value):
350                 self.guiObject.setBorderWidth(int(value))
351         def scrollbarMode(self, value):
352                 self.guiObject.setScrollbarMode(getattr(self.guiObject, value))
353                 #       { "showOnDemand": self.guiObject.showOnDemand,
354                 #               "showAlways": self.guiObject.showAlways,
355                 #               "showNever": self.guiObject.showNever,
356                 #               "showLeft": self.guiObject.showLeft
357                 #       }[value])
358         def enableWrapAround(self, value):
359                 self.guiObject.setWrapAround(True)
360         def itemHeight(self, value):
361                 self.guiObject.setItemHeight(int(value))
362         def pointer(self, value):
363                 (name, pos) = value.split(':')
364                 pos = parsePosition(pos, self.scale)
365                 ptr = loadPixmap(name, self.desktop)
366                 self.guiObject.setPointer(0, ptr, pos)
367         def seek_pointer(self, value):
368                 (name, pos) = value.split(':')
369                 pos = parsePosition(pos, self.scale)
370                 ptr = loadPixmap(name, self.desktop)
371                 self.guiObject.setPointer(1, ptr, pos)
372         def shadowOffset(self, value):
373                 self.guiObject.setShadowOffset(parsePosition(value, self.scale))
374         def noWrap(self, value):
375                 self.guiObject.setNoWrap(1)
376
377 def applySingleAttribute(guiObject, desktop, attrib, value, scale = ((1,1),(1,1))):
378         # Someone still using applySingleAttribute?
379         AttributeParser(guiObject, desktop, scale).applyOne(attrib, value)
380
381 def applyAllAttributes(guiObject, desktop, attributes, scale):
382         AttributeParser(guiObject, desktop, scale).applyAll(attributes)
383
384 def loadSingleSkinData(desktop, skin, path_prefix):
385         """loads skin data like colors, windowstyle etc."""
386         assert skin.tag == "skin", "root element in skin must be 'skin'!"
387         for c in skin.findall("output"):
388                 id = c.attrib.get('id')
389                 if id:
390                         id = int(id)
391                 else:
392                         id = 0
393                 if id == 0: # framebuffer
394                         for res in c.findall("resolution"):
395                                 get_attr = res.attrib.get
396                                 xres = get_attr("xres")
397                                 if xres:
398                                         xres = int(xres)
399                                 else:
400                                         xres = 720
401                                 yres = get_attr("yres")
402                                 if yres:
403                                         yres = int(yres)
404                                 else:
405                                         yres = 576
406                                 bpp = get_attr("bpp")
407                                 if bpp:
408                                         bpp = int(bpp)
409                                 else:
410                                         bpp = 32
411                                 #print "Resolution:", xres,yres,bpp
412                                 from enigma import gMainDC
413                                 gMainDC.getInstance().setResolution(xres, yres)
414                                 desktop.resize(eSize(xres, yres))
415                                 if bpp != 32:
416                                         # load palette (not yet implemented)
417                                         pass
418
419         for c in skin.findall("colors"):
420                 for color in c.findall("color"):
421                         get_attr = color.attrib.get
422                         name = get_attr("name")
423                         color = get_attr("value")
424                         if name and color:
425                                 colorNames[name] = parseColor(color)
426                                 #print "Color:", name, color
427                         else:
428                                 raise SkinError("need color and name, got %s %s" % (name, color))
429
430         for c in skin.findall("fonts"):
431                 for font in c.findall("font"):
432                         get_attr = font.attrib.get
433                         filename = get_attr("filename", "<NONAME>")
434                         name = get_attr("name", "Regular")
435                         scale = get_attr("scale")
436                         if scale:
437                                 scale = int(scale)
438                         else:
439                                 scale = 100
440                         is_replacement = get_attr("replacement") and True or False
441                         render = get_attr("render")
442                         if render:
443                                 render = int(render)
444                         else:
445                                 render = 0
446                         resolved_font = resolveFilename(SCOPE_FONTS, filename, path_prefix=path_prefix)
447                         if not fileExists(resolved_font): #when font is not available look at current skin path
448                                 skin_path = resolveFilename(SCOPE_CURRENT_SKIN, filename)
449                                 if fileExists(skin_path):
450                                         resolved_font = skin_path
451                         addFont(resolved_font, name, scale, is_replacement, render)
452                         #print "Font: ", resolved_font, name, scale, is_replacement
453                 for alias in c.findall("alias"):
454                         get = alias.attrib.get
455                         try:
456                                 name = get("name")
457                                 font = get("font")
458                                 size = int(get("size"))
459                                 height = int(get("height", size)) # to be calculated some day
460                                 width = int(get("width", size))
461                                 global fonts
462                                 fonts[name] = (font, size, height, width)
463                         except Exception, ex:
464                                 print "[SKIN] bad font alias", ex
465
466
467         for c in skin.findall("subtitles"):
468                 from enigma import eWidget, eSubtitleWidget
469                 scale = ((1,1),(1,1))
470                 for substyle in c.findall("sub"):
471                         get_attr = substyle.attrib.get
472                         font = parseFont(get_attr("font"), scale)
473                         col = get_attr("foregroundColor")
474                         if col:
475                                 foregroundColor = parseColor(col)
476                                 haveColor = 1
477                         else:
478                                 foregroundColor = gRGB(0xFFFFFF)
479                                 haveColor = 0
480                         col = get_attr("borderColor")
481                         if col:
482                                 borderColor = parseColor(col)
483                         else:
484                                 borderColor = gRGB(0)
485                         borderwidth = get_attr("borderWidth")
486                         if borderwidth is None:
487                                 # default: use a subtitle border
488                                 borderWidth = 3
489                         else:
490                                 borderWidth = int(borderwidth)
491                         face = eSubtitleWidget.__dict__[get_attr("name")]
492                         eSubtitleWidget.setFontStyle(face, font, haveColor, foregroundColor, borderColor, borderWidth)
493
494         for windowstyle in skin.findall("windowstyle"):
495                 style = eWindowStyleSkinned()
496                 id = windowstyle.attrib.get("id")
497                 if id:
498                         id = int(id)
499                 else:
500                         id = 0
501                 #print "windowstyle:", id
502                 # defaults
503                 font = gFont("Regular", 20)
504                 offset = eSize(20, 5)
505                 for title in windowstyle.findall("title"):
506                         get_attr = title.attrib.get
507                         offset = parseSize(get_attr("offset"), ((1,1),(1,1)))
508                         font = parseFont(get_attr("font"), ((1,1),(1,1)))
509
510                 style.setTitleFont(font);
511                 style.setTitleOffset(offset)
512                 #print "  ", font, offset
513                 for borderset in windowstyle.findall("borderset"):
514                         bsName = str(borderset.attrib.get("name"))
515                         for pixmap in borderset.findall("pixmap"):
516                                 get_attr = pixmap.attrib.get
517                                 bpName = get_attr("pos")
518                                 filename = get_attr("filename")
519                                 if filename and bpName:
520                                         png = loadPixmap(resolveFilename(SCOPE_SKIN_IMAGE, filename, path_prefix=path_prefix), desktop)
521                                         style.setPixmap(eWindowStyleSkinned.__dict__[bsName], eWindowStyleSkinned.__dict__[bpName], png)
522                                 #print "  borderset:", bpName, filename
523                 for color in windowstyle.findall("color"):
524                         get_attr = color.attrib.get
525                         colorType = get_attr("name")
526                         color = parseColor(get_attr("color"))
527                         try:
528                                 style.setColor(eWindowStyleSkinned.__dict__["col" + colorType], color)
529                         except:
530                                 raise SkinError("Unknown color %s" % (colorType))
531                                 #pass
532                         #print "  color:", type, color
533                 x = eWindowStyleManager.getInstance()
534                 x.setStyle(id, style)
535
536 dom_screens = {}
537
538 def loadSkin(name, scope = SCOPE_SKIN):
539         # Now a utility for plugins to add skin data to the screens
540         global dom_screens, display_skin_id
541         filename = resolveFilename(scope, name)
542         if fileExists(filename):
543                 path = os.path.dirname(filename) + "/"
544                 for elem in xml.etree.cElementTree.parse(filename).getroot():
545                         if elem.tag == 'screen':
546                                 name = elem.attrib.get('name', None)
547                                 if name:
548                                         sid = elem.attrib.get('id', None)
549                                         if sid and (sid != display_skin_id):
550                                                 # not for this display
551                                                 elem.clear()
552                                                 continue
553                                         if name in dom_screens:
554                                                 print "loadSkin: Screen already defined elsewhere:", name
555                                                 elem.clear()
556                                         else:
557                                                 dom_screens[name] = (elem, path)
558                                 else:
559                                         elem.clear()
560                         else:
561                                 elem.clear()
562
563 def loadSkinData(desktop):
564         # Kinda hackish, but this is called once by mytest.py
565         global dom_skins
566         skins = dom_skins[:]
567         skins.reverse()
568         for (path, dom_skin) in skins:
569                 loadSingleSkinData(desktop, dom_skin, path)
570                 for elem in dom_skin:
571                         if elem.tag == 'screen':
572                                 name = elem.attrib.get('name', None)
573                                 if name:
574                                         sid = elem.attrib.get('id', None)
575                                         if sid and (sid != display_skin_id):
576                                                 # not for this display
577                                                 elem.clear()
578                                                 continue
579                                         if name in dom_screens:
580                                                 # Kill old versions, save memory
581                                                 dom_screens[name][0].clear()
582                                         dom_screens[name] = (elem, path)
583                                 else:
584                                         # without name, it's useless!
585                                         elem.clear()
586                         else:
587                                 # non-screen element, no need for it any longer
588                                 elem.clear()
589         # no longer needed, we know where the screens are now.
590         del dom_skins
591
592 class additionalWidget:
593         pass
594
595 # Class that makes a tuple look like something else. Some plugins just assume
596 # that size is a string and try to parse it. This class makes that work.
597 class SizeTuple(tuple):
598         def split(self, *args):
599                 return (str(self[0]), str(self[1]))
600         def strip(self, *args):
601                 return '%s,%s' % self
602         def __str__(self):
603                 return '%s,%s' % self
604
605 class SkinContext:
606         def __init__(self, parent=None, pos=None, size=None, font=None):
607                 if parent is not None:
608                         if pos is not None:
609                                 pos, size = parent.parse(pos, size, font)
610                                 self.x, self.y = pos
611                                 self.w, self.h = size
612                         else:
613                                 self.x = None
614                                 self.y = None
615                                 self.w = None
616                                 self.h = None
617         def __str__(self):
618                 return "Context (%s,%s)+(%s,%s) " % (self.x, self.y, self.w, self.h)
619         def parse(self, pos, size, font):
620                 if pos == "fill":
621                         pos = (self.x, self.y)
622                         size = (self.w, self.h)
623                         self.w = 0
624                         self.h = 0
625                 else:
626                         w,h = size.split(',')
627                         w = parseCoordinate(w, self.w, 0, font)
628                         h = parseCoordinate(h, self.h, 0, font)
629                         if pos == "bottom":
630                                 pos = (self.x, self.y + self.h - h)
631                                 size = (self.w, h)
632                                 self.h -= h
633                         elif pos == "top":
634                                 pos = (self.x, self.y)
635                                 size = (self.w, h)
636                                 self.h -= h
637                                 self.y += h
638                         elif pos == "left":
639                                 pos = (self.x, self.y)
640                                 size = (w, self.h)
641                                 self.x += w
642                                 self.w -= w
643                         elif pos == "right":
644                                 pos = (self.x + self.w - w, self.y)
645                                 size = (w, self.h)
646                                 self.w -= w
647                         else:
648                                 size = (w, h)
649                                 pos = pos.split(',')
650                                 pos = (self.x + parseCoordinate(pos[0], self.w, size[0], font), self.y + parseCoordinate(pos[1], self.h, size[1], font))
651                 return (SizeTuple(pos), SizeTuple(size))
652
653 class SkinContextStack(SkinContext):
654         # A context that stacks things instead of aligning them
655         def parse(self, pos, size, font):
656                 if pos == "fill":
657                         pos = (self.x, self.y)
658                         size = (self.w, self.h)
659                 else:
660                         w,h = size.split(',')
661                         w = parseCoordinate(w, self.w, 0, font)
662                         h = parseCoordinate(h, self.h, 0, font)
663                         if pos == "bottom":
664                                 pos = (self.x, self.y + self.h - h)
665                                 size = (self.w, h)
666                         elif pos == "top":
667                                 pos = (self.x, self.y)
668                                 size = (self.w, h)
669                         elif pos == "left":
670                                 pos = (self.x, self.y)
671                                 size = (w, self.h)
672                         elif pos == "right":
673                                 pos = (self.x + self.w - w, self.y)
674                                 size = (w, self.h)
675                         else:
676                                 size = (w, h)
677                                 pos = pos.split(',')
678                                 pos = (self.x + parseCoordinate(pos[0], self.w, size[0], font), self.y + parseCoordinate(pos[1], self.h, size[1], font))
679                 return (SizeTuple(pos), SizeTuple(size))
680
681 def readSkin(screen, skin, names, desktop):
682         if not isinstance(names, list):
683                 names = [names]
684
685         name = "<embedded-in-'%s'>" % screen.__class__.__name__
686
687         # try all skins, first existing one have priority
688         global dom_screens
689         for n in names:
690                 myscreen, path = dom_screens.get(n, (None,None))
691                 if myscreen is not None:
692                         # use this name for debug output
693                         name = n
694                         break
695
696         # otherwise try embedded skin
697         if myscreen is None:
698                 myscreen = getattr(screen, "parsedSkin", None)
699
700         # try uncompiled embedded skin
701         if myscreen is None and getattr(screen, "skin", None):
702                 skin = screen.skin
703                 print "[SKIN] Parsing embedded skin"
704                 if (isinstance(skin, tuple)):
705                         for s in skin:
706                                 candidate = xml.etree.cElementTree.fromstring(s)
707                                 if candidate.tag == 'screen':
708                                         sid = candidate.attrib.get('id', None)
709                                         if (not sid) or (int(sid) == display_skin_id):
710                                                 myscreen = candidate 
711                                                 break;
712                         else:
713                                 print "[SKIN] Hey, no suitable screen!"
714                 else:
715                         myscreen = xml.etree.cElementTree.fromstring(skin)
716                 if myscreen:
717                         screen.parsedSkin = myscreen
718         if myscreen is None:
719                 print "[SKIN] No skin to read..."
720                 myscreen = screen.parsedSkin = xml.etree.cElementTree.fromstring("<screen></screen>")
721
722         screen.skinAttributes = [ ]
723         skin_path_prefix = getattr(screen, "skin_path", path)
724
725         context = SkinContext()
726         s = desktop.size()
727         context.x = 0
728         context.y = 0
729         context.w = s.width()
730         context.h = s.height()
731         del s
732         collectAttributes(screen.skinAttributes, myscreen, context, skin_path_prefix, ignore=("name",))
733         context = SkinContext(context, myscreen.attrib.get('position'), myscreen.attrib.get('size'))
734
735         screen.additionalWidgets = [ ]
736         screen.renderer = [ ]
737         visited_components = set()
738
739         # now walk all widgets and stuff
740         def process_none(widget, context):
741                 pass
742
743         def process_widget(widget, context):
744                 get_attr = widget.attrib.get
745                 # ok, we either have 1:1-mapped widgets ('old style'), or 1:n-mapped
746                 # widgets (source->renderer).
747                 wname = get_attr('name')
748                 wsource = get_attr('source')
749                 if wname is None and wsource is None:
750                         print "widget has no name and no source!"
751                         return
752                 if wname:
753                         #print "Widget name=", wname
754                         visited_components.add(wname)
755                         # get corresponding 'gui' object
756                         try:
757                                 attributes = screen[wname].skinAttributes = [ ]
758                         except:
759                                 raise SkinError("component with name '" + wname + "' was not found in skin of screen '" + name + "'!")
760                         # assert screen[wname] is not Source
761                         collectAttributes(attributes, widget, context, skin_path_prefix, ignore=('name',))
762                 elif wsource:
763                         # get corresponding source
764                         #print "Widget source=", wsource
765                         while True: # until we found a non-obsolete source
766                                 # parse our current "wsource", which might specifiy a "related screen" before the dot,
767                                 # for example to reference a parent, global or session-global screen.
768                                 scr = screen
769                                 # resolve all path components
770                                 path = wsource.split('.')
771                                 while len(path) > 1:
772                                         scr = screen.getRelatedScreen(path[0])
773                                         if scr is None:
774                                                 #print wsource
775                                                 #print name
776                                                 raise SkinError("specified related screen '" + wsource + "' was not found in screen '" + name + "'!")
777                                         path = path[1:]
778                                 # resolve the source.
779                                 source = scr.get(path[0])
780                                 if isinstance(source, ObsoleteSource):
781                                         # however, if we found an "obsolete source", issue warning, and resolve the real source.
782                                         print "WARNING: SKIN '%s' USES OBSOLETE SOURCE '%s', USE '%s' INSTEAD!" % (name, wsource, source.new_source)
783                                         print "OBSOLETE SOURCE WILL BE REMOVED %s, PLEASE UPDATE!" % (source.removal_date)
784                                         if source.description:
785                                                 print source.description
786                                         wsource = source.new_source
787                                 else:
788                                         # otherwise, use that source.
789                                         break
790
791                         if source is None:
792                                 raise SkinError("source '" + wsource + "' was not found in screen '" + name + "'!")
793
794                         wrender = get_attr('render')
795                         if not wrender:
796                                 raise SkinError("you must define a renderer with render= for source '%s'" % (wsource))
797                         for converter in widget.findall("convert"):
798                                 ctype = converter.get('type')
799                                 assert ctype, "'convert'-tag needs a 'type'-attribute"
800                                 #print "Converter:", ctype
801                                 try:
802                                         parms = converter.text.strip()
803                                 except:
804                                         parms = ""
805                                 #print "Params:", parms
806                                 converter_class = my_import('.'.join(("Components", "Converter", ctype))).__dict__.get(ctype)
807                                 c = None
808                                 for i in source.downstream_elements:
809                                         if isinstance(i, converter_class) and i.converter_arguments == parms:
810                                                 c = i
811                                 if c is None:
812                                         c = converter_class(parms)
813                                         c.connect(source)
814                                 source = c
815
816                         renderer_class = my_import('.'.join(("Components", "Renderer", wrender))).__dict__.get(wrender)
817                         renderer = renderer_class() # instantiate renderer
818                         renderer.connect(source) # connect to source
819                         attributes = renderer.skinAttributes = [ ]
820                         collectAttributes(attributes, widget, context, skin_path_prefix, ignore=('render', 'source'))
821                         screen.renderer.append(renderer)
822
823         def process_applet(widget, context):
824                 try:
825                         codeText = widget.text.strip()
826                         widgetType = widget.attrib.get('type')
827                         code = compile(codeText, "skin applet", "exec")
828                 except Exception, ex:
829                         raise SkinError("applet failed to compile: " + str(ex))
830                 if widgetType == "onLayoutFinish":
831                         screen.onLayoutFinish.append(code)
832                 else:
833                         raise SkinError("applet type '%s' unknown!" % widgetType)
834
835         def process_elabel(widget, context):
836                 w = additionalWidget()
837                 w.widget = eLabel
838                 w.skinAttributes = [ ]
839                 collectAttributes(w.skinAttributes, widget, context, skin_path_prefix, ignore=('name',))
840                 screen.additionalWidgets.append(w)
841
842         def process_epixmap(widget, context):
843                 w = additionalWidget()
844                 w.widget = ePixmap
845                 w.skinAttributes = [ ]
846                 collectAttributes(w.skinAttributes, widget, context, skin_path_prefix, ignore=('name',))
847                 screen.additionalWidgets.append(w)
848
849         def process_screen(widget, context):
850                 for w in widget.getchildren():
851                         p = processors.get(w.tag, process_none)
852                         try:
853                                 p(w, context)
854                         except SkinError, e:
855                                 print "[Skin] SKIN ERROR in screen '%s' widget '%s':" % (name, w.tag), e
856
857         def process_panel(widget, context):
858                 n = widget.attrib.get('name')
859                 if n:
860                         try:
861                                 s = dom_screens[n]
862                         except KeyError:
863                                 print "[SKIN] Unable to find screen '%s' referred in screen '%s'" % (n, name)
864                         else:
865                                 process_screen(s[0], context)
866                 layout = widget.attrib.get('layout')
867                 if layout == 'stack':
868                         cc = SkinContextStack
869                 else:
870                         cc = SkinContext
871                 try:
872                         c = cc(context, widget.attrib.get('position'), widget.attrib.get('size'), widget.attrib.get('font'))
873                 except Exception, ex:
874                         raise SkinError("Failed to create skincontext (%s,%s,%s) in %s: %s" % (widget.attrib.get('position'), widget.attrib.get('size'), widget.attrib.get('font'), context, ex) )
875                 process_screen(widget, c)
876
877         processors = {
878                 None: process_none,
879                 "widget": process_widget,
880                 "applet": process_applet,
881                 "eLabel": process_elabel,
882                 "ePixmap": process_epixmap,
883                 "panel": process_panel
884         }
885
886         try:
887                 context.x = 0 # reset offsets, all components are relative to screen
888                 context.y = 0 # coordinates.
889                 process_screen(myscreen, context)
890         except Exception, e:
891                 print "[Skin] SKIN ERROR in %s:" % name, e
892
893         from Components.GUIComponent import GUIComponent
894         nonvisited_components = [x for x in set(screen.keys()) - visited_components if isinstance(x, GUIComponent)]
895         assert not nonvisited_components, "the following components in %s don't have a skin entry: %s" % (name, ', '.join(nonvisited_components))
896         # This may look pointless, but it unbinds 'screen' from the nested scope. A better
897         # solution is to avoid the nested scope above and use the context object to pass
898         # things around.
899         screen = None
900         visited_components = None