source: branches/active/character_customization/tools/pychan_designer/pychan_designer.py @ 736

Revision 736, 13.5 KB checked in by aspidites, 8 years ago (diff)

Patch by Aspidites:

  • renamed scripts package to parpg
  • renamed parpg module to application
  • removed packaging and other related files (kept locally for reference, will reintroduce similar scripts to resolve bug #275
  • updated all import statements to respect changes above
Line 
1from __future__ import print_function
2import sys
3import os
4import logging
5try:
6    from cStringIO import StringIO
7except ImportError:
8    from StringIO import StringIO
9
10from fife import fife
11from fife.extensions.basicapplication import ApplicationBase, Setting
12from fife.extensions import pychan
13
14from parpg.common.optionparser import OptionParser, OptionError
15from parpg.common.utils import dedent_chomp
16from parpg import gui
17
18USAGE_MESSAGE = '''\
19usage: pychan_designer.py [-h] xml_script_path
20Load a pychan xml script and display the gui element it contains.
21
22    -h                show this help message
23    -v                increase the verbosity of console output; may be
24                        specified multiple times
25    -q                decrease the verbosity of console output; may be
26                        specified multiple times
27'''
28
29def is_child(widget, parent):
30    """
31    Recursively search a widget hierarchy to determine if the
32    widget is a decendent of parent.
33    """
34   
35    if widget is None or parent is None:
36        result = False
37    elif hasattr(parent, 'children'):
38        if widget in parent.children:
39            result = True
40        else:
41            result = False
42            for child in parent.children:
43                if is_child(widget, child):
44                    result = True
45                    break
46    else:
47        result = False
48   
49    return result
50
51
52class LabelLogHandler(logging.Handler):
53    def __init__(self, text_box, level=logging.NOTSET):
54        assert hasattr(text_box, 'text') and hasattr(text_box, 'adaptLayout')
55        logging.Handler.__init__(self, level=level)
56        self.text_box = text_box
57   
58    def emit(self, record):
59        message= self.format(record)
60        self.text_box.text = unicode(message, 'utf8')
61        self.text_box.adaptLayout()
62
63class TextBoxLogHandler(LabelLogHandler):
64    def emit(self, record):
65        message= self.format(record)
66        self.text_box.text = '\n'.join([self.text_box.text, message])
67        self.text_box.adaptLayout()
68
69
70class GuichanDesignerApplication(ApplicationBase):
71    def __init__(self, settings_file='settings-dist.xml'):
72        setting = Setting(settings_file=settings_file)
73        super(GuichanDesignerApplication, self).__init__(setting=setting)
74        pychan.loadFonts('fonts/freefont.fontdef')
75        pychan.setupModalExecution(self.mainLoop, self.breakFromMainLoop)
76        # pychan default settings need an overwrite, because we don't like some aspects (like opaque widgets)
77        screen_width, screen_height = \
78            [int(dimension) for dimension in
79             setting.get('FIFE', 'ScreenResolution').split('x')]
80        self.xml_script_path = ''
81        self.active_widget = None
82        self.selected_widget = None
83        self.widget_stack = []
84        self.logger = logging.getLogger('PychanDesignerApplication')
85        self.xml_editor = pychan.loadXML('gui/xml_editor.xml')
86        self.console = pychan.loadXML('gui/console.xml')
87        with file('gui/pychan_designer.xml') as xml_file:
88            self.gui = pychan.loadXML(xml_file)
89        self.gui.min_size = (screen_width, screen_height)
90        self.gui.max_size = (screen_width, screen_height)
91        editor = self.gui.findChild(name='editor')
92        editor.content = self.xml_editor
93        self.gui.mapEvents(
94            {
95                'exitButton': self.quit,
96                'reloadButton': self.reloadXml,
97                'applyButton': self.applyXml,
98                'saveButton': self.saveXml,
99                'xmlEditorTab': self.showXmlEditor,
100                'consoleTab': self.showConsole,
101            }
102        )
103        self.gui.adaptLayout()
104        self.gui.show()
105
106    def showXmlEditor(self):
107        editor = self.gui.findChild(name='editor')
108        editor.content = self.xml_editor
109
110    def showConsole(self):
111        editor = self.gui.findChild(name='editor')
112        editor.content = self.console
113
114    def setupLogging(self, level):
115        console = self.console
116        self.logger.setLevel(level)
117        console_handler = TextBoxLogHandler(console, level)
118        console_formatter = \
119            logging.Formatter('%(levelname)s: %(message)s')
120        console_handler.setFormatter(console_formatter)
121        self.logger.addHandler(console_handler)
122        status_bar = self.gui.findChild(name='statusBar')
123        status_bar_handler = LabelLogHandler(status_bar, logging.ERROR)
124        self.logger.addHandler(status_bar_handler)
125
126    def _applyActivateEventCapture(self, widget):
127        widget.capture(self._activateCallback, 'mouseEntered')
128        widget.capture(self._deactivateCallback, 'mouseExited')
129
130    def selectWidget(self):
131        widget = self.widget_stack[0]
132        self.selected_widget = widget
133        property_viewer = self.gui.findChild(name='propertyViewer')
134        columns = property_viewer.content
135        name_rows = columns.findChild(name='propertyNameColumnRows')
136        value_rows = columns.findChild(name='propertyValueColumnRows')
137        name_rows.removeAllChildren()
138        value_rows.removeAllChildren()
139        assert len(name_rows.children) == 0 and \
140               len(value_rows.children) == 0, \
141               'propertyViewer was not properly cleared!'
142        for attribute in sorted(widget.ATTRIBUTES,
143                                cmp=lambda a, b: cmp(a.name, b.name)):
144            name = attribute.name
145            name_label = pychan.Label(text=unicode(name, 'utf8'))
146            name_label.font = 'FreeMono'
147            name_label.background_color = (250, 250, 250)
148            name_container = pychan.HBox()
149            name_container.border_size = 1
150            name_container.base_color = (250, 250, 250)
151            name_container.addChild(name_label)
152            alternate_name = '_'.join(['old', name])
153            if hasattr(widget, alternate_name):
154                value = getattr(widget, alternate_name)
155            else:
156                value = getattr(widget, name)
157            if isinstance(value, fife.Color):
158                value = (value.r, value.g, value.b, value.a)
159            elif isinstance(value, fife.GuiFont):
160                value = value.name
161            elif isinstance(value, fife.GuiImage):
162                # FIXME Technomage 2011-01-27: Unfortunately I haven't found a
163                #     way to display the image path only, so for now it's being
164                #     skipped.
165                continue
166            value_label = pychan.TextField(text=unicode(repr(value), 'utf8'))
167            value_label.font = 'FreeMono'
168            value_label.background_color = (220, 220, 220)
169            value_label.min_size = (0, 20)
170            value_container = pychan.HBox()
171            value_container.border_size = 1
172            value_container.min_size = (0, 24)
173            value_container.base_color = (250, 250, 250)
174            value_container.addChild(value_label)
175            name_container.min_size = value_container.min_size
176           
177            value_label.capture(
178                self._createChangeAttributeCallback(name, widget,
179                                                    type(attribute)),
180                'keyPressed'
181            )
182            name_rows.addChild(name_container)
183            value_rows.addChild(value_container)
184        columns.adaptLayout()
185       
186        if self.selected_widget is not None:
187            self.unhighlightWidget(self.selected_widget)
188        self.highlightWidget(widget)
189
190    def _createChangeAttributeCallback(self, attribute_name, wrapped_widget,
191                                       attribute_type):
192        def _changeAttributeCallback(widget, event):
193            if (event.getKey().getValue() == pychan.events.guichan.Key.ENTER):
194                try:
195                    value = eval(widget.text)
196                    setattr(wrapped_widget, attribute_name, value)
197                except (ValueError, TypeError, NameError) as exception:
198                    self.logger.error(exception)
199                    widget.text = unicode(getattr(wrapped_widget,
200                                                  attribute_name))
201        return _changeAttributeCallback
202
203    def _activateCallback(self, widget):
204        self.logger.debug(
205            'mouse entered {0}(name={1!r})'.format(
206                type(widget).__name__,
207                widget.name
208            )
209        )
210        if len(self.widget_stack) == 0:
211            self.widget_stack.append(widget)
212            self.activateWidget(widget)
213        elif is_child(self.widget_stack[0], widget):
214            self.widget_stack.append(widget)
215        else:
216            parent = self.widget_stack[0]
217            self.widget_stack.insert(0, widget)
218            self.deactivateWidget(parent)
219            self.activateWidget(widget)
220
221    def _deactivateCallback(self, widget):
222        self.logger.debug(
223            'mouse exited {0}(name={1!r})'.format(
224                type(widget).__name__,
225                widget.name
226            )
227        )
228        widget_stack = self.widget_stack
229        index = widget_stack.index(widget)
230        self.deactivateWidget(widget)
231        if index == 0 and len(widget_stack) > 1:
232            parent = widget_stack[1]
233            self.activateWidget(parent)
234        widget_stack.remove(widget)
235
236    def activateWidget(self, widget):
237        self.highlightWidget(widget)
238        self.logger.debug(
239            'activated {0}(name={1!r})'.format(
240                type(widget).__name__,
241                widget.name
242            )
243        )
244
245    def deactivateWidget(self, widget):
246        self.unhighlightWidget(widget)
247        self.logger.debug(
248            'deactivated {0}(name={1!r})'.format(
249                type(widget).__name__,
250                widget.name
251            )
252        )
253
254    def highlightWidget(self, widget):
255        if not hasattr(widget, 'highlighted') or not widget.highlighted:
256            widget.highlighted = True
257            widget.old_base_color = widget.base_color
258            widget.base_color = (255, 0, 0)
259            widget.old_background_color = widget.background_color
260            widget.background_color = (255, 0, 0)
261            widget.old_border_size = widget.border_size
262            widget.border_size = 1
263            if hasattr(widget, 'opaque'):
264                widget.old_opaque = widget.opaque
265                widget.opaque = 1
266            widget.adaptLayout()
267   
268    def unhighlightWidget(self, widget):
269        if hasattr(widget, 'highlighted') and widget.highlighted:
270            widget.highlighted = False
271            widget.base_color = widget.old_base_color
272            widget.background_color = widget.old_background_color
273            widget.border_size = widget.old_border_size
274            if hasattr(widget, 'opaque'):
275                widget.opaque = widget.old_opaque
276            widget.adaptLayout()
277
278    def saveXml(self):
279        with file(self.xml_script_path, 'w') as xml_file:
280            xml_content = self.xml_editor.text
281            xml_file.write(xml_content)
282        self.logger.info('saved file {0}'.format(self.xml_script_path))
283
284    def applyXml(self):
285        xml_content = self.xml_editor.text
286        xml_stream = StringIO(str(xml_content))
287        xml_stream.seek(0)
288        self.loadXml(xml_stream)
289
290    def reloadXml(self):
291        with file(self.xml_script_path, 'r') as xml_file:
292            self.loadXml(xml_file)
293
294    def loadXml(self, xml_file):
295        self.logger.debug(
296            'loading file {0}'.format(getattr(xml_file, 'name', ''))
297        )
298        top_widget = pychan.loadXML(xml_file)
299        top_widget.deepApply(self._applyActivateEventCapture)
300        top_widget.deepApply(lambda widget: widget.capture(self.selectWidget,
301                                                           'mousePressed'))
302        widget_preview = self.gui.findChild(name='widgetPreview')
303        widget_preview.content = top_widget
304        top_widget.adaptLayout()
305        # FIXME Technomage 2011-01-23: Containers are not displayed with their
306        #     background images when attached to another widget. A workaround
307        #     is to call beforeShow after attaching the container.
308        if isinstance(top_widget, pychan.Container):
309            top_widget.beforeShow()
310        xml_editor = self.xml_editor
311        xml_file.seek(0)
312        xml_editor.text = unicode(xml_file.read(), 'utf8')
313        xml_editor.resizeToContent()
314        self.logger.info(
315            'successfully loaded file {0}'.format(
316                getattr(xml_file, 'name', '')
317            )
318        )
319
320
321def main(argv=sys.argv):
322    option_parser = OptionParser(
323        usage=USAGE_MESSAGE,
324        args=argv[1:]
325    )
326    logging_level = logging.WARNING
327    for option in option_parser:
328        if option == '-h' or option =='--help':
329            print(option_parser.usage)
330            sys.exit(0)
331        elif option == '-v':
332            logging_level -= 10
333        elif option == '-q':
334            logging_level += 10
335        else:
336            print('Error: unknown option {0!r}\n'.format(option),
337                  file=sys.stderr)
338            print(option_parser.usage, file=sys.stderr)
339            sys.exit(1)
340    try:
341        xml_script_path = os.path.abspath(option_parser.get_next_prog_arg())
342    except OptionError as exception:
343        print('Error: {0}\n'.format(exception), file=sys.stderr)
344        print(option_parser.usage, file=sys.stderr)
345        sys.exit(1)
346    application = GuichanDesignerApplication()
347    application.setupLogging(logging_level)
348    parpg_root = os.path.abspath(os.path.join('..', '..', 'game'))
349    os.chdir(parpg_root)
350    application.xml_script_path = xml_script_path
351    with file(xml_script_path) as xml_file:
352        application.loadXml(xml_file)
353    application.run()
354
355
356if __name__ == '__main__':
357    main()
Note: See TracBrowser for help on using the repository browser.