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:
parent
985ccba77c
commit
ebe04bd3ca
|
@ -206,8 +206,24 @@ void CTX_wm_area_set(bContext *C, struct ScrArea *area);
|
|||
void CTX_wm_region_set(bContext *C, struct ARegion *region);
|
||||
void CTX_wm_menu_set(bContext *C, struct ARegion *menu);
|
||||
void CTX_wm_gizmo_group_set(bContext *C, struct wmGizmoGroup *gzgroup);
|
||||
const char *CTX_wm_operator_poll_msg_get(struct bContext *C);
|
||||
|
||||
/**
|
||||
* Values to create the message that describes the reason poll failed.
|
||||
*
|
||||
* \note This must be called in the same context as the poll function that created it.
|
||||
*/
|
||||
struct bContextPollMsgDyn_Params {
|
||||
/** The result is allocated . */
|
||||
char *(*get_fn)(bContext *C, void *user_data);
|
||||
/** Optionally free the user-data. */
|
||||
void (*free_fn)(bContext *C, void *user_data);
|
||||
void *user_data;
|
||||
};
|
||||
|
||||
const char *CTX_wm_operator_poll_msg_get(struct bContext *C, bool *r_free);
|
||||
void CTX_wm_operator_poll_msg_set(struct bContext *C, const char *msg);
|
||||
void CTX_wm_operator_poll_msg_set_dynamic(bContext *C,
|
||||
const struct bContextPollMsgDyn_Params *parms);
|
||||
void CTX_wm_operator_poll_msg_clear(struct bContext *C);
|
||||
|
||||
/* Data Context
|
||||
|
|
|
@ -87,6 +87,10 @@ struct bContext {
|
|||
* For more advanced formatting use `operator_poll_msg_dyn_params`.
|
||||
*/
|
||||
const char *operator_poll_msg;
|
||||
/**
|
||||
* Store values to dynamically to create the string (called when a tool-tip is shown).
|
||||
*/
|
||||
struct bContextPollMsgDyn_Params operator_poll_msg_dyn_params;
|
||||
} wm;
|
||||
|
||||
/* data context */
|
||||
|
@ -119,11 +123,16 @@ bContext *CTX_copy(const bContext *C)
|
|||
{
|
||||
bContext *newC = MEM_dupallocN((void *)C);
|
||||
|
||||
memset(&newC->wm.operator_poll_msg_dyn_params, 0, sizeof(newC->wm.operator_poll_msg_dyn_params));
|
||||
|
||||
return newC;
|
||||
}
|
||||
|
||||
void CTX_free(bContext *C)
|
||||
{
|
||||
/* This may contain a dynamically allocated message, free. */
|
||||
CTX_wm_operator_poll_msg_clear(C);
|
||||
|
||||
MEM_freeN(C);
|
||||
}
|
||||
|
||||
|
@ -1011,15 +1020,43 @@ void CTX_wm_gizmo_group_set(bContext *C, struct wmGizmoGroup *gzgroup)
|
|||
|
||||
void CTX_wm_operator_poll_msg_clear(bContext *C)
|
||||
{
|
||||
struct bContextPollMsgDyn_Params *params = &C->wm.operator_poll_msg_dyn_params;
|
||||
if (params->free_fn != NULL) {
|
||||
params->free_fn(C, params->user_data);
|
||||
}
|
||||
params->get_fn = NULL;
|
||||
params->free_fn = NULL;
|
||||
params->user_data = NULL;
|
||||
|
||||
C->wm.operator_poll_msg = NULL;
|
||||
}
|
||||
void CTX_wm_operator_poll_msg_set(bContext *C, const char *msg)
|
||||
{
|
||||
CTX_wm_operator_poll_msg_clear(C);
|
||||
|
||||
C->wm.operator_poll_msg = msg;
|
||||
}
|
||||
|
||||
const char *CTX_wm_operator_poll_msg_get(bContext *C)
|
||||
void CTX_wm_operator_poll_msg_set_dynamic(bContext *C,
|
||||
const struct bContextPollMsgDyn_Params *params)
|
||||
{
|
||||
CTX_wm_operator_poll_msg_clear(C);
|
||||
|
||||
C->wm.operator_poll_msg_dyn_params = *params;
|
||||
}
|
||||
|
||||
const char *CTX_wm_operator_poll_msg_get(bContext *C, bool *r_free)
|
||||
{
|
||||
struct bContextPollMsgDyn_Params *params = &C->wm.operator_poll_msg_dyn_params;
|
||||
if (params->get_fn != NULL) {
|
||||
char *msg = params->get_fn(C, params->user_data);
|
||||
if (msg != NULL) {
|
||||
*r_free = true;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
*r_free = false;
|
||||
return IFACE_(C->wm.operator_poll_msg);
|
||||
}
|
||||
|
||||
|
|
|
@ -947,12 +947,13 @@ static uiTooltipData *ui_tooltip_data_from_button(bContext *C, uiBut *but)
|
|||
/* button is disabled, we may be able to tell user why */
|
||||
if (but->flag & UI_BUT_DISABLED) {
|
||||
const char *disabled_msg = NULL;
|
||||
bool disabled_msg_free = false;
|
||||
|
||||
/* if operator poll check failed, it can give pretty precise info why */
|
||||
if (but->optype) {
|
||||
CTX_wm_operator_poll_msg_clear(C);
|
||||
WM_operator_poll_context(C, but->optype, but->opcontext);
|
||||
disabled_msg = CTX_wm_operator_poll_msg_get(C);
|
||||
disabled_msg = CTX_wm_operator_poll_msg_get(C, &disabled_msg_free);
|
||||
}
|
||||
/* alternatively, buttons can store some reasoning too */
|
||||
else if (but->disabled_info) {
|
||||
|
@ -967,6 +968,9 @@ static uiTooltipData *ui_tooltip_data_from_button(bContext *C, uiBut *but)
|
|||
});
|
||||
field->text = BLI_sprintfN(TIP_("Disabled: %s"), disabled_msg);
|
||||
}
|
||||
if (disabled_msg_free) {
|
||||
MEM_freeN((void *)disabled_msg);
|
||||
}
|
||||
}
|
||||
|
||||
if ((U.flag & USER_TOOLTIPS_PYTHON) && !but->optype && rna_struct.strinfo) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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, ¶ms);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/** \} */
|
|
@ -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
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue