HTML and Flash in Panda with Awesomium

ok im having problems porting this to mac i just dont know enough if somebody would be willing to walk me through it i will do all the work… i really want to learn i just need a little guidance from somebody who knows OS X and C++

Vardis - I used the test.html document that is distributed with awesomium. Stuck the relevant files into the ‘bin’ directory and have the awesomium.py and ‘media’ folder in my sample programs. I get the correct display except with no flash animation.

Tried creating my own HTML file with my own SWF but again I can display text etc but no SWF. The flash animation loads fine in my IE browser. Anyone else had similar problems? Is there any other way to get flash animations into Panda?

Are you running on windows or some other platform?
Some things you can try:
-Look in PANDA_HOME/python for a file named awesomium.log. As you might guessed it contains the log output from awesomium, so check in there for any errors.
-Download and try running the official demo from here:
http://princeofcode.com/download.php?target=Awesomium_v1.0_Demo.zip.
If this doesn’t work then awesomium probably can’t render on your machine.

Hi everybody,

I noticed this thread only yesterday and I’m amazed by the potential of creating GUIs mostly using web programming skills and adding a browser to a game.

Vardis, I’m not familiar with the procedures to create python bindings for C++ code. Can you elaborate about what’s the problem you are facing concerning the injection of keyboard events?

In the meantime I’ve tried to come up with a way overcome the lack of keyboard inputs. The code, based on Vardis example, is at the end of the message. It works but has various shortcomings:

  1. it relies on the messenger hack I described in this thread to catch all events
  2. I couldn’t find a neat way to catch all key-related events but nothing else. I also wonder if it would still work with non-english/non-western keyboards.
  3. It requires specific javascript. Unless there’s a way to inject javascript code in a loaded html page this limits its use to pages that are compatible with this hack. So no way to fill input fields across the net.
  4. It uses a trick, changing the input’s background color, to determine if the incoming key should or should not affect it. Not neat.
  5. I couldn’t quite figure out how to use javascript’s “this” in highlight() and delight()
  6. It doesn’t handle character repeat, i.e. holding the backspace to delete the whole string.

Here’s the code. It relies on the binaries Vardis provided at the beginning of this thread.

Save this in a file “inputTest.html”:

<html>
	<head>
	
	<script type="text/javascript">
		function updateInputField()
		{
			inputField = document.getElementById('anInput')
			
			// this is needed to prevent all key events
			// from altering the content of the input field
			if(inputField.style.backgroundColor != "yellow")
				return
			
			if (Client.depressedKey != "backspace" && Client.depressedKey != "space")
				inputField.value += Client.depressedKey
				
			else if (Client.depressedKey == "space")
				inputField.value += " "				

			else
				inputField.value = inputField.value.substring(0, inputField.value.length-1)
				
		}	
		
		function highlight()
		{
			inputField = document.getElementById('anInput')
			inputField.style.backgroundColor = "yellow"
		}
		
		function delight()
		{
			inputField = document.getElementById('anInput')
			inputField.style.backgroundColor = "white"
		}

	</script>
	
	</head>
	<body>
		<form name="aForm">
			<input id="anInput" style="border:5px dotted red" type="text" value="Write something here!"
				onfocus="highlight()" onblur="delight()"/>
		</form>
	</body>
</html>

and then save this in a file “inputTest.py”:

import array, sys

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import Texture
from pandac.PandaModules import CardMaker
from pandac.PandaModules import NodePath
from pandac.PandaModules import Point3,Vec3,Vec4
from pandac.PandaModules import PNMImage
from pandac.PandaModules import TextureStage
from pandac.PandaModules import WindowProperties
from direct.interval.IntervalGlobal import *

from pawesomium import *

WIDTH = 800
HEIGHT = 512

WIN_WIDTH = 800
WIN_HEIGHT = 600
	
class HtmlView(DirectObject, WebViewListener):
	def __init__(self):		
		
		WebViewListener.__init__(self)
		
		# turn off normal mouse controls
		base.disableMouse()
	
		# black background suits better in our case
		base.win.setClearColor(Vec4(0.0, 0.0, 0.0, 1.0))
	
		self.mx, self.my = 0, 0
		self.transparency = True
		
		self.webCore = WebCore(LogLevel.LOG_VERBOSE, True, PixelFormat.PF_BGRA)
		self.webCore.setBaseDirectory('./media')
		self.webCore.setCustomResponsePage(404, "404response.html");			
		
		self.webView = self.webCore.createWebView(WIDTH, HEIGHT, self.transparency, False, 70)		
		self.webView.setListener(self)
		
		self.htmlFile = "inputTest.html"
		self.webView.loadFile(self.htmlFile, '')		
		
		self.imgBuffer = array.array('B')
		for i in xrange(WIDTH*HEIGHT):
			self.imgBuffer.append(0)
			self.imgBuffer.append(0)
			self.imgBuffer.append(0)
			self.imgBuffer.append(255)
			
		self.setupTexture()
			
		self.accept("escape", sys.exit, [0])
		
		self.accept("mouse1", self.mouseDown, [MouseButton.LEFT_MOUSE_BTN])
		self.accept("mouse3", self.mouseDown, [MouseButton.RIGHT_MOUSE_BTN])
		self.accept("mouse1-up", self.mouseUp, [MouseButton.LEFT_MOUSE_BTN])
		self.accept("mouse3-up", self.mouseUp, [MouseButton.RIGHT_MOUSE_BTN])
		
		self.accept("f1", self.toggleTransparency)
		self.accept("f2", self.reload)
		self.accept("f3", self.zoomIn)
		self.accept("f4", self.zoomOut)		
		
		taskMgr.add(self.update, 'UpdateTask')
		
	def setupTexture(self):
		cm = CardMaker('quadMaker')
		cm.setColor(1.0, 1.0, 1.0, 1.0)
					
		aspect = base.camLens.getAspectRatio()
		htmlWidth = 2.0*aspect * WIDTH / float(WIN_WIDTH)		
		htmlHeight = 2.0*float(HEIGHT) / float(WIN_HEIGHT) 
		
		# the html area will be center aligned and vertically top aligned
		cm.setFrame(-htmlWidth/2.0, htmlWidth/2.0, 1.0 - htmlHeight, 1.0)
		card = cm.generate()
		self.quad = NodePath(card)
		self.quad.reparentTo(aspect2d)	
		
		self.guiTex = Texture("guiTex")
		self.guiTex.setupTexture(Texture.TT2dTexture, WIDTH, HEIGHT, 1, Texture.TUnsignedByte, Texture.FRgba)		
		self.guiTex.setKeepRamImage(True)		
		self.guiTex.makeRamImage()				
		self.guiTex.setWrapU(Texture.WMRepeat)
		self.guiTex.setWrapV(Texture.WMRepeat)
		
		ts = TextureStage('webTS')
		self.quad.setTexture(ts, self.guiTex)
		self.quad.setTexScale(ts, 1.0, -1.0)
		
		self.quad.setTransparency(1)
		self.quad.setTwoSided(True)
		self.quad.setColor(1.0, 1.0, 1.0, 1.0)
				
	def mouseDown(self, button):
		self.webView.injectMouseDown(button)
		
	def mouseUp(self, button):
		self.webView.injectMouseUp(button)
	
	def reload(self):
		self.webView.loadFile(self.htmlFile, '')
		
	def zoomIn(self):
		self.webView.zoomIn()
		
	def zoomOut(self):
		self.webView.zoomOut()
		
	def toggleTransparency(self):
		self.transparency = not self.transparency
		self.webView.setTransparent(self.transparency)
		
	def update(self, task):		
		if base.mouseWatcherNode.hasMouse():
			x, y = self._translateRelativeCoordinates(base.mouseWatcherNode.getMouseX(), base.mouseWatcherNode.getMouseY())					
			self.webView.injectMouseMove(x, y)			
			
			if (self.mx - x) != 0 and (self.my - y) != 0:
				self.mx, self.my = x, y				

		if self.webView.isDirty():
			self.webView.render(self.imgBuffer.buffer_info()[0], WIDTH*4, 4, 0)
			textureBuffer = self.guiTex.modifyRamImage()
			textureBuffer.setData(self.imgBuffer.tostring())		
			
		self.webCore.update()
		return Task.cont				
	
	def _translateRelativeCoordinates(self, x, y):
		sx = int((x + 1.0) * 0.5 * base.win.getXSize())
		sy = int((-1.0*y + 1.0) * 0.5 * base.win.getYSize())
		return sx, sy
		
	# --------------------[ WebViewListener implementation ]--------------------------
	def onCallback(self, name, args):
		pass
			
	def onBeginNavigation(self, url,  frameName):
		pass
		
	def onBeginLoading(self, url, frameName, statusCode, mimeType):
		pass
		
	def onFinishLoading(self):
		pass
		
	def onReceiveTitle(self, title, frameName):
		pass
		
	def onChangeTooltip(self, tooltip):
		pass
		
	def onChangeCursor(self, cursor):
		pass
		
	def onChangeKeyboardFocus(self, isFocused):
		pass
		
	def onChangeTargetURL(self, url):
		pass
		
	def handleKeyEvents(self, event, arguments):
		self.webView.setProperty( "depressedKey", JSValue(event))
		self.webView.executeJavascript("updateInputField()", "")
	
