Module concur.integrations.puppet

Automation back-end with screen capture and programmatical user input.

Normally, this is used with functions in concur.testing.

Expand source code
"""Automation back-end with screen capture and programmatical user input.

Normally, this is used with functions in `concur.testing`.
"""

from __future__ import absolute_import

import glfw
import imgui
import time
import OpenGL.GL as gl

# Save as video
from PIL import Image
import numpy as np

from concur.integrations.glfw import create_window, create_window_dock, begin_maximized_window
from concur.integrations.opengl import create_offscreen_fb, get_fb_data
from imgui.integrations import compute_fb_scale
from imgui.integrations.opengl import ProgrammablePipelineRenderer


class PuppetRenderer(ProgrammablePipelineRenderer):
    """Renderer for automated testing. User inputs are set programmatically, rather than interactively."""
    def __init__(self, window):
        super(PuppetRenderer, self).__init__()
        self.window = window

        self.io.display_size = glfw.get_framebuffer_size(self.window)

        self._map_keys()
        self._gui_time = None

        self._click = [False] * 3
        self._mouse_buttons = [False] * 3
        self._mouse_pos = 100, 100
        self._mouse_wheel = 0.0

    def _map_keys(self):
        key_map = self.io.key_map

        key_map[imgui.KEY_TAB] = glfw.KEY_TAB
        key_map[imgui.KEY_LEFT_ARROW] = glfw.KEY_LEFT
        key_map[imgui.KEY_RIGHT_ARROW] = glfw.KEY_RIGHT
        key_map[imgui.KEY_UP_ARROW] = glfw.KEY_UP
        key_map[imgui.KEY_DOWN_ARROW] = glfw.KEY_DOWN
        key_map[imgui.KEY_PAGE_UP] = glfw.KEY_PAGE_UP
        key_map[imgui.KEY_PAGE_DOWN] = glfw.KEY_PAGE_DOWN
        key_map[imgui.KEY_HOME] = glfw.KEY_HOME
        key_map[imgui.KEY_END] = glfw.KEY_END
        key_map[imgui.KEY_DELETE] = glfw.KEY_DELETE
        key_map[imgui.KEY_BACKSPACE] = glfw.KEY_BACKSPACE
        key_map[imgui.KEY_ENTER] = glfw.KEY_ENTER
        key_map[imgui.KEY_ESCAPE] = glfw.KEY_ESCAPE
        key_map[imgui.KEY_A] = glfw.KEY_A
        key_map[imgui.KEY_C] = glfw.KEY_C
        key_map[imgui.KEY_V] = glfw.KEY_V
        key_map[imgui.KEY_X] = glfw.KEY_X
        key_map[imgui.KEY_Y] = glfw.KEY_Y
        key_map[imgui.KEY_Z] = glfw.KEY_Z

    def process_inputs(self):
        """Process the virtual user inputs. Called by `main` at the beginning of each frame."""
        io = imgui.get_io()

        window_size = glfw.get_window_size(self.window)
        fb_size = glfw.get_framebuffer_size(self.window)

        io.display_size = window_size
        io.display_fb_scale = compute_fb_scale(window_size, fb_size)
        io.delta_time = 1.0/60

        current_time = glfw.get_time()

        if self._gui_time:
            self.io.delta_time = current_time - self._gui_time
        else:
            self.io.delta_time = 1. / 60.

        self._gui_time = current_time

        for i, b in enumerate(self._click):
            if b:
                io.mouse_down[i] = True
                self._click[i] = False
            else:
                io.mouse_down[i] = False
        for i, b in enumerate(self._mouse_buttons):
            if b:
                io.mouse_down[i] = True

        io.mouse_pos = self._mouse_pos
        io.mouse_wheel = self._mouse_wheel
        self._mouse_wheel = 0.0

    def click(self, button=0):
        """Simulate a mouse button click.

        * 0 .. left button
        * 1 .. right button
        * 2 .. middle button
        """
        self._click[button] = True

    def set_mouse_pos(self, x, y):
        'Set the mouse cursor position to a specified value.'
        self._mouse_pos = x, y

    def scroll_up(self):
        'Scroll the mouse wheel up one click.'
        self._mouse_wheel = 1

    def scroll_dn(self):
        'Scroll the mouse wheel down one click.'
        self._mouse_wheel = -1

    def mouse_dn(self, button=0):
        """Push a specified mouse button."""
        self._mouse_buttons[button] = True

    def mouse_up(self, button=0):
        """Release a specified mouse button."""
        self._mouse_buttons[button] = False

    def key_dn(self, key):
        imgui.get_io().keys_down[key] = True
        exit(0)

    def key_up(self, key):
        imgui.get_io().keys_down[key] = False

    def write_char(self, c):
        """Write a character with a given char code."""
        assert 0 < c < 0x10000
        imgui.get_io().add_input_character(c)


