When using system.gui.errorBox()
is there a way to give more information in the details tab? I'd like to have a general, simple error message with more details on the details tab for someone who might actually be able to fix the issue.
I've never used this. Is it gui blocking?
It's just like any other error box you've gotten when a script throws an error. It's not blocking.
Recently, I had to add a menu item to a popup in a project I was working on. Clicking on one of the existing menu items caused the standard error box to appear, and at first, I mistook it for a bug, but the information inside the popup revealed that the error box was intentional. I said to myself: "Huh, I wonder how they did that." Then, I did what I was there to do and never looked into it further.
...now I know.
Gui blocking popups are a pet peeve of mine, and it seems like most of the built in stuff is gui blocking, so I've gotten in the habit of using my own lightweight popup generator or using other methods of information delivery. I haven't looked at the built in system boxes in a long time.
I don't know how far you want to go with this, but looking through the docs, there doesn't appear to be a direct way. The system function returns nothing, and the only two arguments allowed are for the main error message and the title.
If you want to stick with the built in popup, giving it a custom title creates an easy way to identify and get the window. From there, anything can be done to it.
Example:
# I keep this in my script library. It's an easy way to dig out nested components
def getComponentOfClass(container, className):
for component in container.components:
if component.__class__.__name__ == className:
return component
else:
foundComponent = getComponentOfClass(component, className)
if foundComponent:
return foundComponent
# Create the error box using the built in function
from java.awt import Window
errorMessage = "Unable to turn on Compressor 12."
details = "You don't have proper security privileges."
system.gui.errorBox(errorMessage, 'Custom Title')
# Iterate through the windows and find the one with the custom title
for window in Window.getWindows():
# I always check visibility when I use this approach,
# ...because getWindows() seems to return quite a few disposed windows, and it's a waste of time to process or evaluate those.
# Not all windows will have a title property, so those also have to be filtered out
if window.visible and hasattr(window, 'title') and window.title == 'Custom Title':
tabbedErrorDisplay = getComponentOfClass(window, 'TabbedErrorDisplay')
tabbedErrorDisplay.setEnabledAt(1, True) # The tab is disabled by default
# Dig the text area out of the stack trace panel, and customize the text
stackTracePanel = tabbedErrorDisplay.getComponentAt(1)
textArea = getComponentOfClass(stackTracePanel, 'JTextArea')
textArea.text = details
break
Result:
wow, that's alot of extra work.... I'll just add a couple <br>
and put the details below
nice work though!
errorBox
is just a thin wrapper around our underlying abstraction, ErrorUtil
.
ErrorUtil
has a lot of functionality packed in.
The "proper" way to show details is to have some kind of exception. Phil's got a Python to Java exception adapter script that would allow you to trigger a "native" Python exception and display it in ErrorUtil as proper "details" with a stacktrace.
Nice work justinedwards.jle and thanks for sharing!
I've just added a function to my utilities library to implement detailed error boxes based on your script.
I added the following lines after getting the text area to help with formatting the detail text. This turns on word wrapping and sets it to do line breaks between whole words instead of characters.
I'm also casting the detail text to unicode in the function because it was mangling my apostrophes.
textArea.setLineWrap(True)
textArea.setWrapStyleWord(True)
textArea.text = unicode(details)
Ok I went down a rabbit hole after I realized that the compact / full options and the copy options on the Details pane do not work when using this method.
So here's a complete version that keeps those controls from breaking.
def show_detailed_error_box(message, title, details):
from java.awt import Window
from com.inductiveautomation.ignition.client.util.gui.errors import DisplayableError
from java.lang import Throwable
# Function for finding a component of a a class. Thanks to forum user justinedwards.jle
def getComponentOfClass(container, className):
for component in container.components:
if component.__class__.__name__ == className:
return component
else:
foundComponent = getComponentOfClass(component, className)
if foundComponent:
return foundComponent
# Display the error box. Then we can add the details.
system.gui.errorBox(message, title)
for window in Window.getWindows():
if window.visible and hasattr(window, "title") and window.title == title:
# Locate the tabbedErrorDisplay of the window and enable the Details pane
tabbedErrorDisplay = getComponentOfClass(window, "TabbedErrorDisplay")
tabbedErrorDisplay.setEnabledAt(1, True)
displayableError = DisplayableError(message, title, Throwable(unicode(details))
tabbedErrorDisplay.setError(displayableError)
break
Cool. It's neat that we can make our own exception popups that aren't gui blocking, but even when I was testing this, and I knew the popup was going to intentionally occur, it still struck me as a goof up every time due to years of associating that style popup with broken code. I have difficulty imagining scenarios where I would use this in production even with the proper method that Paul recommended.
When it comes to user errors such as bad or duplicate inputs, I often times don't even bother the user with a popup message. I'll make the text field some other color with a label that displays the issue, or I'll display a little info icon that the user can click for more info if he or she can't tell what has been done wrong.
When I display additional information in a popup, I typically create my own simple JDialog window, and I like to add a listener to it, so it will automatically dispose of itself the moment it loses focus.
Example:
It's simple, elegant, and it doesn't get in the way of whatever the user is trying to do.
Example code... please?
Generic Library Script:
from com.inductiveautomation.ignition.client.images import ImageLoader
from java.awt import BorderLayout, MouseInfo
from java.awt.event import WindowFocusListener
from javax.swing import JDialog, JLabel
'''
This function constucts a lightweight popup that:
• does not lock the gui thread.
• will appear directly under the mouse cursor, unless that positioning would result an part or all of the popup being outside of its desktop window.
• will dispose of itself when focus is lost.
'''
def setLightWeightPopup(event, headerTitle, iconPath, popupText, width, height):
# Define a class that implements WindowFocusListener to handle focus events
class FocusHandler(WindowFocusListener):
def windowGainedFocus(self, event):
pass # Do nothing when the window gains focus
def windowLostFocus(self, event):
event.getWindow().dispose() # Dispose of the window when it loses focus
# set the header for the popup
dialog = JDialog(None, headerTitle)
# Add the focus handler to the dialog
dialog.addWindowFocusListener(FocusHandler())
if iconPath:
# Create a custom icon for the dialog
icon = ImageLoader.getInstance().loadImage(iconPath)
# Add the custom icon to the header bar of the dialog
dialog.setIconImage(icon)
# Add the internal message to a label, and configure the popup
label = JLabel(popupText, JLabel.LEFT)
dialog.setLayout(BorderLayout())
dialog.add(label, BorderLayout.NORTH)
dialog.setSize(width, height)
# Retrieve the desktop window
desktop = system.gui.getParentWindow(event)
# Define an offset margin to be used for positioning calculations.
offsetMargine = 100
# Get the current location of the mouse pointer.
location = MouseInfo.getPointerInfo().location
# Calculate the default X and Y coordinates for the dialog, offset by the margin.
defaultX = location.x - offsetMargine
defaultY = location.y - offsetMargine
# Calculate the minimum and maximum X and Y coordinates where the dialog can be positioned,
# based on the desktop's location and size, adjusted by the offset margin.
minX = desktop.locationOnScreen.x + offsetMargine
maxX = desktop.locationOnScreen.x + desktop.width - offsetMargine
minY = desktop.locationOnScreen.y + offsetMargine
maxY = desktop.locationOnScreen.y + desktop.height - offsetMargine
# Determine the X coordinate for the dialog.
# Ensure it is within the bounds of the desktop, adjusting if it's too far left or right.
if defaultX < minX:
dialogX = minX
elif defaultX + width > maxX:
dialogX = maxX - width
else:
dialogX = defaultX
# Determine the Y coordinate for the dialog.
# Ensure it is within the bounds of the desktop, adjusting if it's too high or low.
if defaultY < minY:
dialogY = minY
elif defaultY + height > maxY:
dialogY = maxY - height
else:
dialogY = defaultY
# Set the location of the dialog to the calculated X and Y coordinates.
dialog.setLocation(dialogX, dialogY)
# Show the popup
dialog.visible = True
Example Call:
headerTitle = 'TroubleShooting Assistant'
iconPath = 'Builtin/icons/48/wrench.png'
popupText = "<html>Nice try, but we're looking for something something a little more... email-ish."
# Calculate or predetermine the dimensions in some way
width = 300
height = 100
libraryScripts.setLightWeightPopup(event, headerTitle, iconPath, popupText, width, height)