PyAPI: support Operator.poll functions 'disabled' hint

Python scripts can now define the reason it's poll function fails using:

`Operator.poll_message_set(message, ...)`

This supports both regular text as well as delaying message creation
using a callback which should be used in situations where constructing
detailed messages is too much overhead for a poll function.

Ref D11001
This commit is contained in:
2021-04-20 11:57:28 +10:00
parent 985ccba77c
commit ebe04bd3ca
9 changed files with 277 additions and 4 deletions

View File

@@ -79,6 +79,7 @@ set(SRC
bpy_rna_driver.c
bpy_rna_gizmo.c
bpy_rna_id_collection.c
bpy_rna_operator.c
bpy_rna_types_capi.c
bpy_rna_ui.c
bpy_traceback.c
@@ -118,6 +119,7 @@ set(SRC
bpy_rna_driver.h
bpy_rna_gizmo.h
bpy_rna_id_collection.h
bpy_rna_operator.h
bpy_rna_types_capi.h
bpy_rna_ui.h
bpy_traceback.h

View File

@@ -167,6 +167,14 @@ void bpy_context_clear(bContext *UNUSED(C), const PyGILState_STATE *gilstate)
}
}
static void bpy_context_end(bContext *C)
{
if (UNLIKELY(C == NULL)) {
return;
}
CTX_wm_operator_poll_msg_clear(C);
}
/**
* Use for `CTX_*_set(..)` functions need to set values which are later read back as expected.
* In this case we don't want the Python context to override the values as it causes problems
@@ -524,6 +532,9 @@ void BPY_python_end(void)
/* finalizing, no need to grab the state, except when we are a module */
gilstate = PyGILState_Ensure();
/* Clear Python values in the context so freeing the context after Python exits doesn't crash. */
bpy_context_end(BPY_context_get());
/* Decrement user counts of all callback functions. */
BPY_rna_props_clear_all();

View File

