]>
Commit | Line | Data |
---|---|---|
1 | #---------------------------------------------------------------------- | |
2 | # Name: wx.tools.pywxrc | |
3 | # Purpose: XML resource compiler | |
4 | # | |
5 | # Author: Robin Dunn | |
6 | # Based on wxrc.cpp by Vaclav Slavik, Eduardo Marques | |
7 | # Ported to Python in order to not require yet another | |
8 | # binary in wxPython distributions | |
9 | # | |
10 | # Massive rework by Eli Golovinsky | |
11 | # | |
12 | # RCS-ID: $Id$ | |
13 | # Copyright: (c) 2004 by Total Control Software, 2000 Vaclav Slavik | |
14 | # Licence: wxWindows license | |
15 | #---------------------------------------------------------------------- | |
16 | ||
17 | """ | |
18 | pywxrc -- Python XML resource compiler | |
19 | (see http://wiki.wxpython.org/index.cgi/pywxrc for more info) | |
20 | ||
21 | Usage: python pywxrc.py -h | |
22 | python pywxrc.py [-p] [-g] [-e] [-o filename] xrc input files... | |
23 | ||
24 | -h, --help show help message | |
25 | -p, --python generate python module | |
26 | -g, --gettext output list of translatable strings (may be combined with -p) | |
27 | -e, --embed embed XRC resources in the output file | |
28 | -o, --output output filename, or - for stdout | |
29 | """ | |
30 | ||
31 | import sys, os, getopt, glob, re | |
32 | import xml.dom.minidom as minidom | |
33 | import wx | |
34 | import wx.xrc | |
35 | ||
36 | #---------------------------------------------------------------------- | |
37 | ||
38 | class PythonTemplates: | |
39 | FILE_HEADER = """\ | |
40 | # This file was automatically generated by pywxrc, do not edit by hand. | |
41 | # -*- coding: UTF-8 -*- | |
42 | ||
43 | import wx | |
44 | import wx.xrc as xrc | |
45 | ||
46 | __res = None | |
47 | ||
48 | def get_resources(): | |
49 | \"\"\" This function provides access to the XML resources in this module.\"\"\" | |
50 | global __res | |
51 | if __res == None: | |
52 | __init_resources() | |
53 | return __res | |
54 | ||
55 | """ | |
56 | ||
57 | CLASS_HEADER = """\ | |
58 | class xrc%(windowName)s(wx.%(windowClass)s): | |
59 | def PreCreate(self): | |
60 | \"\"\" This function is called during the class's initialization. | |
61 | ||
62 | Override it for custom setup before the window is created usually to | |
63 | set additional window styles using SetWindowStyle() and SetExtraStyle().\"\"\" | |
64 | pass | |
65 | ||
66 | def __init__(self, parent): | |
67 | # Two stage creation (see http://wiki.wxpython.org/index.cgi/TwoStageCreation) | |
68 | pre = wx.Pre%(windowClass)s() | |
69 | get_resources().LoadOn%(windowClass)s(pre, parent, "%(windowName)s") | |
70 | self.PreCreate() | |
71 | self.PostCreate(pre) | |
72 | ||
73 | # Define variables for the controls | |
74 | """ | |
75 | ||
76 | CREATE_WIDGET_VAR = """\ | |
77 | self.%(widgetName)s = xrc.XRCCTRL(self, \"%(widgetName)s\") | |
78 | """ | |
79 | ||
80 | MENU_CLASS_HEADER = """\ | |
81 | class xrc%(windowName)s(wx.%(windowClass)s): | |
82 | def __init__(self): | |
83 | pre = get_resources().LoadMenu("%(windowName)s") | |
84 | ||
85 | # This is a copy of Robin's PostCreate voodoo magic in wx.Window that | |
86 | # relinks the self object with the menu object. | |
87 | self.this = pre.this | |
88 | self.thisown = pre.thisown | |
89 | pre.thisown = 0 | |
90 | if hasattr(self, '_setOORInfo'): | |
91 | self._setOORInfo(self) | |
92 | if hasattr(self, '_setCallbackInfo'): | |
93 | self._setCallbackInfo(self, self.__class__) | |
94 | ||
95 | # Define variables for the menu items | |
96 | """ | |
97 | ||
98 | CREATE_MENUITEM_VAR = """\ | |
99 | self.%(widgetName)s = self.FindItemById(xrc.XRCID(\"%(widgetName)s\")) | |
100 | """ | |
101 | ||
102 | INIT_RESOURE_HEADER = """\ | |
103 | # ------------------------ Resource data ---------------------- | |
104 | ||
105 | def __init_resources(): | |
106 | global __res | |
107 | __res = xrc.EmptyXmlResource() | |
108 | """ | |
109 | ||
110 | LOAD_RES_FILE = """\ | |
111 | __res.Load('%(resourceFilename)s')""" | |
112 | ||
113 | FILE_AS_STRING = """\ | |
114 | %(filename)s = '''\\ | |
115 | %(fileData)s''' | |
116 | ||
117 | """ | |
118 | ||
119 | PREPARE_MEMFS = """\ | |
120 | wx.FileSystem.AddHandler(wx.MemoryFSHandler()) | |
121 | """ | |
122 | ||
123 | ADD_FILE_TO_MEMFS = """\ | |
124 | wx.MemoryFSHandler.AddFile('XRC/%(memoryPath)s/%(filename)s', %(filename)s) | |
125 | """ | |
126 | ||
127 | LOAD_RES_MEMFS = """\ | |
128 | __res.Load('memory:XRC/%(memoryPath)s/%(resourceFilename)s') | |
129 | """ | |
130 | ||
131 | GETTEXT_DUMMY_FUNC = """ | |
132 | # ----------------------- Gettext strings --------------------- | |
133 | ||
134 | def __gettext_strings(): | |
135 | # This is a dummy function that lists all the strings that are used in | |
136 | # the XRC file in the _("a string") format to be recognized by GNU | |
137 | # gettext utilities (specificaly the xgettext utility) and the | |
138 | # mki18n.py script. For more information see: | |
139 | # http://wiki.wxpython.org/index.cgi/Internationalization | |
140 | ||
141 | def _(str): pass | |
142 | ||
143 | %s | |
144 | """ | |
145 | ||
146 | #---------------------------------------------------------------------- | |
147 | ||
148 | class XmlResourceCompiler: | |
149 | ||
150 | templates = PythonTemplates() | |
151 | ||
152 | """This class generates Python code from XML resource files (XRC).""" | |
153 | ||
154 | def MakePythonModule(self, inputFiles, outputFilename, | |
155 | embedResources=False, generateGetText=False): | |
156 | ||
157 | outputFile = self._OpenOutputFile(outputFilename) | |
158 | ||
159 | classes = [] | |
160 | resources = [] | |
161 | gettextStrings = [] | |
162 | ||
163 | # process all the inputFiles, collecting the output data | |
164 | for inFile in inputFiles: | |
165 | resourceDocument = minidom.parse(inFile) | |
166 | classes.append(self.GenerateClasses(resourceDocument)) | |
167 | ||
168 | if embedResources: | |
169 | res = self.GenerateInitResourcesEmbedded(inFile, resourceDocument) | |
170 | else: | |
171 | res = self.GenerateInitResourcesFile(inFile, resourceDocument) | |
172 | resources.append(res) | |
173 | ||
174 | if generateGetText: | |
175 | gettextStrings += self.FindStringsInNode(resourceDocument.firstChild) | |
176 | ||
177 | # now write it all out | |
178 | print >>outputFile, self.templates.FILE_HEADER | |
179 | ||
180 | # Note: Technically it is not legal to have anything other | |
181 | # than ascii for class and variable names, but since the user | |
182 | # can create the XML with non-ascii names we'll go ahead and | |
183 | # allow for it here, and then let Python complain about it | |
184 | # later when they try to run the program. | |
185 | classes = u"\n".join(classes) | |
186 | print >>outputFile, classes.encode("UTF-8") | |
187 | ||
188 | print >>outputFile, self.templates.INIT_RESOURE_HEADER | |
189 | if embedResources: | |
190 | print >>outputFile, self.templates.PREPARE_MEMFS | |
191 | resources = u"\n".join(resources) | |
192 | print >>outputFile, resources.encode("UTF-8") | |
193 | ||
194 | if generateGetText: | |
195 | # These have already been converted to utf-8... | |
196 | gettextStrings = [' _("%s")' % s for s in gettextStrings] | |
197 | gettextStrings = "\n".join(gettextStrings) | |
198 | print >>outputFile, self.templates.GETTEXT_DUMMY_FUNC % gettextStrings | |
199 | ||
200 | #------------------------------------------------------------------- | |
201 | ||
202 | def MakeGetTextOutput(self, inputFiles, outputFilename): | |
203 | """ | |
204 | Just output the gettext strings by themselves, with no other | |
205 | code generation. | |
206 | """ | |
207 | outputFile = self._OpenOutputFile(outputFilename) | |
208 | for inFile in inputFiles: | |
209 | resourceDocument = minidom.parse(inFile) | |
210 | resource = resourceDocument.firstChild | |
211 | strings = self.FindStringsInNode(resource) | |
212 | strings = ['_("%s");' % s for s in strings] | |
213 | print >>outputFile, "\n".join(strings) | |
214 | ||
215 | #------------------------------------------------------------------- | |
216 | ||
217 | def GenerateClasses(self, resourceDocument): | |
218 | outputList = [] | |
219 | ||
220 | resource = resourceDocument.firstChild | |
221 | topWindows = [e for e in resource.childNodes | |
222 | if e.nodeType == e.ELEMENT_NODE and e.tagName == "object"] | |
223 | ||
224 | # Generate a class for each top-window object (Frame, Panel, Dialog, etc.) | |
225 | for topWindow in topWindows: | |
226 | windowClass = topWindow.getAttribute("class") | |
227 | windowClass = re.sub("^wx", "", windowClass) | |
228 | windowName = topWindow.getAttribute("name") | |
229 | ||
230 | if windowClass == "Menu": | |
231 | outputList.append(self.templates.MENU_CLASS_HEADER % locals()) | |
232 | else: | |
233 | outputList.append(self.templates.CLASS_HEADER % locals()) | |
234 | ||
235 | # Generate a variable for each control, and standard event handlers | |
236 | # for standard controls. | |
237 | for widget in topWindow.getElementsByTagName("object"): | |
238 | widgetClass = widget.getAttribute("class") | |
239 | widgetClass = re.sub("^wx", "", widgetClass) | |
240 | widgetName = widget.getAttribute("name") | |
241 | if (widgetName != "" and widgetClass != ""): | |
242 | if widgetClass == "MenuItem": | |
243 | outputList.append(self.templates.CREATE_MENUITEM_VAR % locals()) | |
244 | elif widgetClass not in \ | |
245 | ['tool', 'unknown', 'notebookpage', 'separator', 'sizeritem']: | |
246 | outputList.append(self.templates.CREATE_WIDGET_VAR % locals()) | |
247 | outputList.append('\n\n') | |
248 | ||
249 | return "".join(outputList) | |
250 | ||
251 | #------------------------------------------------------------------- | |
252 | ||
253 | def GenerateInitResourcesEmbedded(self, resourceFilename, resourceDocument): | |
254 | outputList = [] | |
255 | files = [] | |
256 | ||
257 | resourcePath = os.path.split(resourceFilename)[0] | |
258 | memoryPath = self.GetMemoryFilename(os.path.splitext(os.path.split(resourceFilename)[1])[0]) | |
259 | resourceFilename = self.GetMemoryFilename(os.path.split(resourceFilename)[1]) | |
260 | ||
261 | self.ReplaceFilenamesInXRC(resourceDocument.firstChild, files, resourcePath) | |
262 | ||
263 | filename = resourceFilename | |
264 | fileData = resourceDocument.toxml() # what about this? encoding=resourceDocument.encoding) | |
265 | outputList.append(self.templates.FILE_AS_STRING % locals()) | |
266 | ||
267 | for f in files: | |
268 | filename = self.GetMemoryFilename(f) | |
269 | fileData = self.FileToString(os.path.join(resourcePath, f)) | |
270 | outputList.append(self.templates.FILE_AS_STRING % locals()) | |
271 | ||
272 | for f in [resourceFilename] + files: | |
273 | filename = self.GetMemoryFilename(f) | |
274 | outputList.append(self.templates.ADD_FILE_TO_MEMFS % locals()) | |
275 | ||
276 | outputList.append(self.templates.LOAD_RES_MEMFS % locals()) | |
277 | ||
278 | return "".join(outputList) | |
279 | ||
280 | #------------------------------------------------------------------- | |
281 | ||
282 | def GenerateInitResourcesFile(self, resourceFilename, resourceDocument): | |
283 | # take only the filename portion out of resourceFilename | |
284 | resourceFilename = os.path.split(resourceFilename)[1] | |
285 | outputList = [] | |
286 | outputList.append(self.templates.LOAD_RES_FILE % locals()) | |
287 | return "".join(outputList) | |
288 | ||
289 | #------------------------------------------------------------------- | |
290 | ||
291 | def GetMemoryFilename(self, filename): | |
292 | # Remove special chars from the filename | |
293 | return re.sub(r"[^A-Za-z0-9_]", "_", filename) | |
294 | ||
295 | #------------------------------------------------------------------- | |
296 | ||
297 | def FileToString(self, filename): | |
298 | outputList = [] | |
299 | ||
300 | buffer = open(filename, "rb").read() | |
301 | fileLen = len(buffer) | |
302 | ||
303 | linelng = 0 | |
304 | for i in xrange(fileLen): | |
305 | s = buffer[i] | |
306 | c = ord(s) | |
307 | if s == '\n': | |
308 | tmp = s | |
309 | linelng = 0 | |
310 | elif c < 32 or c > 127 or s == "'": | |
311 | tmp = "\\x%02x" % c | |
312 | elif s == "\\": | |
313 | tmp = "\\\\" | |
314 | else: | |
315 | tmp = s | |
316 | ||
317 | if linelng > 70: | |
318 | linelng = 0 | |
319 | outputList.append("\\\n") | |
320 | ||
321 | outputList.append(tmp) | |
322 | linelng += len(tmp) | |
323 | ||
324 | return "".join(outputList) | |
325 | ||
326 | #------------------------------------------------------------------- | |
327 | ||
328 | def NodeContainsFilename(self, node): | |
329 | """ Does 'node' contain filename information at all? """ | |
330 | ||
331 | # Any bitmaps: | |
332 | if node.nodeName == "bitmap": | |
333 | return True | |
334 | ||
335 | if node.nodeName == "icon": | |
336 | return True | |
337 | ||
338 | # URLs in wxHtmlWindow: | |
339 | if node.nodeName == "url": | |
340 | return True | |
341 | ||
342 | # wxBitmapButton: | |
343 | parent = node.parentNode | |
344 | if parent.__class__ != minidom.Document and \ | |
345 | parent.getAttribute("class") == "wxBitmapButton" and \ | |
346 | (node.nodeName == "focus" or node.nodeName == "disabled" or | |
347 | node.nodeName == "selected"): | |
348 | return True | |
349 | ||
350 | # wxBitmap or wxIcon toplevel resources: | |
351 | if node.nodeName == "object": | |
352 | klass = node.getAttribute("class") | |
353 | if klass == "wxBitmap" or klass == "wxIcon": | |
354 | return True | |
355 | ||
356 | return False | |
357 | ||
358 | #------------------------------------------------------------------- | |
359 | ||
360 | def ReplaceFilenamesInXRC(self, node, files, resourcePath): | |
361 | """ Finds all files mentioned in resource file, e.g. <bitmap>filename</bitmap> | |
362 | and replaces them with the memory filenames. | |
363 | ||
364 | Fills a list of the filenames found.""" | |
365 | ||
366 | # Is 'node' XML node element? | |
367 | if node is None: return | |
368 | if node.nodeType != minidom.Document.ELEMENT_NODE: return | |
369 | ||
370 | containsFilename = self.NodeContainsFilename(node); | |
371 | ||
372 | for n in node.childNodes: | |
373 | ||
374 | if (containsFilename and | |
375 | (n.nodeType == minidom.Document.TEXT_NODE or | |
376 | n.nodeType == minidom.Document.CDATA_SECTION_NODE)): | |
377 | ||
378 | filename = n.nodeValue | |
379 | memoryFilename = self.GetMemoryFilename(filename) | |
380 | n.nodeValue = memoryFilename | |
381 | ||
382 | if filename not in files: | |
383 | files.append(filename) | |
384 | ||
385 | # Recurse into children | |
386 | if n.nodeType == minidom.Document.ELEMENT_NODE: | |
387 | self.ReplaceFilenamesInXRC(n, files, resourcePath); | |
388 | ||
389 | #------------------------------------------------------------------- | |
390 | ||
391 | def FindStringsInNode(self, parent): | |
392 | def is_number(st): | |
393 | try: | |
394 | i = int(st) | |
395 | return True | |
396 | except ValueError: | |
397 | return False | |
398 | ||
399 | strings = [] | |
400 | if parent is None: | |
401 | return strings; | |
402 | ||
403 | for child in parent.childNodes: | |
404 | if ((parent.nodeType == parent.ELEMENT_NODE) and | |
405 | # parent is an element, i.e. has subnodes... | |
406 | (child.nodeType == child.TEXT_NODE or | |
407 | child.nodeType == child.CDATA_SECTION_NODE) and | |
408 | # ...it is textnode... | |
409 | ( | |
410 | parent.tagName == "label" or | |
411 | (parent.tagName == "value" and | |
412 | not is_number(child.nodeValue)) or | |
413 | parent.tagName == "help" or | |
414 | parent.tagName == "longhelp" or | |
415 | parent.tagName == "tooltip" or | |
416 | parent.tagName == "htmlcode" or | |
417 | parent.tagName == "title" or | |
418 | parent.tagName == "item" | |
419 | )): | |
420 | # ...and known to contain translatable string | |
421 | if (parent.getAttribute("translate") != "0"): | |
422 | strings.append(self.ConvertText(child.nodeValue)) | |
423 | ||
424 | # subnodes: | |
425 | if child.nodeType == child.ELEMENT_NODE: | |
426 | strings += self.FindStringsInNode(child) | |
427 | ||
428 | return strings | |
429 | ||
430 | #------------------------------------------------------------------- | |
431 | ||
432 | def ConvertText(self, st): | |
433 | st2 = "" | |
434 | dt = list(st) | |
435 | ||
436 | skipNext = False | |
437 | for i in range(len(dt)): | |
438 | if skipNext: | |
439 | skipNext = False | |
440 | continue | |
441 | ||
442 | if dt[i] == '_': | |
443 | if dt[i+1] == '_': | |
444 | st2 += '_' | |
445 | skipNext = True | |
446 | else: | |
447 | st2 += '&' | |
448 | elif dt[i] == '\n': | |
449 | st2 += '\\n' | |
450 | elif dt[i] == '\t': | |
451 | st2 += '\\t' | |
452 | elif dt[i] == '\r': | |
453 | st2 += '\\r' | |
454 | elif dt[i] == '\\': | |
455 | if dt[i+1] not in ['n', 't', 'r']: | |
456 | st2 += '\\\\' | |
457 | else: | |
458 | st2 += '\\' | |
459 | elif dt[i] == '"': | |
460 | st2 += '\\"' | |
461 | else: | |
462 | st2 += dt[i] | |
463 | ||
464 | return st2.encode("UTF-8") | |
465 | ||
466 | ||
467 | #------------------------------------------------------------------- | |
468 | ||
469 | def _OpenOutputFile(self, outputFilename): | |
470 | if outputFilename == "-": | |
471 | outputFile = sys.stdout | |
472 | else: | |
473 | try: | |
474 | outputFile = open(outputFilename, "wt") | |
475 | except IOError: | |
476 | raise IOError("Can't write output to '%s'" % outputFilename) | |
477 | return outputFile | |
478 | ||
479 | ||
480 | ||
481 | ||
482 | ||
483 | #--------------------------------------------------------------------------- | |
484 | ||
485 | def main(args): | |
486 | resourceFilename = "" | |
487 | outputFilename = None | |
488 | embedResources = False | |
489 | generateGetText = False | |
490 | generatePython = False | |
491 | ||
492 | try: | |
493 | opts, args = getopt.gnu_getopt(args, | |
494 | "hpgeo:", | |
495 | "help python gettext embed output=".split()) | |
496 | except getopt.GetoptError, e: | |
497 | print "\nError : %s\n" % str(e) | |
498 | print __doc__ | |
499 | sys.exit(1) | |
500 | ||
501 | # If there is no input file argument, show help and exit | |
502 | if not args: | |
503 | print __doc__ | |
504 | print "No xrc input file was specified." | |
505 | sys.exit(1) | |
506 | ||
507 | # Parse options and arguments | |
508 | for opt, val in opts: | |
509 | if opt in ["-h", "--help"]: | |
510 | print __doc__ | |
511 | sys.exit(1) | |
512 | ||
513 | if opt in ["-p", "--python"]: | |
514 | generatePython = True | |
515 | ||
516 | if opt in ["-o", "--output"]: | |
517 | outputFilename = val | |
518 | ||
519 | if opt in ["-e", "--embed"]: | |
520 | embedResources = True | |
521 | ||
522 | if opt in ["-g", "--gettext"]: | |
523 | generateGetText = True | |
524 | ||
525 | ||
526 | # check for and expand any wildcards in the list of input files | |
527 | inputFiles = [] | |
528 | for arg in args: | |
529 | inputFiles += glob.glob(arg) | |
530 | ||
531 | ||
532 | comp = XmlResourceCompiler() | |
533 | ||
534 | try: | |
535 | if generatePython: | |
536 | if not outputFilename: | |
537 | outputFilename = os.path.splitext(args[0])[0] + "_xrc.py" | |
538 | comp.MakePythonModule(inputFiles, outputFilename, | |
539 | embedResources, generateGetText) | |
540 | ||
541 | elif generateGetText: | |
542 | if not outputFilename: | |
543 | outputFilename = '-' | |
544 | comp.MakeGetTextOutput(inputFiles, outputFilename) | |
545 | ||
546 | else: | |
547 | print __doc__ | |
548 | print "One or both of -p, -g must be specified." | |
549 | sys.exit(1) | |
550 | ||
551 | ||
552 | except IOError, e: | |
553 | print >>sys.stderr, "%s." % str(e) | |
554 | else: | |
555 | if outputFilename != "-": | |
556 | print >>sys.stderr, "Resources written to %s." % outputFilename | |
557 | ||
558 | if __name__ == "__main__": | |
559 | main(sys.argv[1:]) | |
560 |