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