htmlView = HtmlView()

def catchAllEvents(event, arguments):

	if event.startswith("shift-"):
		event = event[6:].upper()

	if len(event) == 1 or event == "backspace" or event == "space":
		htmlView.handleKeyEvents(event, arguments)
	
messenger.catchAllMethod = catchAllEvents

run()

run simply typing “python inputTest.py

Manu

Note that, for the record, you can accept all button and keystroke events, without having to hack Messenger.py. You do this by telling Panda to throw a single event for button and keystroke events, instead of (or in addition to) its default mode of throwing a different event for each button.

base.buttonThrowers[0].node().setButtonDownEvent('button')
base.buttonThrowers[0].node().setButtonRepeatEvent('button')
base.buttonThrowers[0].node().setKeystrokeEvent('keystroke')
self.accept('button', self.buttonHandler)
self.accept('keystroke', self.keystrokeHandler)

You want to listen for ‘button’ events for things like the tab key, the backspace key, and the arrow keys; but you want the ‘keystroke’ events for actual typing. The difference is that the ‘button’ event refers to a particular button on the keyboard, and includes key-repeat; whereas the ‘keystroke’ event refers to the more abstract concept of what the user’s typing. A ‘button’ will send ‘shift-a’ or ‘shift-2’ but a ‘keystroke’ will send ‘A’ and ‘@’. Also, keystrokes respect os-specific multi-key sequences necessary for typing certain international characters.

David

David: that is niiiiice! Thank you for pointing it out. I used it in the code that follows.

I’ve tried to push things a little to see what I could fix and what I could not using only python/panda+javascript. Specifically I’ve tried to implement as much as possible of the functionality of a TEXTAREA element as it is a little more of a general case compared to an INPUT element. The code follows but this is a summary of what I found:

  1. as David pointed out the messenger hack is no longer needed: yieppiee!!
  2. JSValue’s constructor doesn’t like unicode strings. That means that when setting a property from python for use by javascript I must first apply str(), which doesn’t bode well for international characters, does it?
  3. text insertion now works as you would expect except for some strange beginning-of-the-line issues with newline characters
  4. text selection works as you would expect
  5. arrow keys work well to move between adjacent characters. Mostly well moving up and down lines.
  6. extending the text selection via shift-arrow keys works only forward. Try it out and you’ll see what I mean.
  7. I tried to implement copy, cut & paste. It shouldn’t be too difficult but currently I cannot find a way to pass data back from javascript. The problem likes in JSArguments, the object returned by onCallback(). I don’t know how to get to the data inside it. Maybe the bindings are not complete?

The good news about this is that most of these problems are only due to the impossibility to simply inject keyboard events directly. If the bindings get expanded to include injectTextEvent most of these issues would disappear.

