Dynamic property palette
The property palette can be created and changed dynamically during the runtime of a PythonPart. In this chapter we are going to focus on various methods, which can control the look and layout of the elements of the property palette.
Control properties
The class ControlProperties represents the internal data structure behind controls in the PythonPart property palette. A control has properties responsible for:
- text shown on the left side of the palette
- value checking (minimal / maximal value, value list)
- background color of the control field
- hiding/showing and enabling/disabling a control
To modify these control properties during the runtime of the PythonPart, use the methods provided in the ControlPropertiesUtil. Using this utility, properties mentioned above can be changed in the script when the palette is initialized or with each property modification done by the user.
In a standard PythonPart the ControlPropertiesUtil object is passed as an argument to the following functions:
- initialize_control_properties - called before the property palette is displayed
- modify_control_properties - called after each change within the property palette
Implement them in your script to get the access to the methods provided in the utility.
In a script object PythonPart, the ControlPropertiesUtil is part of the BaseScriptObjectData - an object that is prepared and given to you by the framework as an argument in the create_script_object function.
In an interactor PythonPart the ControlPropertiesUtil must be constructed first.
from ControlPropertiesUtil import ControlPropertiesUtil
...
ctrl_prop_util = ControlPropertiesUtil(control_props_list, build_ele_list) #(1)!
- The
control_props_list
andbuild_ele_list
are python lists containing ControlProperties and BuildingElement objects respectively. These lists are passed by the framework directly to the create_interactor function based on the .pyp file when the PythonPart is initialized.
After the utility is constructed, its methods can be used as follows:
if ...:
ctrl_prop_util.set_text("Distance", "Distance from left") #(1)!
else:
ctrl_prop_util.set_text("Distance", "Distance from right")
- In this case the text shown in the palette for the parameter Distance is changed
from the default value defined in the
<Text>
tag in the .pyp file intoDistance from left
.
Hide or disable a control
Sometimes it's useful to disable or hide a control inside the property palette dynamically e.g., based on a value of another parameter. Based in the complexity of the condition, this behavior can be implemented directly in the .pyp file or in the script.
In the .pyp file
Managing the visibility and enable state of a specific input control can be done dynamically
directly in the .pyp file by adding the tags <Enable>
and/or <Visible>
in the <Property>
node.
<Parameter>
<Name>String</Name>
<Text>String parameter</Text>
<Value>This is a string</Value>
<ValueType>String</ValueType>
<Enable>True</Enable>
<Visible>0</Visible>
</Parameter>
To apply the condition to multiple directly consecutive parameters, put them into one virtual parameter called condition group
<Parameter>
<ValueType>ConditionGroup</ValueType>
<Visible>FooParameter == 2</Visible>
<Parameter>
<Name>BarParameter</Name>
...
</Parameter>
<Parameter>
<Name>BazParameter</Name>
...
</Parameter>
</Parameter>
Example
Here are some examples of more complex conditions for controlling the visibility.
<Enable>FooParameter == True</Enable> <!--(1)!-->
<Visible>RadioButtonValue in [1, 5]</Visible>
<Enable>FooParameter == __StringTable.get_string("1220", "Column")</Enable> <!--(2)!-->
<Enable>$list_row != FooParameter</Enable> <!--(3)!-->
<Visible>NamedTupleParameter[$list_row].TupleItem</Visible>
<Visible>__is_visible_control("FooParameter") and FooParameter > 0</Visible> <!--(4)!-->
<Enable>__is_input_mode()</Enable> <!--(5)!-->
<Visible> <!--(6)!-->
if FooParameter < 1000:
return True:
if BarParameter ><!--(7)!--> 1000:
return True
return False
</Visible>
- A logical formula including other parameters
- A logical formula including access to the string table
- A logical formula including access to the list row (in case of list parameter)
- A logical formula including a check for the visibility of another parameter
- A logical formula checking the input mode.
The function
__is_input_mode()
returns False for PythonPart modification - A multi-line logical formula
- Because the .pyp file uses the xml syntax, use
<
and>
instead of<
and>
operators.
In the script
If the complexity of the enable or visible condition is so high that it cannot be implemented in the .pyp file, you must implement it in the script. Use the ControlPropertiesUtil class mentioned above. First, define a function returning True, when the control should be visible/enabled:
def initialize_control_properties(build_ele, ctrl_prop_util, _doc):
...
def visible_foo_parameter(...) -> bool: #(1)!
#perform operations to determine the visibility
return ...
def enable_bar_parameter(...) -> bool:
#perform operations to determine the enable state
return ...
-
Provide the right parameter in (...) depending on the type of the parameter value:
value type parameter single value ()
list of values (row_index: int)
single namedtuple (field_name: str)
list of namedtuple (row_index: int, field_name: str)
Then assign the function to the parameter whose visibility/enable state you want to control with it. Use the set_enable_function to control the enable state or set_visible_function to control the visibility.
ctrl_prop_util.set_visible_function("FooParameter", visible_length)
ctrl_prop_util.set_enable_function("BarParameter", enable_length)
Example on GitHub
Refer to the example Visibility ( PYP | PY) to see how the controls can be hidden/shown/disabled/enabled dynamically in the PYP file or in the script. Additionally, you can also have a look at the example ValueList ( PYP | PY)
Active page
Sometimes it's useful to do display the handles or some preview data depending on which page of the property palette is currently active. This behavior can be implemented with the set_active_palette_page_index function. You can implement it into your standard PythonPart as well as into your interactor
Dynamic text
If the text inside the property palette needs to be dynamic, this can be implemented
in the script (as described in the chapter before) or
directly in the PYP file using the <TextDyn>
tag. The same can be done for the
<ValueTextDyn>
tag, which creates a dynamic value text.
A single line Python expression can be implemented in this tag like this:
<TextDyn>__StringTable.get_string(1003, "Placement") + ("Right" if DistanceHeader else "Left")<!--(1)!--></TextDyn>
- Functions from the math package are possible and can be used by math.xxx.
A multi-line Python expression can also be implemented. The Python source code for the expression
must be left aligned in the .pyp file. Because the .pyp file uses the XML syntax, <
and
>
operators must be used instead of <
and >
.
In case of a list parameter, the row index can be accessed with $list_row
.
Dynamic palette content
Currently, the PythonParts framework does not directly support handling multiple palettes simultaneously. However, you can make the entire content of the palette show-up dynamically by simply creating multiple include parameters and showing them depending on the situation.
Create a .pyp file and multiple .incpyp files, e.g. like this:
Following .pyp will dynamically include the FirstPalette.incpyp
or SecondPalette.incpyp
, based on
the current input step.
<?xml version="1.0" encoding="utf-8"?>
<Element>
...
<Page>
<Parameter>
<Name>FirstPalette</Name>
<Text/>
<Value>FirstPalette.incpyp</Value>
<ValueType>Include</ValueType>
<Visible>InputStep == 1</Visible>
</Parameter>
<Parameter>
<Name>SecondPalette</Name>
<Text/>
<Value>SecondPalette.incpyp</Value>
<ValueType>Include</ValueType>
<Visible>InputStep == 2</Visible>
</Parameter>
</Page>
<Page>
<Name>__HiddenPage__</Name>
<Text></Text>
<Parameter>
<Name>InputStep</Name> <!--(1)!-->
<Text/>
<Value>1</Value>
<ValueType>Integer</ValueType>
<Persistent>No</Persistent> <!--(2)!-->
</Parameter>
</Page>
</Element>
- This parameter would only temporarily save the information about current input step
- Make sure, it is not saved anywhere by disabling its persistency
Following .incpyp has a button with an event id of 1001
. Pressing the button should
show the next palette.
<?xml version="1.0" encoding="utf-8"?>
<Include>
<Parameter>
<Name>SomeText</Name>
<Text>This text will appear only</Text>
<Value>during the first step</Value>
<ValueType>Text</ValueType>
</Parameter>
<Parameter>
<Name>NextStepButtonRow</Name>
<Text>Go to the next palette</Text>
<ValueType>Row</ValueType>
<Parameter>
<Name>NextStepButton</Name>
<Text>Go!</Text>
<EventId>1001</EventId>
<ValueType>Button</ValueType>
</Parameter>
</Parameter>
</Include>
Following .incpyp includes a button with an event id of 1002
. Pressing the button should
show the previous palette.
<?xml version="1.0" encoding="utf-8"?>
<Include>
<Parameter>
<Name>SomeOtherText</Name>
<Text>This text will appear only</Text>
<Value>during the second step</Value>
<ValueType>Text</ValueType>
</Parameter>
<Parameter>
<Name>PreviousStepButtonRow</Name>
<Text>Go to the previous palette</Text>
<ValueType>Row</ValueType>
<Parameter>
<Name>PreviousStepButton</Name>
<Text>Go back!</Text>
<EventId>1002</EventId>
<ValueType>Button</ValueType>
</Parameter>
</Parameter>
</Include>
In the script, you have to handle the on_control_event
accordingly. The following example
is valid for a script object, but you can implement
the behavior similarly also in other types of contract:
def on_control_event(self, event_id: int) -> bool:
match event_id:
case 1001:
self.build_ele.InputStep.value = 2
return True #(1)!
case 1002:
self.build_ele.InputStep.value = 1
return True
return False
- This will update the palette so that the content of
SecondPalette.incpyp
is shown. You should update the palette only when it's necessary
Tip
In a script object, you can also react to the event of pressing ++ESC++ and e.g. go from step 2 to step 1:
def on_cancel_function(self) -> OnCancelFunctionResult:
if self.build_ele.InputStep.value == 2:
self.build_ele.InputStep.value = 1
return OnCancelFunctionResult.CONTINUE_INPUT
return OnCancelFunctionResult.CANCEL_INPUT #(1)!
- In case the current input step is 1, simply terminate the PythonPart.
Palette handling in interactor
In an interactor PythonPart, you as a developer have to take care of handling the palette. This includes:
- showing and closing and the start and end of the script respectively
- updating the palette during the runtime to show up-to-date values and/or change the states of controls (show/hide or enable/disable them)
Info
In a standard PythonPart or in a
Script Object, all this is handled
by the framework. The only thing you need to take care of, is return True
when you
want the palette to update itself (e.g. after the
modify_element_property
event)
Class for the job is BuildingElementPaletteService. Here are the steps you have to implement:
-
When initializing the interactor:
-
Initialize the palette service:
def __init__(...): self.build_ele = build_ele_list[0] ... self.palette_service = BuildingElementPaletteService( self.build_ele, build_ele_composite, self.build_ele.script_name, control_props_list, self.build_ele.pyp_file_name + "\\" #(1)! )
-
You can provide the path with all the picture assets, that you use in the palette. Whenever you define e.g. a picture parameter, or any other control with a defined path to a picture file, the framework will look for the file under this path.
The path parts must be separated with a backslash
\
and there must be a trailing backslash at the end, e.g.:C:\\path\\to\\picture\\assets\\
.The
build_ele.pyp_file_name
will get you the path to the directory of the PYP file, however without the trailing backslash!
-
-
Show the palette:
-
-
During the runtime of a PythonPart, on every property modification (in the
modify_element_property
event):-
Write the new value back into the BuildingElement and recalculate other parameter values, that might depend on the modified one. Do it by calling the modify_element_property method.
-
New parameter value might have result in some controls change their appearance (they get disabled, hidden, text may changed, etc..). To show the up-to-date state of the palette to the user, you must update it by calling update_palette.
Here is how the implementation may look like:
def modify_element_property(self, page, name, value): update_necessary = self.palette_service.modify_element_property(page, name, value) # here you can implement some additional calculations if update_necessary self.palette_service.update_palette(-1, False) #(1)!
- The
-1
wil update the currently active page
-
-
During the runtime of a PythonPart, on each event triggered by pressing a button (during the
on_control_event
):Note
The implementation shown below is needed only, if pressing a button actually changes the appearance of the palette. Otherwise the update is not necessary and should be avoided!
-
Write the new value back into the BuildingElement by calling the on_control_event method
-
Show the up-to-date state of the palette to the user by calling update_palette.
Here is how the implementation may look like:
-
-
At the end of the PythonPart runtime, close the palette. This must be handled in both
on_cancel_function
event, like:... as well as in the
on_cancel_by_menu_function
event, like: