WM: Event simulation support for Python
authorCampbell Barton <ideasman42@gmail.com>
Sat, 2 Feb 2019 04:14:51 +0000 (15:14 +1100)
committerCampbell Barton <ideasman42@gmail.com>
Sat, 2 Feb 2019 04:23:55 +0000 (15:23 +1100)
This feature is intended only for testing,
to automate simulating user input.

- Enabled by '--enable-event-simulate'.
- Disables handling all real input events.
- Access by calling `Window.event_simulate(..)`
- Disabling `bpy.app.use_event_simulate`
  to allow handling real events (can only disable).

Currently only mouse & keyboard events work well,
NDOF, IME... etc could be added as needed.

See D4286 for example usage.

source/blender/blenkernel/BKE_global.h
source/blender/makesrna/intern/rna_wm_api.c
source/blender/python/intern/bpy_app.c
source/blender/windowmanager/WM_api.h
source/blender/windowmanager/intern/wm_event_system.c
source/blender/windowmanager/intern/wm_window.c
source/creator/creator_args.c

index 045fc77..706dec7 100644 (file)
@@ -109,6 +109,8 @@ enum {
        G_FLAG_RENDER_VIEWPORT = (1 << 0),
        G_FLAG_BACKBUFSEL = (1 << 1),
        G_FLAG_PICKSEL = (1 << 2),
+       /** Support simulating events (for testing). */
+       G_FLAG_EVENT_SIMULATE = (1 << 3),
 
        G_FLAG_SCRIPT_AUTOEXEC = (1 << 13),
        /** When this flag is set ignore the prefs #USER_SCRIPT_AUTOEXEC_DISABLE. */
@@ -119,7 +121,7 @@ enum {
 
 /** Don't overwrite these flags when reading a file. */
 #define G_FLAG_ALL_RUNTIME \
-       (G_FLAG_SCRIPT_AUTOEXEC | G_FLAG_SCRIPT_OVERRIDE_PREF)
+       (G_FLAG_SCRIPT_AUTOEXEC | G_FLAG_SCRIPT_OVERRIDE_PREF | G_FLAG_EVENT_SIMULATE)
 
 /** Flags to read from blend file. */
 #define G_FLAG_ALL_READFILE 0
index 252012a..c306511 100644 (file)
@@ -24,6 +24,7 @@
 
 #include <stdlib.h>
 #include <stdio.h>
+#include <ctype.h>
 
 #include "BLI_utildefines.h"
 
@@ -468,6 +469,78 @@ static PointerRNA rna_WindoManager_operator_properties_last(const char *idname)
        return PointerRNA_NULL;
 }
 
+static wmEvent *rna_Window_event_add_simulate(
+        wmWindow *win, ReportList *reports,
+        int type, int value, const char *unicode,
+        int x, int y,
+        bool shift, bool ctrl, bool alt, bool oskey)
+{
+       if ((G.f & G_FLAG_EVENT_SIMULATE) == 0) {
+               BKE_report(reports, RPT_ERROR, "Not running with '--enable-event-simulate' enabled");
+               return NULL;
+       }
+
+       if (!ELEM(value, KM_PRESS, KM_RELEASE, KM_NOTHING)) {
+               BKE_report(reports, RPT_ERROR, "value: only 'PRESS/RELEASE/NOTHING' are supported");
+               return NULL;
+       }
+       if (ISKEYBOARD(type) || ISMOUSE_BUTTON(type)) {
+               if (!ELEM(value, KM_PRESS, KM_RELEASE)) {
+                       BKE_report(reports, RPT_ERROR, "value: must be 'PRESS/RELEASE' for keyboard/buttons");
+                       return NULL;
+               }
+       }
+       if (ELEM(type, MOUSEMOVE, INBETWEEN_MOUSEMOVE)) {
+               if (value != KM_NOTHING) {
+                       BKE_report(reports, RPT_ERROR, "value: must be 'NOTHING' for motion");
+                       return NULL;
+               }
+       }
+       if (unicode != NULL) {
+               if (value != KM_PRESS) {
+                       BKE_report(reports, RPT_ERROR, "value: must be 'PRESS' when unicode is set");
+                       return NULL;
+               }
+       }
+       /* TODO: validate NDOF. */
+
+       char ascii = 0;
+       if (unicode != NULL) {
+               int len = BLI_str_utf8_size(unicode);
+               if (len == -1 || unicode[len] != '\0') {
+                       BKE_report(reports, RPT_ERROR, "Only a single character supported");
+                       return NULL;
+               }
+               if (len == 1 && isascii(unicode[0])) {
+                       ascii = unicode[0];
+               }
+       }
+
+       wmEvent e = {NULL};
+       e.type = type;
+       e.val = value;
+       e.x = x;
+       e.y = y;
+       /* Note: KM_MOD_FIRST, KM_MOD_SECOND aren't used anywhere, set as bools */
+       e.shift = shift;
+       e.ctrl = ctrl;
+       e.alt = alt;
+       e.oskey = oskey;
+
+       const wmEvent *evt = win->eventstate;
+       e.prevx = evt->x;
+       e.prevy = evt->y;
+       e.prevval = evt->val;
+       e.prevtype = evt->type;
+
+       if (unicode != NULL) {
+               e.ascii = ascii;
+               STRNCPY(e.utf8_buf, unicode);
+       }
+
+       return WM_event_add_simulate(win, &e);
+}
+
 #else
 
 #define WM_GEN_INVOKE_EVENT (1 << 0)
@@ -524,6 +597,26 @@ void RNA_api_window(StructRNA *srna)
 
        RNA_def_function(srna, "cursor_modal_restore", "WM_cursor_modal_restore");
        RNA_def_function_ui_description(func, "Restore the previous cursor after calling ``cursor_modal_set``");
+
+       /* Arguments match 'rna_KeyMap_item_new'. */
+       func = RNA_def_function(srna, "event_simulate", "rna_Window_event_add_simulate");
+       RNA_def_function_flag(func, FUNC_USE_REPORTS);
+       parm = RNA_def_enum(func, "type", rna_enum_event_type_items, 0, "Type", "");
+       RNA_def_parameter_flags(parm, 0, PARM_REQUIRED);
+       parm = RNA_def_enum(func, "value", rna_enum_event_value_items, 0, "Value", "");
+       RNA_def_parameter_flags(parm, 0, PARM_REQUIRED);
+       parm = RNA_def_string(func, "unicode", NULL, 0, "", "");
+       RNA_def_parameter_clear_flags(parm, PROP_NEVER_NULL, 0);
+
+       RNA_def_int(func, "x", 0, INT_MIN, INT_MAX, "", "", INT_MIN, INT_MAX);
+       RNA_def_int(func, "y", 0, INT_MIN, INT_MAX, "", "", INT_MIN, INT_MAX);
+
+       RNA_def_boolean(func, "shift", 0, "Shift", "");
+       RNA_def_boolean(func, "ctrl", 0, "Ctrl", "");
+       RNA_def_boolean(func, "alt", 0, "Alt", "");
+       RNA_def_boolean(func, "oskey", 0, "OS Key", "");
+       parm = RNA_def_pointer(func, "event", "Event", "Item", "Added key map item");
+       RNA_def_function_return(func, parm);
 }
 
 void RNA_api_wm(StructRNA *srna)
