Python port of Falagard demo (console window with history)
Posted: Sat Feb 12, 2011 18:28
I have completed a Python port of the Falagard demo, a basic console window with history. The added ease of rapid development with PyCEGUI is more than worth the minor 10% performance hit! It is nearly identical to the native version, with some enhancements:
- Fixed keyboard input so it behaves as expected. (UP/DOWN arrow keys work when edit box is active; F12 always toggles the console regardless of input focus).
- KeyDown + KeyUp events are always injected in addition to Char events whenever possible.
- Added some utility functions to easily inspect PyCEGUI objects and conditionally log to console.
- Some other minor bugs fixed.
- Immediate mode Python from (PyCEGUI) console, so PyCEGUI functions can be tested interactively
- Allegro5 + D3D/OpenGL renderer
- Based on C++ source code of native samples. I tried to maintain the same variable names and comments for easy diff comparisons.
- Released as one large file for convenience, but logically broken into 4 or 5 files (one file per class + utilities).
- It is simple to start creating your own PyCEGUI apps in no time by subclassing one of the base classes!
- Currently only works with Python 2.6 due to an issue with the PyCEGUI bindings.
Code: Select all
# based on sample from http://www.cegui.org.uk/wiki/index.php/PyCEGUI
import os, sys
import new
# you must have PyCEGUI python package installed (see pypi repository)
# or you must make it work yourself using binary bindings from SDK
from PyCEGUI import *
class PyCeguiBaseApplication(object):
def __init__(self):
# TODO: figure out smart default
self.CEGUI_SAMPLE_DATAPATH = 'C:/Python26/Lib/site-packages/PyCEGUI'
self.DATAPATH_VAR_NAME = 'CEGUI_SAMPLE_DATAPATH'
def getDataPathPrefix(self):
# set data path prefix / base directory. This will
# be either from an environment variable, or based on
# the PyCEGUI library path
if self.DATAPATH_VAR_NAME in os.environ:
return os.environ[self.DATAPATH_VAR_NAME]
else:
return self.CEGUI_SAMPLE_DATAPATH
def initialiseResourceGroupDirectories(self):
# initialise the required dirs for the DefaultResourceProvider
rp = System.getSingleton().getResourceProvider()
dataPathPrefix = self.getDataPathPrefix()
# for each resource type, set a resource group directory
rp.setResourceGroupDirectory('schemes'
,dataPathPrefix + '/datafiles/schemes')
rp.setResourceGroupDirectory('imagesets'
,dataPathPrefix + '/datafiles/imagesets')
rp.setResourceGroupDirectory('fonts'
,dataPathPrefix + '/datafiles/fonts')
rp.setResourceGroupDirectory('layouts'
,dataPathPrefix + '/datafiles/layouts')
rp.setResourceGroupDirectory('looknfeels'
,dataPathPrefix + '/datafiles/looknfeel')
rp.setResourceGroupDirectory('schemas'
,dataPathPrefix + '/datafiles/xml_schemas')
rp.setResourceGroupDirectory('animations'
,dataPathPrefix + '/datafiles/animations')
def initialiseDefaultResourceGroups(self):
# set the default resource groups to be used
Imageset.setDefaultResourceGroup('imagesets')
Font.setDefaultResourceGroup('fonts')
Scheme.setDefaultResourceGroup('schemes')
WidgetLookManager.setDefaultResourceGroup('looknfeels')
WindowManager.setDefaultResourceGroup('layouts')
AnimationManager.setDefaultResourceGroup('animations');
# setup default group for validation schemas
parser = System.getSingleton().getXMLParser()
if parser.isPropertyPresent('SchemaDefaultResourceGroup'):
parser.setProperty('SchemaDefaultResourceGroup', 'schemas')
import string
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import PyCEGUIOpenGLRenderer
class PyCeguiGlutBaseApplication(PyCeguiBaseApplication):
specialKeyMap = {
GLUT_KEY_F1: Key.F1
,GLUT_KEY_F2: Key.F2
,GLUT_KEY_F3: Key.F3
,GLUT_KEY_F4: Key.F4
,GLUT_KEY_F5: Key.F5
,GLUT_KEY_F6: Key.F6
,GLUT_KEY_F7: Key.F7
,GLUT_KEY_F8: Key.F8
,GLUT_KEY_F9: Key.F9
,GLUT_KEY_F10: Key.F10
,GLUT_KEY_F11: Key.F11
,GLUT_KEY_F12: Key.F12
,GLUT_KEY_LEFT: Key.ArrowLeft
,GLUT_KEY_UP: Key.ArrowUp
,GLUT_KEY_RIGHT: Key.ArrowRight
,GLUT_KEY_DOWN: Key.ArrowDown
,GLUT_KEY_PAGE_UP: Key.PageUp
,GLUT_KEY_PAGE_DOWN: Key.PageDown
,GLUT_KEY_HOME: Key.Home
,GLUT_KEY_END: Key.End
,GLUT_KEY_INSERT: Key.Insert}
def init_static_data(self):
self.quitFlag = False
self.lastFrameTime = 0
self.glut_modifiers = dict(
shift=dict(is_held=False
,bit_flag=GLUT_ACTIVE_SHIFT
,scancode=Key.LeftShift)
,ctrl =dict(is_held=False
,bit_flag=GLUT_ACTIVE_CTRL
,scancode=Key.LeftControl)
,alt =dict(is_held=False
,bit_flag=GLUT_ACTIVE_ALT
,scancode=Key.LeftAlt))
self.fps_lastTime = 0
self.fps_frames = 0
self.fps_textbuff = ''
self.logo_geometry = None
def __init__(self, x=0, y=0, width=800, height=600
,title='PyCEGUI GLUT Base Application'):
PyCeguiBaseApplication.__init__(self)
self.init_static_data()
self.root = None
self.rot = 0
# Do GLUT init
glutInit(sys.argv)
glutInitDisplayMode(GLUT_DEPTH|GLUT_DOUBLE|GLUT_RGBA)
glutInitWindowSize(width, height)
glutInitWindowPosition(x, y)
glutCreateWindow(title)
glutSetCursor(GLUT_CURSOR_NONE)
self.renderer = PyCEGUIOpenGLRenderer.OpenGLRenderer.bootstrapSystem()
glutDisplayFunc(self.drawFrame)
glutReshapeFunc(self.reshape)
glutMotionFunc(self.mouseMotion)
glutPassiveMotionFunc(self.mouseMotion)
glutMouseFunc(self.mouseButton)
glutKeyboardFunc(self.keyChar)
glutSpecialFunc(self.keySpecial)
glutKeyboardUpFunc(self.keyCharUp)
glutSpecialUpFunc(self.keySpecialUp)
# Set the clear color
glClearColor(0.0, 0.0, 0.0, 1.0)
self.initialiseResourceGroupDirectories()
self.initialiseDefaultResourceGroups()
# setup required to do direct rendering of FPS value
scrn = Rect(Vector2(0,0), self.renderer.getDisplaySize())
self.fps_geometry = self.renderer.createGeometryBuffer()
self.fps_geometry.setClippingRegion(scrn)
# setup for logo
ImagesetManager.getSingleton().createFromImageFile(
'cegui_logo', 'logo.png', 'imagesets')
self.logo_geometry = self.renderer.createGeometryBuffer()
self.logo_geometry.setClippingRegion(scrn)
self.logo_geometry.setPivot(Vector3(50, 34.75, 0))
self.logo_geometry.setTranslation(Vector3(10, 520, 0))
ImagesetManager.getSingleton().get('cegui_logo').getImage(
'full_image').draw(self.logo_geometry ,Rect(0,0, 100, 69.5) ,None)
# clearing this queue actually makes sure it's created(!)
self.renderer.getDefaultRenderingRoot().clearGeometry(RQ_OVERLAY)
# subscribe handler to render overlay items
self.renderer.getDefaultRenderingRoot().subscribeEvent(
RenderingSurface.EventRenderQueueStarted, self, 'overlayHandler')
def __del__(self):
PyCEGUIOpenGLRenderer.OpenGLRenderer.destroySystem()
def overlayHandler(self, args):
# queueID attribute missing in CEGUI Python bindings
# if not args.queueID == RQ_OVERLAY:
# return False
# render FPS:
fnt = System.getSingleton().getDefaultFont()
if fnt:
self.fps_geometry.reset()
fnt.drawText(self.fps_geometry, self.fps_textbuff, Vector2(0, 0)
,None, colour(0xFFFFFFFF))
self.fps_geometry.draw()
self.logo_geometry.draw()
return True
def execute(self, sampleApp):
sampleApp.initialiseSample()
# set starting time
self.fps_lastTime = self.lastFrameTime = glutGet(GLUT_ELAPSED_TIME)
glutMainLoop()
return True
def drawFrame(self):
guiSystem = System.getSingleton()
# do time based updates
thisTime = glutGet(GLUT_ELAPSED_TIME)
elapsed = thisTime - self.lastFrameTime
self.lastFrameTime = thisTime
# inject the time pulse
guiSystem.injectTimePulse(elapsed / 1000.0)
# update fps fields
self.doFPSUpdate()
# update logo rotation
self.logo_geometry.setRotation(Vector3(self.rot, 0, 0))
self.rot += (180.0 * elapsed) / 1000.0
if self.rot > 360.0:
self.rot -= 360.0
# do rendering for this frame.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
guiSystem.renderGUI()
glutPostRedisplay()
glutSwapBuffers()
# here we check the 'quitting' state and cleanup as required.
# this is probably not the best way to do this, but since we're
# using glut, and glutMainLoop can never return, we need some
# way of checking when to exit. And this is it...
if self.quitFlag:
PyCEGUIOpenGLRenderer.OpenGLRenderer.destroySystem()
exit(0)
def reshape(self, w, h):
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(60.0, float(w)/h, 1.0, 50.0)
glMatrixMode(GL_MODELVIEW)
System.getSingleton().notifyDisplaySizeChanged(Size(w, h))
def mouseMotion(self, x, y):
System.getSingleton().injectMousePosition(x, y)
def mouseButton(self, button, state, x, y):
if button == GLUT_LEFT_BUTTON:
if state == GLUT_UP:
System.getSingleton().injectMouseButtonUp(LeftButton)
else:
System.getSingleton().injectMouseButtonDown(LeftButton)
elif button == GLUT_RIGHT_BUTTON:
if state == GLUT_UP:
System.getSingleton().injectMouseButtonUp(RightButton)
else:
System.getSingleton().injectMouseButtonDown(RightButton)
elif button == GLUT_MIDDLE_BUTTON:
if state == GLUT_UP:
System.getSingleton().injectMouseButtonUp(MiddleButton)
else:
System.getSingleton().injectMouseButtonDown(MiddleButton)
def keyChar(self, key, x, y):
key = key.encode('ascii', 'ignore')
self.handleModifierKeys()
scancode = self.ascii_to_scancode(key)
if scancode:
custom_args = KeyEventArgs(None)
custom_args.scancode = scancode
logi('BEFORE keyChar.fireEvent: %s' %
pretty_attr(custom_args ,'custom_args'))
System.getSingleton().fireEvent('AppKeyDown', custom_args)
logc('AFTER keyChar.fireEvent: %s \n' %
pretty_attr(custom_args ,'custom_args'))
log('INJECT KEYDN: %s %s' % (scancode, self.modifier_keys_state())
,'inject.key.down')
result = System.getSingleton().injectKeyDown(int(scancode))
log('RESULT injectKeyDown: %s' % result, 'result')
log('INJECT CHAR: [%3d]' % ord(key), 'inject.char')
result = System.getSingleton().injectChar(ord(key))
log('RESULT injectChar: %s' % result, 'result')
if scancode == Key.Escape:
self.quitFlag = True
return False
def keyCharUp(self, key, x, y):
key = key.encode('ascii', 'ignore')
self.handleModifierKeys()
scancode = self.ascii_to_scancode(key)
if scancode:
log('INJECT KEYUP: %s %s' % (scancode, self.modifier_keys_state())
,'inject.key.up')
System.getSingleton().injectKeyUp(int(scancode))
return False
def keySpecial(self, key, x, y):
self.handleModifierKeys()
scancode = self.special_to_scancode(key)
if scancode:
custom_args = KeyEventArgs(None)
custom_args.scancode = scancode
logi('BEFORE keySpecial.fireevent: %s' %
pretty_attr(custom_args ,'custom_args'))
GlobalEventSet.getSingleton().fireEvent(
'AppKeyDown', custom_args)
logc('AFTER keySpecial.fireevent: %s \n' %
pretty_attr(custom_args ,'custom_args'))
log('INJECT KEYDN: %s %s' % (scancode, self.modifier_keys_state())
,'inject.key.down')
result = System.getSingleton().injectKeyDown(int(scancode))
log('RESULT injectKeyDown: %s' % result, 'result')
return False
def keySpecialUp(self, key, x, y):
self.handleModifierKeys()
scancode = self.special_to_scancode(key)
if scancode:
log('INJECT KEYUP: %s %s' % (scancode, self.modifier_keys_state())
,'inject.key.up')
System.getSingleton().injectKeyUp(int(scancode))
return False
def handleModifierKeys(self):
status = glutGetModifiers()
for name, key in self.glut_modifiers.items():
if (status & key['bit_flag']):
if not key['is_held']:
key['is_held'] = True
log('INJECT MODDN: %s' % key['scancode'], 'inject.key.down')
System.getSingleton().injectKeyDown(key['scancode'])
else:
if key['is_held']:
key['is_held'] = False
log('INJECT MODUP: %s' % key['scancode'], 'inject.key.up')
System.getSingleton().injectKeyUp(key['scancode'])
def doFPSUpdate(self):
# another frame
self.fps_frames += 1
# has at least a second passed since we last updated the text?
if (self.lastFrameTime - self.fps_lastTime >= 1000):
# update FPS text to output
self.fps_textbuff = 'FPS: %d' % self.fps_frames
self.fps_frames = 0
self.fps_lastTime = self.lastFrameTime
mapping = dict([(ord(c), getattr(Key, c.upper()))
for c in string.ascii_letters])
mapping.update({
96: Key.Grave, 126: Key.Grave,
49: Key.One, 33: Key.One,
50: Key.Two, 64: Key.At,
51: Key.Three, 35: Key.Three,
52: Key.Four, 36: Key.Four,
53: Key.Five, 37: Key.Five,
54: Key.Six, 94: Key.Six,
55: Key.Seven, 38: Key.Seven,
56: Key.Eight, 42: Key.Multiply,
57: Key.Nine, 40: Key.Nine,
48: Key.Zero, 41: Key.Zero,
45: Key.Minus, 95: Key.Underline,
61: Key.Equals, 43: Key.Equals,
91: Key.LeftBracket, 123: Key.LeftBracket,
93: Key.RightBracket, 125: Key.RightBracket,
59: Key.Semicolon, 58: Key.Colon,
39: Key.Apostrophe, 34: Key.Apostrophe,
92: Key.Backslash, 124: Key.Backslash,
44: Key.Comma, 60: Key.Comma,
46: Key.Period, 62: Key.Period,
47: Key.Slash, 63: Key.Slash,
13: Key.Return,
8: Key.Backspace,
9: Key.Tab,
32: Key.Space,
127: Key.Delete,
27: Key.Escape})
def ascii_to_scancode(self, a):
a = ord(a)
return self.mapping[a] if (a in self.mapping) else 0
def special_to_scancode(self, c):
return self.specialKeyMap[c] if (c in self.specialKeyMap) else 0
def modifier_keys_state(self):
s = ''
if self.glut_modifiers['ctrl']['is_held']:
s += 'CTRL '
if self.glut_modifiers['alt']['is_held']:
s += 'ALT '
if self.glut_modifiers['shift']['is_held']:
s += 'SHIFT '
return s
class PyCeguiSample(object):
def run(self):
if self.initialise:
self.initialise()
self.cleanup()
return 0
def initialise(self):
self.sampleApp = PyCeguiGlutBaseApplication(x=0, y=0
, width=800, height=600 ,title='PyCEGUI Falagard Demo')
# execute the base application (which sets
# up the demo via 'self' and runs it.
if self.sampleApp.execute(self):
# signal that app initialised and ran
return True
self.sampleApp = None
# signal app did not initialise and run.
return False
def cleanup(self):
if self.sampleApp:
self.sampleApp = None
class FalagardDemo1Sample(PyCeguiSample):
def __init__(self):
self.console = None
def initialiseSample(self):
# Get window manager which we wil use for a few jobs here.
winMgr = WindowManager.getSingleton()
# Load the scheme to initialse the VanillaSkin which we use
# in this sample
SchemeManager.getSingleton().create('VanillaSkin.scheme')
# set default mouse image
System.getSingleton().setDefaultMouseCursor('Vanilla-Images'
,'MouseArrow')
# load an image to use as a background
ImagesetManager.getSingleton().createFromImageFile('BackgroundImage'
,'GPN-2000-001437.tga')
# here we will use a StaticImage as the root,
# then we can use it to place a background image
background = winMgr.createWindow('Vanilla/StaticImage')
# set area rectangle
background.setArea(URect(cegui_reldim(0) ,cegui_reldim(0)
,cegui_reldim(1) ,cegui_reldim(1)))
# disable frame and standard background
background.setProperty('FrameEnabled', 'false')
background.setProperty('BackgroundEnabled', 'false')
#set the background image
background.setProperty('Image', 'set:BackgroundImage image:full_image')
# install this as the root GUI sheet
System.getSingleton().setGUISheet(background)
FontManager.getSingleton().create('DejaVuSans-10.font')
# load some demo windows and attach to the background 'root'
background.addChildWindow(
winMgr.loadWindowLayout('VanillaWindows.layout'))
# create an instance of the console class.
self.console = DemoConsole('Demo')
# listen for key presses on the root window.
GlobalEventSet.getSingleton().subscribeEvent('/AppKeyDown'
,self, 'handleRootKeydown')
# activate the background window
background.activate()
# success!
return True
def cleanupSample(self):
self.console = None
def handleRootKeydown(self, keyArgs):
if keyArgs.scancode == Key.F12:
self.console.toggleVisibility()
logi('BEFORE handleRootKeydown: %s' %
pretty_attr(keyArgs, 'WindowEventArgs'))
keyArgs.handled += 1
logc('AFTER handleRootKeydown: %s' %
pretty_attr(keyArgs, 'WindowEventArgs'))
return True
return False
class DemoConsole(object):
def __init__(self, name, parent=None):
# these must match the IDs assigned in the layout
self.submitButtonID = 1;
self.entryBoxID = 2;
self.historyID = 3;
window_manager = WindowManager.getSingleton()
self.console_root = window_manager.loadWindowLayout(
'VanillaConsole.layout', name)
self.history_pos = 0
self.history = []
# we will destroy the console box windows ourselves
self.console_root.setDestroyedByParent(False)
# Do events wire-up
GlobalEventSet.getSingleton().subscribeEvent('/AppKeyDown',
self, 'handleKeydown')
self.console_root.getChild(self.submitButtonID).subscribeEvent(
PushButton.EventClicked, self, 'handleSubmit')
self.console_root.getChild(self.entryBoxID).subscribeEvent(
Editbox.EventTextAccepted, self, 'handleSubmit')
# decide where to attach the console main window
parent = parent or System.getSingleton().getGUISheet()
# attach this window if parent is valid
if parent:
parent.addChildWindow(self.console_root)
def __del__(self):
# destroy the windows that we loaded earlier
WindowManager.getSingleton().destroyWindow(self.console_root)
def toggleVisibility(self):
self.console_root.hide() if self.console_root.isVisible(True
) else self.console_root.show()
def handleSubmit(self, WindowEventArgs):
# get the text entry editbox
editbox = self.console_root.getChild(self.entryBoxID)
# get text out of the editbox
edit_text = editbox.getText()
# if the string is not empty
if edit_text:
# add this entry to the command history buffer
self.history.append(edit_text)
# reset history position
self.history_pos = len(self.history)
# append newline to this entry
edit_text += '\n'
# get history window
history = self.console_root.getChild(self.historyID)
# append new text to history output
history.setText(history.getText() + edit_text)
# scroll to bottom of history output
history.setCaratIndex(sys.maxint)
# erase text in text entry box.
editbox.setText('')
# re-activate the text entry box
editbox.activate()
return True
def handleKeydown(self, keyEventArgs):
# if no input focus, ignore events
if not self.console_root.getActiveChild():
return False
# get the text entry editbox
editbox = self.console_root.getChild(self.entryBoxID)
if keyEventArgs.scancode == Key.ArrowUp:
self.history_pos = max(self.history_pos - 1, -1)
if self.history_pos >= 0:
editbox.setText(self.history[self.history_pos])
editbox.setCaratIndex(sys.maxint)
else:
editbox.setText('')
editbox.activate()
elif keyEventArgs.scancode == Key.ArrowDown:
self.history_pos = min(self.history_pos + 1, len(self.history))
if self.history_pos < len(self.history):
editbox.setText(self.history[self.history_pos])
editbox.setCaratIndex(sys.maxint)
else:
editbox.setText('')
editbox.activate()
else:
return False
logi('BEFORE handleKeyDown: %s' %
pretty_attr(keyEventArgs, 'keyEventArgs'))
keyEventArgs.handled += 100
logc('AFTER handleKeyDown: %s' %
pretty_attr(keyEventArgs, 'keyEventArgs'))
return True
# macro to aid in the creation of UDims (copied from CEGUIUDim.h)
def cegui_reldim(x):
return UDim(x, 0)
def pretty_attr(py_object, label, interesting_classes='default'):
if interesting_classes == 'default':
interesting_classes = pretty_attr.default_classes
def _pretty_attr(py_object, label, interesting_classes, indent='.'):
if isinstance(py_object, pretty_attr.instancemethodType):
return label + ' [instancemethod]'
elif interesting_classes and isinstance(py_object, interesting_classes):
result = ''
for attribute_name in dir(py_object):
if not attribute_name.startswith('__'):
attribute = getattr(py_object, attribute_name)
result += indent + _pretty_attr(attribute
,attribute_name + ' = '
,interesting_classes
,indent + '.') + '\n'
return label + str(py_object) + '\n' + result
# TODO: add support for lists/tuples/dicts
else:
# just print simple string for uninteresting objects
return label + str(py_object)
return _pretty_attr(py_object, label + ' = ', interesting_classes)[:-1]
pretty_attr.instancemethodType = type(
new.instancemethod(pretty_attr, None, object))
pretty_attr.default_classes = (
KeyEventArgs
,WindowEventArgs)
import fnmatch;
def block_indent(ss):
return ss.replace('\n', '\n' + log.indent)
def log(message, message_class='default', indent=None):
if indent is None:
indent = log.indent_level * ' '
for wildcard_pattern in log.classes:
if fnmatch.fnmatch(message_class, wildcard_pattern):
print block_indent(indent + message)
def logi(message, message_class='default'):
log(message, message_class, log.indent_level * ' ')
log.indent_level += 2
log.indent = ' ' * log.indent_level
def logc(message, message_class='default'):
log.indent_level = log.indent_level - 2
log.indent = ' ' * log.indent_level
log(message, message_class, log.indent_level * ' ')
log.indent_level = 0
log.indent = ''
log.classes = (
'default'
#,'inject*'
#,'result'
,)
app = FalagardDemo1Sample()
app.run()
del app