Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pysdl2 example for issue #324 #379

Merged
merged 8 commits into from
Jan 8, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Authors
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Greg Farrell <gregfarrell.org@@gmail.com>
Finn Hughes <finn.hughes1@@gmail.com>
Marcelo Fernandez <fernandezm@@gmail.com>
Simon Hatt <9hatt2@@gmail.com>
Neil Munday <www.mundayweb.com>
391 changes: 391 additions & 0 deletions examples/pysdl2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,391 @@
"""
Simple SDL2 / cefpython3 example.

Only handles mouse events but could be extended to handle others.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated. It should say that keyboard and mouse support is incomplete:

  • Keyboard support may be extended to support:
    • marking text in inputs with the shift key
    • selecting all text with ctrl+a
    • not all special keys may be supported, but can be extended by modifying the getKeyCode() function
  • Mouse support may be extended to support:
    • mouse hover by calling SendMouseMoveEvent()
    • mouse wheel scrolling by calling SendMouseWheelEvent()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add mouse move etc. events and update the comments.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is no more valid:

Only handles mouse events but could be extended to handle others.


Requires pysdl2 (and SDL2 library).

Tested configurations:
- SDL2 2.0.5 with PySDL2 0.9.3 on Fedora 25 (x86_64)
- SDL2 with PySDL2 0.9.5 on Ubuntu 14.04

Install instructions.

1. Install SDL libraries for your OS, e.g:

Fedora:

sudo dnf install SDL2 SDL2_ttf SDL2_image SDL2_gfx SDL2_mixer

Ubuntu:

sudo apt-get install libsdl2-dev

2. Install PySDL via PIP:

sudo pip2 install PySDL2

Event handling:

Where possible SDL2 events are mapped to CEF ones. Not all keyboard
modifiers are handled in this example but these could be
added by the reader (if desired). Modifiers that do not work
for example:

- Ctrl
- Mouse dragging
- Marking text inputs with the shift key

Due to SDL2's lack of GUI widgets there are no GUI controls
for the user. However, as an exercise this example could
be extended by create some simple SDL2 widgets. An example of
widgets made using PySDL2 can be found as part of the Pi
Entertainment System at:
https://github.com/neilmunday/pes/blob/master/lib/pes/ui.py
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for such comments, that is useful.

"""

import os
import sys
try:
from cefpython3 import cefpython as cef
except ImportError:
print("cefpython3 module not found - please install")
sys.exit(1)
try:
import sdl2
import sdl2.ext
except ImportError:
print("SDL2 module not found - please install")
sys.exit(1)
try:
from PIL import Image
except ImportError:
print("PIL module not found - please install")
sys.exit(1)