Some issues are probably deep in awesomium/webkit and might not have an immediate solution.

  1. If I type enough text to require the side scroller bar an unsightly white square that shouldn’t be there appears underneath the bar.
  2. document.getSelection() is allegedly supported by webkit but doesn’t seem to be supported by the particular build used by awesomium. This makes things much more complicated for cut&paste purposes unless the bindings are expanded to include WebView’s cur(), paste() and copy() methods.

So, here’s the code:

<!-- File: textareaTest.html -->
<html>
	<head>
		<script type="text/javascript">
			function updateTextarea()
			{
				inputField = document.activeElement
				if (inputField.nodeName != "TEXTAREA")
					return
					
				initialCursorPosition = inputField.selectionStart
				textEnd = inputField.innerHTML.substring(inputField.selectionEnd)
				
				characterToInsert = Client.depressedKey	// Client.depressedKey might 
													    // be needed elsewhere too
				if(Client.depressedKey != "backspace")
				{
					textStart = inputField.innerHTML.substring(0, inputField.selectionStart)
					newCursorPosition = initialCursorPosition + 1
				}
				else
				{
					characterToInsert = ""
					if(inputField.selectionEnd - inputField.selectionStart > 0)
					{
						textStart = inputField.innerHTML.substring(0, inputField.selectionStart)
						newCursorPosition = initialCursorPosition
					}
					else
					{
						textStart = inputField.innerHTML.substring(0, inputField.selectionStart-1)
						newCursorPosition = initialCursorPosition - 1
					}
				}
				
				inputField.innerHTML = textStart + characterToInsert + textEnd
				inputField.selectionStart = inputField.selectionEnd = newCursorPosition
			}	
			
			function moveCursor()
			{
				inputField = document.activeElement
				if (inputField.nodeName != "TEXTAREA")
					return
			
				currentCursorPosition = inputField.selectionStart
				newCursorPosition = currentCursorPosition + Client.cursorShift
				inputField.selectionEnd = inputField.selectionStart = newCursorPosition
			}
			
			function moveCursorOneLine()
			{
				inputField = document.activeElement
				if (inputField.nodeName != "TEXTAREA")
					return

				currentCursorPosition = inputField.selectionStart
				newCursorPosition = currentCursorPosition + (Client.cursorShift * (inputField.cols - 1))
				inputField.selectionEnd = inputField.selectionStart = newCursorPosition
			}
			
			function moveSelectionEnd()
			{
				inputField = document.activeElement
				if (inputField.nodeName != "TEXTAREA")
					return
			
				// Most text editors handle modifications to the selection
				// via shift-arrow keys better than this. It's a start.
				currentEndPosition = inputField.selectionEnd
				newEndPosition = currentEndPosition + Client.selectionEndShift
				inputField.selectionEnd = newEndPosition
			}
			
			function moveSelectionEndOneLine()
			{
				inputField = document.activeElement
				if (inputField.nodeName != "TEXTAREA")
					return
			
				// Most text editors handle modifications to the selection
				// via shift-arrow keys better than this. It's a start.
				currentEndPosition = inputField.selectionEnd
				newEndPosition = currentEndPosition + (Client.selectionEndShift * (inputField.cols - 1))
				inputField.selectionEnd = newEndPosition
			}
			
		</script>
	</head>
	<body>
		<textarea id="anInputField" style="border:5px dashed blue;"  cols="60" rows="10"
				  onfocus="highlight(this)" onblur="delight(this)">A textarea field with lots and lots of space...</textarea>
	</body>
</html>
## file: textareaTest.py
import array, sys, string

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import Texture
from pandac.PandaModules import CardMaker
from pandac.PandaModules import NodePath
from pandac.PandaModules import Point3,Vec3,Vec4
from pandac.PandaModules import PNMImage
from pandac.PandaModules import TextureStage
from pandac.PandaModules import WindowProperties
from direct.interval.IntervalGlobal import *

from pawesomium import *

WIDTH = 800
HEIGHT = 600

WIN_WIDTH = 800
WIN_HEIGHT = 600
	