def main(widget_gen, name="Concur Puppet", width=640, height=480, save_screencast=None, return_sshot=False, headless=False, fps=60):
    """ Create a GLFW window, spin up the main loop, and display a given widget inside.

    The resulting window is not hooked up to the user input. Instead, input is handled
    by a PuppetRenderer instance.

    `widget_gen` takes as an argument a `PuppetRenderer` instance, and returns a widget.
    `fps` optionally limits FPS (if None, FPS is unlimited)
    """
    imgui.create_context()

    # Set config flags
    imgui.get_io().config_flags |= imgui.CONFIG_DOCKING_ENABLE | imgui.CONFIG_VIEWPORTS_ENABLE

    window = create_window(name, width, height, visible=not headless)
    impl = PuppetRenderer(window)
    widget = widget_gen(impl)
    offscreen_fb = create_offscreen_fb(width, height)

    if save_screencast:
        import imageio
        writer = imageio.get_writer(save_screencast, mode='I', fps=60)

    try:
        while not glfw.window_should_close(window):
            t0 = time.perf_counter()
            glfw.poll_events()
            impl.process_inputs()

            imgui.new_frame()

            create_window_dock(window)
            begin_maximized_window("Default##Concur", window)

            try:
                next(widget)
            except StopIteration:
                break
            finally:
                imgui.end()

                gl.glClearColor(0.5, 0.5, 0.5, 1)
                gl.glClear(gl.GL_COLOR_BUFFER_BIT)
                imgui.render()

                if save_screencast:
                    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, offscreen_fb)
                    impl.render(imgui.get_draw_data())
                    image = get_fb_data(offscreen_fb, width, height)
                    writer.append_data(image)
                gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)

                impl.render(imgui.get_draw_data())
                glfw.swap_buffers(window)

            t1 = time.perf_counter()
            if fps is not None and t1 - t0 < 1/fps:
                time.sleep(1/fps - (t1 - t0))

        if return_sshot:
            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, offscreen_fb)
            impl.render(imgui.get_draw_data())
            image = get_fb_data(offscreen_fb, width, height)
            ret = image
        else:
            ret = None

    finally:
        impl.shutdown()
        imgui.destroy_context(imgui.get_current_context())
        glfw.terminate()
        if save_screencast:
            writer.close()

    return ret

Functions

def main(widget_gen, name='Concur Puppet', width=640, height=480, save_screencast=None, return_sshot=False, headless=False, fps=60)

Create a GLFW window, spin up the main loop, and display a given widget inside.

The resulting window is not hooked up to the user input. Instead, input is handled by a PuppetRenderer instance.

widget_gen takes as an argument a PuppetRenderer instance, and returns a widget. fps optionally limits FPS (if None, FPS is unlimited)

Expand source code
def main(widget_gen, name="Concur Puppet", width=640, height=480, save_screencast=None, return_sshot=False, headless=False, fps=60):
    """ Create a GLFW window, spin up the main loop, and display a given widget inside.

    The resulting window is not hooked up to the user input. Instead, input is handled
    by a PuppetRenderer instance.

    `widget_gen` takes as an argument a `PuppetRenderer` instance, and returns a widget.
    `fps` optionally limits FPS (if None, FPS is unlimited)
    """
    imgui.create_context()

    # Set config flags
    imgui.get_io().config_flags |= imgui.CONFIG_DOCKING_ENABLE | imgui.CONFIG_VIEWPORTS_ENABLE

    window = create_window(name, width, height, visible=not headless)
    impl = PuppetRenderer(window)
    widget = widget_gen(impl)
    offscreen_fb = create_offscreen_fb(width, height)

    if save_screencast:
        import imageio
        writer = imageio.get_writer(save_screencast, mode='I', fps=60)

    try:
        while not glfw.window_should_close(window):
            t0 = time.perf_counter()
            glfw.poll_events()
            impl.process_inputs()

            imgui.new_frame()

            create_window_dock(window)
            begin_maximized_window("Default##Concur", window)

            try:
                next(widget)
            except StopIteration:
                break
            finally:
                imgui.end()

                gl.glClearColor(0.5, 0.5, 0.5, 1)
                gl.glClear(gl.GL_COLOR_BUFFER_BIT)
                imgui.render()

                if save_screencast:
                    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, offscreen_fb)
                    impl.render(imgui.get_draw_data())
                    image = get_fb_data(offscreen_fb, width, height)
                    writer.append_data(image)
                gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)

                impl.render(imgui.get_draw_data())
                glfw.swap_buffers(window)

            t1 = time.perf_counter()
            if fps is not None and t1 - t0 < 1/fps:
                time.sleep(1/fps - (t1 - t0))

        if return_sshot:
            gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, offscreen_fb)
            impl.render(imgui.get_draw_data())
            image = get_fb_data(offscreen_fb, width, height)
            ret = image
        else:
            ret = None

    finally:
        impl.shutdown()
        imgui.destroy_context(imgui.get_current_context())
        glfw.terminate()
        if save_screencast:
            writer.close()

    return ret

Classes

class PuppetRenderer (window)

Renderer for automated testing. User inputs are set programmatically, rather than interactively.

Expand source code
class PuppetRenderer(ProgrammablePipelineRenderer):
    """Renderer for automated testing. User inputs are set programmatically, rather than interactively."""
    def __init__(self, window):
        super(PuppetRenderer, self).__init__()
        self.window = window

        self.io.display_size = glfw.get_framebuffer_size(self.window)

        self._map_keys()
        self._gui_time = None

        self._click = [False] * 3
        self._mouse_buttons = [False] * 3
        self._mouse_pos = 100, 100
        self._mouse_wheel = 0.0

    def _map_keys(self):
        key_map = self.io.key_map

        key_map[imgui.KEY_TAB] = glfw.KEY_TAB
        key_map[imgui.KEY_LEFT_ARROW] = glfw.KEY_LEFT
        key_map[imgui.KEY_RIGHT_ARROW] = glfw.KEY_RIGHT
        key_map[imgui.KEY_UP_ARROW] = glfw.KEY_UP
        key_map[imgui.KEY_DOWN_ARROW] = glfw.KEY_DOWN
        key_map[imgui.KEY_PAGE_UP] = glfw.KEY_PAGE_UP
        key_map[imgui.KEY_PAGE_DOWN] = glfw.KEY_PAGE_DOWN
        key_map[imgui.KEY_HOME] = glfw.KEY_HOME
        key_map[imgui.KEY_END] = glfw.KEY_END
        key_map[imgui.KEY_DELETE] = glfw.KEY_DELETE
        key_map[imgui.KEY_BACKSPACE] = glfw.KEY_BACKSPACE
        key_map[imgui.KEY_ENTER] = glfw.KEY_ENTER
        key_map[imgui.KEY_ESCAPE] = glfw.KEY_ESCAPE
        key_map[imgui.KEY_A] = glfw.KEY_A
        key_map[imgui.KEY_C] = glfw.KEY_C
        key_map[imgui.KEY_V] = glfw.KEY_V
        key_map[imgui.KEY_X] = glfw.KEY_X
        key_map[imgui.KEY_Y] = glfw.KEY_Y
        key_map[imgui.KEY_Z] = glfw.KEY_Z

    def process_inputs(self):
        """Process the virtual user inputs. Called by `main` at the beginning of each frame."""
        io = imgui.get_io()

        window_size = glfw.get_window_size(self.window)
        fb_size = glfw.get_framebuffer_size(self.window)

        io.display_size = window_size
        io.display_fb_scale = compute_fb_scale(window_size, fb_size)
        io.delta_time = 1.0/60

        current_time = glfw.get_time()

        if self._gui_time:
            self.io.delta_time = current_time - self._gui_time
        else:
            self.io.delta_time = 1. / 60.

        self._gui_time = current_time

        for i, b in enumerate(self._click):
            if b:
                io.mouse_down[i] = True
                self._click[i] = False
            else:
                io.mouse_down[i] = False
        for i, b in enumerate(self._mouse_buttons):
            if b:
                io.mouse_down[i] = True

        io.mouse_pos = self._mouse_pos
        io.mouse_wheel = self._mouse_wheel
        self._mouse_wheel = 0.0

    def click(self, button=0):
        """Simulate a mouse button click.

        * 0 .. left button
        * 1 .. right button
        * 2 .. middle button
        """
        self._click[button] = True

    def set_mouse_pos(self, x, y):
        'Set the mouse cursor position to a specified value.'
        self._mouse_pos = x, y

    def scroll_up(self):
        'Scroll the mouse wheel up one click.'
        self._mouse_wheel = 1

    def scroll_dn(self):
        'Scroll the mouse wheel down one click.'
        self._mouse_wheel = -1

    def mouse_dn(self, button=0):
        """Push a specified mouse button."""
        self._mouse_buttons[button] = True

    def mouse_up(self, button=0):
        """Release a specified mouse button."""
        self._mouse_buttons[button] = False

    def key_dn(self, key):
        imgui.get_io().keys_down[key] = True
        exit(0)

    def key_up(self, key):
        imgui.get_io().keys_down[key] = False

    def write_char(self, c):
        """Write a character with a given char code."""
        assert 0 < c < 0x10000
        imgui.get_io().add_input_character(c)