def main():
# The following variables control the dimensions of the window
# and browser display area
width = 1024
height = 768
# headerHeight is useful for leaving space for controls
# at the top of the window (future implementation?)
headerHeight = 0
browserHeight = height - headerHeight
browserWidth = width
# Mouse wheel fudge to enhance scrolling
scrollEnhance = 20
# Initialise CEF for offscreen rendering
WindowUtils = cef.WindowUtils()
sys.excepthook = cef.ExceptHook
cef.Initialize(settings={"windowless_rendering_enabled": True})
window_info = cef.WindowInfo()
window_info.SetAsOffscreen(0)
# Initialise SDL2 for video (add other init constants if you
# require other SDL2 functionality e.g. mixer,
# TTF, joystick etc.
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
# Create the window
window = sdl2.video.SDL_CreateWindow(
'cefpython3 SDL2 Demo',
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
width,
height,
0
)
# Define default background colour (black in this case)
backgroundColour = sdl2.SDL_Color(0, 0, 0)
# Create the renderer using hardware acceleration
renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.render.SDL_RENDERER_ACCELERATED)
# Set-up the RenderHandler, passing in the SDL2 renderer
renderHandler = RenderHandler(renderer, width, height - headerHeight)
# Create the browser instance
browser = cef.CreateBrowserSync(window_info, url="https://www.google.com/")
browser.SetClientHandler(LoadHandler())
browser.SetClientHandler(renderHandler)
# Must call WasResized at least once to let know CEF that
# viewport size is available and that OnPaint may be called.
browser.SendFocusEvent(True)
browser.WasResized()
# Begin the main rendering loop
shiftDown = False
running = True
while running:
# Convert SDL2 events into CEF events (where appropriate)
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT or (event.type == sdl2.SDL_KEYDOWN and event.key.keysym.sym == sdl2.SDLK_ESCAPE):
running = False
break
if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
False,
1
)
elif event.type == sdl2.SDL_MOUSEBUTTONUP:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
True,
1
)
elif event.type == sdl2.SDL_MOUSEMOTION:
if event.motion.y > headerHeight:
# Mouse move triggered in browser region
browser.SendMouseMoveEvent(event.motion.x, event.motion.y - headerHeight, False)
elif event.type == sdl2.SDL_MOUSEWHEEL:
# Mouse wheel event
x = event.wheel.x
if x < 0:
x -= scrollEnhance
else:
x += scrollEnhance
y = event.wheel.y
if y < 0:
y -= scrollEnhance
else:
y += scrollEnhance
browser.SendMouseWheelEvent(0, 0, x, y)
elif event.type == sdl2.SDL_TEXTINPUT:
# Handle text events to get actual characters typed rather than the key pressed
keycode = ord(event.text.text)
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYDOWN:
# Handle key down events for non-text keys
if event.key.keysym.sym == sdl2.SDLK_RETURN:
keycode = event.key.keysym.sym
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.key.keysym.sym in [
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = getKeyCode(event.key.keysym.sym)
if keycode != None:
key_event = {
"type": cef.KEYEVENT_RAWKEYDOWN,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYUP:
# Handle key up events for non-text keys
if event.key.keysym.sym in [
sdl2.SDLK_RETURN,
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = getKeyCode(event.key.keysym.sym)
if keycode != None:
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
# Clear the renderer
sdl2.SDL_SetRenderDrawColor(
renderer,
backgroundColour.r,
backgroundColour.g,
backgroundColour.b,
255
)
sdl2.SDL_RenderClear(renderer)
# Tell CEF to update which will trigger the OnPaint
# method of the RenderHandler instance
cef.MessageLoopWork()
# Update display
sdl2.SDL_RenderCopy(
renderer,
renderHandler.texture,
None,
sdl2.SDL_Rect(0, headerHeight, browserWidth, browserHeight)
)
sdl2.SDL_RenderPresent(renderer)
# User exited
exit_app()

def getKeyCode(key):
"""Helper function to convert SDL2 key codes to cef ones"""
keyMap = {
sdl2.SDLK_RETURN: 13,
sdl2.SDLK_DELETE: 46,
sdl2.SDLK_BACKSPACE: 8,
sdl2.SDLK_LEFT: 37,
sdl2.SDLK_RIGHT: 39,
sdl2.SDLK_UP: 38,
sdl2.SDLK_DOWN: 40,
sdl2.SDLK_HOME: 36,
sdl2.SDLK_END: 35,
}
if key in keyMap:
return keyMap[key]
# Key not mapped, raise exception
print("Keyboard mapping incomplete: \
unsupported SDL key %d. \
See https://wiki.libsdl.org/SDLKeycodeLookup for mapping."
% key)
return None

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blank lines required according to Pep8. See the Contributing-code.md page for details on code style requirements.

class LoadHandler(object):
"""Simple handler for loading URLs."""

def OnLoadingStateChange(self, browser, is_loading, **_):
if not is_loading:
print("loading complete")

def OnLoadError(self, browser, frame, error_code, failed_url, **_):
if not frame.IsMain():
return
print("Failed to load %s" % failed_url)

class RenderHandler(object):
"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding comments like this.

Handler for rendering web pages to the
screen via SDL2.

The object's texture property is exposed
to allow the main rendering loop to access
the SDL2 texture.
"""

def __init__(self, renderer, width, height):
self.__width = width
self.__height = height
self.__renderer = renderer
self.texture = None

def GetViewRect(self, rect_out, **_):
rect_out.extend([0, 0, self.__width, self.__height])
return True

def OnPaint(self, browser, element_type, paint_buffer, **_):
"""
Using the pixel data from CEF's offscreen rendering
the data is converted by PIL into a SDL2 surface
which can then be rendered as a SDL2 texture.
"""
if element_type == cef.PET_VIEW:
image = Image.frombuffer(
'RGBA',
(self.__width, self.__height),
paint_buffer.GetString(mode="rgba", origin="top-left"),
'raw',
'BGRA'
)
# Following PIL to SDL2 surface code from pysdl2 source.
mode = image.mode
rmask = gmask = bmask = amask = 0
if mode == "RGB":
# 3x8-bit, 24bpp
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x0000FF
gmask = 0x00FF00
bmask = 0xFF0000
else:
rmask = 0xFF0000
gmask = 0x00FF00
bmask = 0x0000FF
depth = 24
pitch = self.__width * 3
elif mode in ("RGBA", "RGBX"):
# RGBX: 4x8-bit, no alpha
# RGBA: 4x8-bit, alpha
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x00000000
gmask = 0x0000FF00
bmask = 0x00FF0000
if mode == "RGBA":
amask = 0xFF000000
else:
rmask = 0xFF000000
gmask = 0x00FF0000
bmask = 0x0000FF00
if mode == "RGBA":
amask = 0x000000FF
depth = 32
pitch = self.__width * 4
else:
print("Unsupported mode: %s" % mode)
exit_app()

pxbuf = image.tobytes()
# Create surface
surface = sdl2.SDL_CreateRGBSurfaceFrom(
pxbuf,
self.__width,
self.__height,
depth,
pitch,
rmask,
gmask,
bmask,
amask
)
if self.texture:
# free memory used by previous texture
sdl2.SDL_DestroyTexture(self.texture)
# Create texture
self.texture = sdl2.SDL_CreateTextureFromSurface(self.__renderer, surface)
# Free the surface
sdl2.SDL_FreeSurface(surface)
else:
print("Unsupport element_type in OnPaint")

def exit_app():
"""Tidy up SDL2 and CEF before exiting."""
sdl2.SDL_Quit()
cef.Shutdown()
print("exited")

if __name__ == "__main__":
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put here only a call to main(), and define main at the top of the file just below the imports. That's how it is done in all other examples, so it should be consistent in this example as well.

main()