class HtmlView(DirectObject, WebViewListener):
	def __init__(self):		
		
		WebViewListener.__init__(self)
		
		# turn off normal mouse controls
		base.disableMouse()
	
		# black background suits better in our case
		base.win.setClearColor(Vec4(0.0, 0.0, 0.0, 1.0))
	
		self.mx, self.my = 0, 0
		self.transparency = True
		
		self.webCore = WebCore(LogLevel.LOG_VERBOSE, True, PixelFormat.PF_BGRA)
		self.webCore.setBaseDirectory('.')
		#self.webCore.setCustomResponsePage(404, "404response.html");			
		
		self.webView = self.webCore.createWebView(WIDTH, HEIGHT, self.transparency, False, 70)		
		self.webView.setListener(self)
		
		self.htmlFile = "textareaTest.html"
		self.webView.loadFile(self.htmlFile, '')		
		
		self.imgBuffer = array.array('B')
		for i in xrange(WIDTH*HEIGHT):
			self.imgBuffer.append(0)
			self.imgBuffer.append(0)
			self.imgBuffer.append(0)
			self.imgBuffer.append(255)
			
		textClipboard = ""
		self.setupEventsHandling()
		self.setupTexture()
	
	def setupEventsHandling(self):
	
		self.webView.setCallback("setTextClipboard");
	
		self.accept("escape", sys.exit, [0])
		
		self.accept("mouse1", self.mouseDown, [MouseButton.LEFT_MOUSE_BTN])
		self.accept("mouse3", self.mouseDown, [MouseButton.RIGHT_MOUSE_BTN])
		self.accept("mouse1-up", self.mouseUp, [MouseButton.LEFT_MOUSE_BTN])
		self.accept("mouse3-up", self.mouseUp, [MouseButton.RIGHT_MOUSE_BTN])
		
		self.accept("f1", self.toggleTransparency)
		self.accept("f2", self.reload)
		self.accept("f3", self.zoomIn)
		self.accept("f4", self.zoomOut)	

		base.buttonThrowers[0].node().setButtonDownEvent('buttonDown')
		base.buttonThrowers[0].node().setButtonRepeatEvent('keyrepeat')
		base.buttonThrowers[0].node().setKeystrokeEvent('keystroke')
		
		self.accept('buttonDown', self.buttonHandler)
		self.accept('keyrepeat', self.buttonHandler)
		self.accept('keystroke', self.keystrokeHandler)		
		
		taskMgr.add(self.update, 'UpdateTask')
		
	def setupTexture(self):
		cm = CardMaker('quadMaker')
		cm.setColor(1.0, 1.0, 1.0, 1.0)
					
		aspect = base.camLens.getAspectRatio()
		htmlWidth = 2.0*aspect * WIDTH / float(WIN_WIDTH)		
		htmlHeight = 2.0*float(HEIGHT) / float(WIN_HEIGHT) 
		
		# the html area will be center aligned and vertically top aligned
		cm.setFrame(-htmlWidth/2.0, htmlWidth/2.0, 1.0 - htmlHeight, 1.0)
		card = cm.generate()
		self.quad = NodePath(card)
		self.quad.reparentTo(aspect2d)	
		
		self.guiTex = Texture("guiTex")
		self.guiTex.setupTexture(Texture.TT2dTexture, WIDTH, HEIGHT, 1, Texture.TUnsignedByte, Texture.FRgba)		
		self.guiTex.setKeepRamImage(True)		
		self.guiTex.makeRamImage()				
		self.guiTex.setWrapU(Texture.WMRepeat)
		self.guiTex.setWrapV(Texture.WMRepeat)
		
		ts = TextureStage('webTS')
		self.quad.setTexture(ts, self.guiTex)
		self.quad.setTexScale(ts, 1.0, -1.0)
		
		self.quad.setTransparency(1)
		self.quad.setTwoSided(True)
		self.quad.setColor(1.0, 1.0, 1.0, 1.0)
				
	def mouseDown(self, button):
		self.webView.injectMouseDown(button)
		
	def mouseUp(self, button):
		self.webView.injectMouseUp(button)
	
	def reload(self):
		self.webView.loadFile(self.htmlFile, '')
		
	def zoomIn(self):
		self.webView.zoomIn()
		
	def zoomOut(self):
		self.webView.zoomOut()
		
	def toggleTransparency(self):
		self.transparency = not self.transparency
		self.webView.setTransparent(self.transparency)
		
	def update(self, task):		
		if base.mouseWatcherNode.hasMouse():
			x, y = self._translateRelativeCoordinates(base.mouseWatcherNode.getMouseX(), base.mouseWatcherNode.getMouseY())					
			self.webView.injectMouseMove(x, y)			
			
			if (self.mx - x) != 0 and (self.my - y) != 0:
				self.mx, self.my = x, y				

		if self.webView.isDirty():
			self.webView.render(self.imgBuffer.buffer_info()[0], WIDTH*4, 4, 0)
			textureBuffer = self.guiTex.modifyRamImage()
			textureBuffer.setData(self.imgBuffer.tostring())		
			
		self.webCore.update()
		return Task.cont				
	
	def _translateRelativeCoordinates(self, x, y):
		sx = int((x + 1.0) * 0.5 * base.win.getXSize())
		sy = int((-1.0*y + 1.0) * 0.5 * base.win.getYSize())
		return sx, sy
		
	# --------------------[ WebViewListener implementation ]--------------------------		
	def onBeginNavigation(self, url,  frameName):
		pass
		
	def onBeginLoading(self, url, frameName, statusCode, mimeType):
		pass
		
	def onFinishLoading(self):
		pass
		
	def onReceiveTitle(self, title, frameName):
		pass
		
	def onChangeTooltip(self, tooltip):
		pass
		
	def onChangeCursor(self, cursor):
		pass
		
	def onChangeKeyboardFocus(self, isFocused):
		pass
		
	def onChangeTargetURL(self, url):
		pass
	
	def onCallback(self, name, args):
		pass
		
	def buttonHandler(self, event):

		if event == "backspace":
			self.webView.setProperty("depressedKey", JSValue("backspace"))
			self.webView.executeJavascript("updateTextarea()", "")
			
		elif event.startswith("arrow_"):
			if event == "arrow_right":
				self.webView.setProperty("cursorShift", JSValue(1))
				self.webView.executeJavascript("moveCursor()", "")
				
			elif event == "arrow_left":
				self.webView.setProperty("cursorShift", JSValue(-1))
				self.webView.executeJavascript("moveCursor()", "")
				
			elif event == "arrow_up":
				self.webView.setProperty("cursorShift", JSValue(-1))
				self.webView.executeJavascript("moveCursorOneLine()", "")
				
			elif event == "arrow_down":
				self.webView.setProperty("cursorShift", JSValue(1))
				self.webView.executeJavascript("moveCursorOneLine()", "")

		elif event.startswith("shift-arrow_"):
			if event == "shift-arrow_right":
				self.webView.setProperty("selectionEndShift", JSValue(1))
				self.webView.executeJavascript("moveSelectionEnd()", "")
	
			elif event == "shift-arrow_left":
				self.webView.setProperty("selectionEndShift", JSValue(-1))
				self.webView.executeJavascript("moveSelectionEnd()", "")
				
			elif event == "shift-arrow_up":
				self.webView.setProperty("selectionEndShift", JSValue(-1))
				self.webView.executeJavascript("moveSelectionEndOneLine()", "")
				
			elif event == "shift-arrow_down":
				self.webView.setProperty("selectionEndShift", JSValue(1))
				self.webView.executeJavascript("moveSelectionEndOneLine()", "")

			
	legalCharacters = string.letters + string.digits + string.punctuation + string.whitespace
	def keystrokeHandler(self, event):
	
		if event in self.legalCharacters:
			self.webView.setProperty( "depressedKey", JSValue(str(event)))
			self.webView.executeJavascript("updateTextarea()", "")
	
htmlView = HtmlView()

run()

I’m not sure but maybe using the stuff from second-life could be useful:
wiki.secondlife.com/wiki/LLQtWebKit which is GPL
wiki.secondlife.com/wiki/LLMozLib2 which is older but MPL
Ok - propably not…