index 992694a..6172330 100644 (file)
@@ -259,6 +259,41 @@ static int bpy_app_debug_set(PyObject *UNUSED(self), PyObject *value, void *clos
        return 0;
 }
 
+PyDoc_STRVAR(bpy_app_global_flag_doc,
+"Boolean, for application behavior (started with --enable-* matching this attribute name)"
+);
+static PyObject *bpy_app_global_flag_get(PyObject *UNUSED(self), void *closure)
+{
+       const int flag = POINTER_AS_INT(closure);
+       return PyBool_FromLong(G.f & flag);
+}
+
+static int bpy_app_global_flag_set(PyObject *UNUSED(self), PyObject *value, void *closure)
+{
+       const int flag = POINTER_AS_INT(closure);
+       const int param = PyObject_IsTrue(value);
+
+       if (param == -1) {
+               PyErr_SetString(PyExc_TypeError, "bpy.app.use_* can only be True/False");
+               return -1;
+       }
+
+       if (param)  G.f |=  flag;
+       else        G.f &= ~flag;
+
+       return 0;
+}
+
+static int bpy_app_global_flag_set__only_disable(PyObject *UNUSED(self), PyObject *value, void *closure)
+{
+       const int param = PyObject_IsTrue(value);
+       if (param == 1) {
+               PyErr_SetString(PyExc_ValueError, "This bpy.app.use_* option can only be disabled");
+               return -1;
+       }
+       return bpy_app_global_flag_set(NULL, value, closure);
+}
+
 #define BROKEN_BINARY_PATH_PYTHON_HACK
 
 PyDoc_STRVAR(bpy_app_binary_path_python_doc,
@@ -316,12 +351,6 @@ static int bpy_app_debug_value_set(PyObject *UNUSED(self), PyObject *value, void
        return 0;
 }
 
-static PyObject *bpy_app_global_flag_get(PyObject *UNUSED(self), void *closure)
-{
-       const int flag = POINTER_AS_INT(closure);
-       return PyBool_FromLong(G.f & flag);
-}
-
 PyDoc_STRVAR(bpy_app_tempdir_doc,
 "String, the temp directory used by blender (read-only)"
 );
@@ -401,6 +430,7 @@ static PyGetSetDef bpy_app_getsets[] = {
        {(char *)"debug_io",        bpy_app_debug_get, bpy_app_debug_set, (char *)bpy_app_debug_doc, (void *)G_DEBUG_IO},
 
        {(char *)"use_static_override", bpy_app_use_static_override_get, bpy_app_use_static_override_set, (char *)bpy_app_use_static_override_doc, NULL},
+       {(char *)"use_event_simulate", bpy_app_global_flag_get, bpy_app_global_flag_set__only_disable, (char *)bpy_app_global_flag_doc, (void *)G_FLAG_EVENT_SIMULATE},
 
        {(char *)"binary_path_python", bpy_app_binary_path_python_get, NULL, (char *)bpy_app_binary_path_python_doc, NULL},
 
index 613374d..7d8f5c1 100644 (file)
@@ -616,6 +616,9 @@ bool        WM_event_is_tablet(const struct wmEvent *event);
 bool        WM_event_is_ime_switch(const struct wmEvent *event);
 #endif
 
+/* For testing only 'G_FLAG_EVENT_SIMULATE' */
+struct wmEvent *WM_event_add_simulate(struct wmWindow *win, const struct wmEvent *event_to_add);
+
 const char *WM_window_cursor_keymap_status_get(const struct wmWindow *win, int button_index, int type_index);
 void WM_window_cursor_keymap_status_refresh(struct bContext *C, struct wmWindow *win);
 
index 8ccc245..a2ee5f0 100644 (file)
@@ -127,6 +127,18 @@ wmEvent *wm_event_add(wmWindow *win, const wmEvent *event_to_add)
        return wm_event_add_ex(win, event_to_add, NULL);
 }
 
+wmEvent *WM_event_add_simulate(wmWindow *win, const wmEvent *event_to_add)
+{
+       if ((G.f & G_FLAG_EVENT_SIMULATE) == 0) {
+               BLI_assert(0);
+               return NULL;
+       }
+       wmEvent *event = wm_event_add(win, event_to_add);
+       win->eventstate->x = event->x;
+       win->eventstate->y = event->y;
+       return event;
+}
+
 void wm_event_free(wmEvent *event)
 {
        if (event->customdata) {
@@ -3899,6 +3911,10 @@ void wm_event_add_ghostevent(wmWindowManager *wm, wmWindow *win, int type, int U
 {
        wmWindow *owin;
 
+       if (UNLIKELY(G.f & G_FLAG_EVENT_SIMULATE)) {
+               return;
+       }
+
        /* Having both, event and evt, can be highly confusing to work with, but is necessary for
         * our current event system, so let's clear things up a bit:
         * - data added to event only will be handled immediately, but will not be copied to the next event
index fa11979..79e6b51 100644 (file)
@@ -1049,6 +1049,11 @@ void wm_cursor_position_to_ghost(wmWindow *win, int *x, int *y)
 
 void wm_get_cursor_position(wmWindow *win, int *x, int *y)
 {
+       if (UNLIKELY(G.f & G_FLAG_EVENT_SIMULATE)) {
+               *x = win->eventstate->x;
+               *y = win->eventstate->y;
+               return;
+       }
        GHOST_GetCursorPosition(g_system, x, y);
        wm_cursor_position_from_ghost(win, x, y);
 }
index 294944f..4040b09 100644 (file)
@@ -565,6 +565,7 @@ static int arg_handle_print_help(int UNUSED(argc), const char **UNUSED(argv), vo
        BLI_argsPrintArgDoc(ba, "--app-template");
        BLI_argsPrintArgDoc(ba, "--factory-startup");
        BLI_argsPrintArgDoc(ba, "--enable-static-override");
+       BLI_argsPrintArgDoc(ba, "--enable-event-simulate");
        printf("\n");
        BLI_argsPrintArgDoc(ba, "--env-system-datafiles");
        BLI_argsPrintArgDoc(ba, "--env-system-scripts");
@@ -1016,6 +1017,15 @@ static int arg_handle_enable_static_override(int UNUSED(argc), const char **UNUS
        return 0;
 }
 
+static const char arg_handle_enable_event_simulate_doc[] =
+"\n\tEnable event simulation testing feature 'bpy.types.Window.event_simulate'."
+;
+static int arg_handle_enable_event_simulate(int UNUSED(argc), const char **UNUSED(argv), void *UNUSED(data))
+{
+       G.f |= G_FLAG_EVENT_SIMULATE;
+       return 0;
+}
+
 static const char arg_handle_env_system_set_doc_datafiles[] =
 "\n\tSet the "STRINGIFY_ARG (BLENDER_SYSTEM_DATAFILES)" environment variable.";
 static const char arg_handle_env_system_set_doc_scripts[] =
@@ -1950,6 +1960,7 @@ void main_args_setup(bContext *C, bArgs *ba)
        BLI_argsAdd(ba, 1, NULL, "--app-template", CB(arg_handle_app_template), NULL);
        BLI_argsAdd(ba, 1, NULL, "--factory-startup", CB(arg_handle_factory_startup_set), NULL);
        BLI_argsAdd(ba, 1, NULL, "--enable-static-override", CB(arg_handle_enable_static_override), NULL);
+       BLI_argsAdd(ba, 1, NULL, "--enable-event-simulate", CB(arg_handle_enable_event_simulate), NULL);
 
        /* TODO, add user env vars? */
        BLI_argsAdd(ba, 1, NULL, "--env-system-datafiles", CB_EX(arg_handle_env_system_set, datafiles), NULL);