From 141c6073ca39f0d59c67ebef89b094395b903a4a Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 2 Feb 2019 15:14:51 +1100 Subject: WM: Event simulation support for Python 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 | 4 +- source/blender/makesrna/intern/rna_wm_api.c | 93 ++++++++++++++++++++++ source/blender/python/intern/bpy_app.c | 42 ++++++++-- source/blender/windowmanager/WM_api.h | 3 + .../blender/windowmanager/intern/wm_event_system.c | 16 ++++ source/blender/windowmanager/intern/wm_window.c | 5 ++ 6 files changed, 156 insertions(+), 7 deletions(-) (limited to 'source/blender') diff --git a/source/blender/blenkernel/BKE_global.h b/source/blender/blenkernel/BKE_global.h index 045fc772426..706dec7580c 100644 --- a/source/blender/blenkernel/BKE_global.h +++ b/source/blender/blenkernel/BKE_global.h @@ -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 diff --git a/source/blender/makesrna/intern/rna_wm_api.c b/source/blender/makesrna/intern/rna_wm_api.c index 252012a87ea..c3065111020 100644 --- a/source/blender/makesrna/intern/rna_wm_api.c +++ b/source/blender/makesrna/intern/rna_wm_api.c @@ -24,6 +24,7 @@ #include #include +#include #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) diff --git a/source/blender/python/intern/bpy_app.c b/source/blender/python/intern/bpy_app.c index 992694a71c2..6172330a875 100644 --- a/source/blender/python/intern/bpy_app.c +++ b/source/blender/python/intern/bpy_app.c @@ -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}, diff --git a/source/blender/windowmanager/WM_api.h b/source/blender/windowmanager/WM_api.h index 613374dc1df..7d8f5c1c0b2 100644 --- a/source/blender/windowmanager/WM_api.h +++ b/source/blender/windowmanager/WM_api.h @@ -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); diff --git a/source/blender/windowmanager/intern/wm_event_system.c b/source/blender/windowmanager/intern/wm_event_system.c index 8ccc2457a90..a2ee5f04449 100644 --- a/source/blender/windowmanager/intern/wm_event_system.c +++ b/source/blender/windowmanager/intern/wm_event_system.c @@ -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 diff --git a/source/blender/windowmanager/intern/wm_window.c b/source/blender/windowmanager/intern/wm_window.c index fa11979e39e..79e6b5104a3 100644 --- a/source/blender/windowmanager/intern/wm_window.c +++ b/source/blender/windowmanager/intern/wm_window.c @@ -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); } -- cgit v1.2.3