@@ -244,12 +244,16 @@ static PyObject *pyop_call(PyObject *UNUSED(self), PyObject *args)
}
if (WM_operator_poll_context((bContext *)C, ot, context) == false) {
const char *msg = CTX_wm_operator_poll_msg_get(C);
bool msg_free = false;
const char *msg = CTX_wm_operator_poll_msg_get(C, &msg_free);
PyErr_Format(PyExc_RuntimeError,
"Operator bpy.ops.%.200s.poll() %.200s",
opname,
msg ? msg : "failed, context is incorrect");
CTX_wm_operator_poll_msg_clear(C);
if (msg_free) {
MEM_freeN((void *)msg);
}
error_val = -1;
}
else {

View File

@@ -0,0 +1,150 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup pythonintern
*
* This file extends `bpy.types.Operator` with C/Python API methods and attributes.
*/
#include <Python.h>
#include "BLI_string.h"
#include "BKE_context.h"
#include "../generic/python_utildefines.h"
#include "BPY_extern.h"
#include "bpy_capi_utils.h"
/* -------------------------------------------------------------------- */
/** \name Operator `poll_message_set` Method
* \{ */
static char *pyop_poll_message_get_fn(bContext *UNUSED(C), void *user_data)
{
PyGILState_STATE gilstate = PyGILState_Ensure();
PyObject *py_args = user_data;
PyObject *py_func_or_msg = PyTuple_GET_ITEM(py_args, 0);
if (PyUnicode_Check(py_func_or_msg)) {
return BLI_strdup(PyUnicode_AsUTF8(py_func_or_msg));
}
PyObject *py_args_after_first = PyTuple_GetSlice(py_args, 1, PY_SSIZE_T_MAX);
PyObject *py_msg = PyObject_CallObject(py_func_or_msg, py_args_after_first);
Py_DECREF(py_args_after_first);
char *msg = NULL;
bool error = false;
/* NULL for no string. */
if (py_msg == NULL) {
error = true;
}
else {
if (py_msg == Py_None) {
/* pass */
}
else if (PyUnicode_Check(py_msg)) {
msg = BLI_strdup(PyUnicode_AsUTF8(py_msg));
}
else {
PyErr_Format(PyExc_TypeError,
"poll_message_set(function, ...): expected string or None, got %.200s",
Py_TYPE(py_msg)->tp_name);
error = true;
}
Py_DECREF(py_msg);
}
if (error) {
PyErr_Print();
PyErr_Clear();
}
PyGILState_Release(gilstate);
return msg;
}
static void pyop_poll_message_free_fn(bContext *UNUSED(C), void *user_data)
{
/* Handles the GIL. */
BPY_DECREF(user_data);
}
PyDoc_STRVAR(BPY_rna_operator_poll_message_set_doc,
".. method:: poll_message_set(message, ...)\n"
"\n"
" Set the message to show in the tool-tip when poll fails.\n"
"\n"
" When message is callable, "
"additional user defined positional arguments are passed to the message function.\n"
"\n"
" :param message: The message or a function that returns the message.\n"
" :type message: string or a callable that returns a string or None.\n");
static PyObject *BPY_rna_operator_poll_message_set(PyObject *UNUSED(self), PyObject *args)
{
const ssize_t args_len = PyTuple_GET_SIZE(args);
if (args_len == 0) {
PyErr_SetString(PyExc_ValueError,
"poll_message_set(message, ...): requires a message argument");
return NULL;
}
PyObject *py_func_or_msg = PyTuple_GET_ITEM(args, 0);
if (PyUnicode_Check(py_func_or_msg)) {
if (args_len > 1) {
PyErr_SetString(PyExc_ValueError,
"poll_message_set(message): does not support additional arguments");
return NULL;
}
}
else if (PyCallable_Check(py_func_or_msg)) {
/* pass */
}
else {
PyErr_Format(PyExc_TypeError,
"poll_message_set(message, ...): "
"expected at least 1 string or callable argument, got %.200s",
Py_TYPE(py_func_or_msg)->tp_name);
return NULL;
}
bContext *C = BPY_context_get();
struct bContextPollMsgDyn_Params params = {
.get_fn = pyop_poll_message_get_fn,
.free_fn = pyop_poll_message_free_fn,
.user_data = Py_INCREF_RET(args),
};
CTX_wm_operator_poll_msg_set_dynamic(C, &params);
Py_RETURN_NONE;
}
PyMethodDef BPY_rna_operator_poll_message_set_method_def = {
"poll_message_set",
(PyCFunction)BPY_rna_operator_poll_message_set,
METH_VARARGS | METH_STATIC,
BPY_rna_operator_poll_message_set_doc,
};
/** \} */

View File

@@ -0,0 +1,31 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup pythonintern
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern PyMethodDef BPY_rna_operator_poll_message_set_method_def;
#ifdef __cplusplus
}
#endif

View File

@@ -41,6 +41,8 @@
#include "bpy_rna_types_capi.h"
#include "bpy_rna_ui.h"
#include "bpy_rna_operator.h"
#include "../generic/py_capi_utils.h"
#include "RNA_access.h"
@@ -86,6 +88,17 @@ static struct PyMethodDef pyrna_uilayout_methods[] = {
/** \} */
/* -------------------------------------------------------------------- */
/** \name Operator
* \{ */
static struct PyMethodDef pyrna_operator_methods[] = {
{NULL, NULL, 0, NULL}, /* #BPY_rna_operator_poll_message_set */
{NULL, NULL, 0, NULL},
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Window Manager Clipboard Property
*
@@ -228,6 +241,11 @@ void BPY_rna_types_extend_capi(void)
/* Space */
pyrna_struct_type_extend_capi(&RNA_Space, pyrna_space_methods, NULL);
/* wmOperator */
ARRAY_SET_ITEMS(pyrna_operator_methods, BPY_rna_operator_poll_message_set_method_def);
BLI_assert(ARRAY_SIZE(pyrna_operator_methods) == 2);
pyrna_struct_type_extend_capi(&RNA_Operator, pyrna_operator_methods, NULL);
/* WindowManager */
pyrna_struct_type_extend_capi(
&RNA_WindowManager, pyrna_windowmanager_methods, pyrna_windowmanager_getset);