Ancestors

  • imgui.integrations.opengl.ProgrammablePipelineRenderer
  • imgui.integrations.opengl.BaseOpenGLRenderer

Methods

def click(self, button=0)

Simulate a mouse button click.

  • 0 .. left button
  • 1 .. right button
  • 2 .. middle button
Expand source code
def click(self, button=0):
    """Simulate a mouse button click.

    * 0 .. left button
    * 1 .. right button
    * 2 .. middle button
    """
    self._click[button] = True
def key_dn(self, key)
Expand source code
def key_dn(self, key):
    imgui.get_io().keys_down[key] = True
    exit(0)
def key_up(self, key)
Expand source code
def key_up(self, key):
    imgui.get_io().keys_down[key] = False
def mouse_dn(self, button=0)

Push a specified mouse button.

Expand source code
def mouse_dn(self, button=0):
    """Push a specified mouse button."""
    self._mouse_buttons[button] = True
def mouse_up(self, button=0)

Release a specified mouse button.

Expand source code
def mouse_up(self, button=0):
    """Release a specified mouse button."""
    self._mouse_buttons[button] = False
def process_inputs(self)

Process the virtual user inputs. Called by main() at the beginning of each frame.

Expand source code
def process_inputs(self):
    """Process the virtual user inputs. Called by `main` at the beginning of each frame."""
    io = imgui.get_io()

    window_size = glfw.get_window_size(self.window)
    fb_size = glfw.get_framebuffer_size(self.window)

    io.display_size = window_size
    io.display_fb_scale = compute_fb_scale(window_size, fb_size)
    io.delta_time = 1.0/60

    current_time = glfw.get_time()

    if self._gui_time:
        self.io.delta_time = current_time - self._gui_time
    else:
        self.io.delta_time = 1. / 60.

    self._gui_time = current_time

    for i, b in enumerate(self._click):
        if b:
            io.mouse_down[i] = True
            self._click[i] = False
        else:
            io.mouse_down[i] = False
    for i, b in enumerate(self._mouse_buttons):
        if b:
            io.mouse_down[i] = True

    io.mouse_pos = self._mouse_pos
    io.mouse_wheel = self._mouse_wheel
    self._mouse_wheel = 0.0
def scroll_dn(self)

Scroll the mouse wheel down one click.

Expand source code
def scroll_dn(self):
    'Scroll the mouse wheel down one click.'
    self._mouse_wheel = -1
def scroll_up(self)

Scroll the mouse wheel up one click.

Expand source code
def scroll_up(self):
    'Scroll the mouse wheel up one click.'
    self._mouse_wheel = 1
def set_mouse_pos(self, x, y)

Set the mouse cursor position to a specified value.

Expand source code
def set_mouse_pos(self, x, y):
    'Set the mouse cursor position to a specified value.'
    self._mouse_pos = x, y
def write_char(self, c)

Write a character with a given char code.

Expand source code
def write_char(self, c):
    """Write a character with a given char code."""
    assert 0 < c < 0x10000
    imgui.get_io().add_input_character(c)