Hi guys,
Any idea how to set a vertical menu tree like when I click Menu Item then display the sub-menu in the same view when I click this Menu Item again and then collapse this sub-menu in perspective?
Same as that dropdown.
Thanks,
Priyanka
Hi guys,
Any idea how to set a vertical menu tree like when I click Menu Item then display the sub-menu in the same view when I click this Menu Item again and then collapse this sub-menu in perspective?
Same as that dropdown.
Thanks,
Priyanka
I don't think this component can do what you want, but it's easy enough to make yourself.
The simplest way would be to use flex containers to host your navigation buttons, and make those containers basis depend on a custom property on the menu button.
You can even add a transition: flex-basis 500ms
style prop to the container to make it animate nicely.
Here's the json of an example based on what I use:
{
"custom": {
"current_category": "debug"
},
"params": {},
"propConfig": {
"custom.current_category": {
"binding": {
"config": {
"path": "page.props.primaryView"
},
"transforms": [
{
"code": "\ttry:\n\t\treturn value.split(\u0027/\u0027)[-2]\n\texcept:\n\t\treturn \"debug\"",
"type": "script"
}
],
"type": "property"
},
"persistent": true
}
},
"props": {},
"root": {
"children": [
{
"children": [
{
"custom": {
"view": "some_category/mainpage"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "main_button"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({view.custom.current_category} \u003d {parent.custom.category},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "some category"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "some_category/page_1"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page1"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 1"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "some_category/page_2"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page2"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 2"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "some_category/page_3"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page3"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 3"
},
"type": "ia.input.button"
}
],
"custom": {
"category": "some_category"
},
"meta": {
"name": "some_category"
},
"position": {
"shrink": 0
},
"propConfig": {
"position.basis": {
"binding": {
"config": {
"expression": "if ({view.custom.current_category} \u003d {this.custom.category},\r\n\t\u0027148px\u0027,\r\n\t\u002734px\u0027\r\n)"
},
"type": "expr"
}
}
},
"props": {
"direction": "column",
"style": {
"gap": "4px",
"margin": "6px",
"overflow": "hidden",
"transition": "flex-basis 500ms"
}
},
"type": "ia.container.flex"
},
{
"children": [
{
"custom": {
"view": "other_category/mainpage"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "main_button"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({view.custom.current_category} \u003d {parent.custom.category},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "other category"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "other_category/page_1"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page1"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 1"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "other_category/page_2"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page2"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 2"
},
"type": "ia.input.button"
},
{
"custom": {
"view": "other_category/page_3"
},
"events": {
"component": {
"onActionPerformed": {
"config": {
"params": {},
"view": "{this.custom.view}"
},
"scope": "C",
"type": "nav"
}
}
},
"meta": {
"name": "page3"
},
"position": {
"basis": "34px",
"shrink": 0
},
"propConfig": {
"props.style.classes": {
"binding": {
"config": {
"expression": "if ({this.custom.view} \u003d {page.props.primaryView},\r\n\t\"button_selected\",\r\n\t\"button\"\r\n)"
},
"type": "expr"
}
}
},
"props": {
"justify": "start",
"style": {
"borderColor": "#808080",
"fontWeight": "normal"
},
"text": "\u003e page 3"
},
"type": "ia.input.button"
}
],
"custom": {
"category": "other_category"
},
"meta": {
"name": "other_category"
},
"position": {
"shrink": 0
},
"propConfig": {
"position.basis": {
"binding": {
"config": {
"expression": "if ({view.custom.current_category} \u003d {this.custom.category},\r\n\t\u0027148px\u0027,\r\n\t\u002734px\u0027\r\n)"
},
"type": "expr"
}
}
},
"props": {
"direction": "column",
"style": {
"gap": "4px",
"margin": "6px",
"overflow": "hidden",
"transition": "flex-basis 500ms"
}
},
"type": "ia.container.flex"
}
],
"meta": {
"name": "root"
},
"props": {
"direction": "column"
},
"type": "ia.container.flex"
}
}
This assumes folders some_category
and other_category
, each containing the views mainpage
, page_1
, page_2
and page_3
.
And also the styles button
and button_selected
, because I like highlighting the button that corresponds to the category and the page I'm in.
How it works:
The view itself has a custom property current_category
that uses the page.primaryView
property to determine what category the user is currently in.
Each category container has a category
custom prop.
Those 2 properties are compared in a binding on the mainpage
button's style classes, to selected the proper class.
The category containers have a binding on the basis
property to make it grow or shrink based on whether the currently displayed page matches the category. And as a bonus there's a transition
style prop to make it look nice.
Each button has a view
custom property that is the path it navigates to when clicked.
That property is also used in a binding to select a style class.
And finally each button has an onActionPerformed
navigation event to navigate to the view specified in the custom property.
This will need customization to match your needs but it's a start.
edit: forgot the json ;p
Can use this tree component for navigation in perspective and any idea how to set view?
There are a LOT of issues with the built in menu component. My team started using the tree component for navigation a few months back and it has never given us any issues.
Any idea how to use this?
Menu_2023-06-29_1126.zip (11.3 KB)
This is what we are using. There might be some stuff in there that you don't need.
@ToMakPo Thank you so much I will try.
Here's how I did it.
Figure 1. Do the Page Configuration.
Bind the tree's items
property to the text and apply a script transform on it:
def transform(self, value, quality, timestamp):
# Update the test tree with the structure.
# Anchors and path URLs to be provided in form shown below using '|' (vertical bar) as separator.
# A001 |/Home
# A001/B001 |/Area/1/Bath/1
# A001/B001/C001 |/Area/1/Bath/1/Clamp1
# 2023-06-29 Transistor
items = []
treeExpand = self.view.params.treeExpand
for line in value.splitlines():
if line.strip(): # Ignore empty lines.
if line[0] != "#": # Lines can be commented out with #.
current = items
branch, url = line.split('|')
for part in branch.strip().split("/"): # Strip off leading and trailing spaces.
folderExists = False # Check if the current folder exists in our items list.
for itemsPointerItem in current:
if part == itemsPointerItem['label']:
folderExists = True
current = itemsPointerItem['items']
if not folderExists:
item = {
"label": part,
"expanded": treeExpand,
"items": [],
"data": {"url": url.strip()}
}
current.append(item)
current = item['items']
return items
The selected item is available in selectionData.0.value.url
[
{
"itemPath": "3/0",
"value": {
"url": "/machine/Sealer03"
}
}
]
On the menu tree add an onItemClicked
event, Script:
def runAction(self, event):
pagePath = self.props.selectionData[0].value.url
system.perspective.navigate(pagePath)
Note that I've used a script to do the navigation. If found that if I used a navigation event that the navigation happened before selectionData[0].value.url
had been updated and so a second click was required each time.
Have fun!
@ToMakPo in Your example there is a property pageId within the page, im assuming this should be an integer. I am encountering the error.....
Error running kanoa/core/docks/navigation/menu (1)@D/root.onMessageReceived(self, payload): Traceback (most recent call last): File "function:onMessageReceived", line 14, in onMessageReceived TypeError: com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper$ArrayWrapper indices must be integers
Do you have any idea what might be affecting this?
Show us the script. Except for "You're trying to index an array with something that's not an int", there's not much we can say without the script.
@pascal.fragnoud It is the script that @ToMakPo uploaded in his attached zip. It is in update navigation event handler(within the root container). Which passes in a string- pageId, Into the array, which is causing the exception.
self.page.props.pageId
is not an int.
I suspect you created self.session.custom.pages
as an array, when it should really be a dict/object.
also, take a look at this:
def onMessageReceived(self, payload):
```
system.perspective.print("updateNavigation() msg rcvd with " + system.util.jsonEncode(payload))
# - implement your handler here
# - any navigation action will call this, ex: menu tree, home button, any links
# - this function adds new history items
# - if this function is called when currentIndex is not pointing to the end of history,
# delete every entry after the currentIndex, then add the new history item
# - afterwards, navigate to the newly added history item/view
if payload['type'] != 'url': #urls will be opended in a new tab so we don't need to store history
logger = system.util.getLogger('log1')
logger.info("this is page iD " + self.page.props.pageId)
pageId = self.page.props.pageId
if '/' in pageId: pageId = 'designer' #For debug in designer
pageNavigation = self.session.custom.pages[pageId].navigation
history = history = pageNavigation.history
currentIndex = pageNavigation.currentIndex
if currentIndex < len(history) - 1:
self.session.custom.pages[pageId].navigation.history = history[:currentIndex + 1]
if history[currentIndex]['path'] != payload['path']:
newHistoryItem = {
"path": payload['path'],
"name": payload['name'],
"type": payload['type'],
"params": system.util.jsonEncode(payload['params'])
}
self.session.custom.pages[pageId].navigation['history'].append(newHistoryItem)
if len(self.session.custom.pages[pageId].navigation["history"]) > 30:
self.session.custom.pages[pageId].navigation["history"] = self.session.custom.pages[pageId].navigation["history"][1:]
else:
self.session.custom.pages[pageId].navigation["currentIndex"] += 1
else:
return
self.navigateTo(payload['type'], payload['path'], payload['params'])
returnPreformatted text
I changed 'pages' from an array to a object and the error has gone(I had done this earlier today but had reverted it as again on line 14 I received this error, which I thought was a result of sending the wrong data . )
com.inductiveautomation.ignition.common.script.JythonExecException
Traceback (most recent call last):
File "<function:onMessageReceived>", line 18, in onMessageReceived
KeyError: 'designer'
at org.python.core.Py.KeyError(Py.java:220)
at com.inductiveautomation.perspective.gateway.script.PropertyTreeScriptWrapper$ObjectWrapper.__finditem__(PropertyTreeScriptWrapper.java:464)
at com.inductiveautomation.ignition.common.script.abc.AbstractJythonMap.__finditem__(AbstractJythonMap.java:54)
at org.python.core.PyObject.__getitem__(PyObject.java:717)
at org.python.pycode._pyx237.onMessageReceived$1(<function:onMessageReceived>:43)
at org.python.pycode._pyx237.call_function(<function:onMessageReceived>)
at org.python.core.PyTableCode.call(PyTableCode.java:173)
at org.python.core.PyBaseCode.call(PyBaseCode.java:306)
at org.python.core.PyFunction.function___call__(PyFunction.java:474)
at org.python.core.PyFunction.__call__(PyFunction.java:469)
at org.python.core.PyFunction.__call__(PyFunction.java:464)
at com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:847)
at com.inductiveautomation.ignition.common.script.ScriptManager.runFunction(ScriptManager.java:829)
at com.inductiveautomation.ignition.gateway.project.ProjectScriptLifecycle$TrackingProjectScriptManager.runFunction(ProjectScriptLifecycle.java:868)
at com.inductiveautomation.ignition.common.script.ScriptManager$ScriptFunctionImpl.invoke(ScriptManager.java:1010)
at com.inductiveautomation.ignition.gateway.project.ProjectScriptLifecycle$AutoRecompilingScriptFunction.invoke(ProjectScriptLifecycle.java:950)
at com.inductiveautomation.perspective.gateway.script.ScriptFunctionHelper.invoke(ScriptFunctionHelper.java:161)
at com.inductiveautomation.perspective.gateway.script.ScriptFunctionHelper.invoke(ScriptFunctionHelper.java:98)
at com.inductiveautomation.perspective.gateway.model.MessageHandlerCollection$MessageHandlerImpl$1.lambda$invoke$0(MessageHandlerCollection.java:86)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at com.inductiveautomation.perspective.gateway.threading.BlockingWork$BlockingWorkRunnable.run(BlockingWork.java:58)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: org.python.core.PyException
Traceback (most recent call last):
File "<function:onMessageReceived>", line 18, in onMessageReceived
KeyError: 'designer'
... 24 more
Ignition v8.1.39 (b2024040909)
Java: Azul Systems, Inc. 17.0.10
Do you have any idea why this would be thrown?
Well, clearly you don't have any entry under the 'designer'
key in the session.custom.pages
prop.
I couldn't tell you why, I have no idea what should add those entries.