How to Close a Perspective Popup Asynchronously After a Delay in Ignition?

Hello everyone,

I am working on an Ignition project using Perspective, and I want to create a toast notification using a popup. The popup should automatically close after a certain delay (e.g., 5 seconds) without blocking the main thread. Here's what I've tried so far:

  1. Opening the Popup: I'm using system.perspective.openPopup, and that works perfectly.
  2. Closing the Popup After Delay
  • tried using system.util.invokeAsynchronous to create a separate thread and used time.sleep to add a delay.
  • After the delay, I called system.perspective.closePopup to close the popup.

However, this doesn't seem to work as expected, but the closePopup function does not execute or fails silently. Here's the relevant part of my code:

import system
import time

class ToastManager:
    @staticmethod
    def open_toast(popup_id, view_path, params=None, duration=5):
        # Open the popup
        system.perspective.openPopup(popup_id, view_path, params)

        # Asynchronous task to close the popup after a delay
        def async_task():
            time.sleep(duration)  # Wait for the specified duration
            system.perspective.closePopup(popup_id)  # Attempt to close the popup

        system.util.invokeAsynchronous(async_task)
ToastManager.open_toast("myToastPopup", "PopupViews/MyToastView", {"message": "Hello, world!"}, 5)

The popup opens successfully, but the closePopup function does not work. If I call the async_task function directly it closes the popup with delay. But with invokeAsynchronous it does not close or throw any error.

Where I am getting wrong on this? Seems like the invoke invokeAsynchronous is not working. Where is this going wrong?

Platform
Ignition Platform 8.1.30 (b2023071408)

I only want to close the popup using project library scripting.

Questions

  1. Is it possible to call system.perspective.closePopup from an asynchronous thread using invokeAsynchronous?
  2. Do I need to use a different method to close the popup from a separate thread?
  3. Is there a recommended way to achieve this functionality in Ignition Perspective?

Sleep = bad.
Just add a timer on the view itself which closes itself. Set the time on load to a custom prop, and add a custom prop to get the difference in seconds between now and the first load prop and compare to your open seconds. If true, use an on change script to close the popup passing in "" as the view name

6 Likes

I like your idea but Can you tell me how to check the condition in every seconds? Do I need to set a CRON job to check the condition and close the popup?

In Other words how to set a timer in the view?

  • In Project Browser, select the root of your view.
  • In Perspective Property Editor scroll down to CUSTOM.
  • Create a new custom property startTime.
    • Create an expression binding on startTime with the expression, now(0). This will fire once when the popup opens and save the current timestamp in the custom property.
  • Create a second tag, timeout.
    • Add an expression binding,
      (now(1000) - {this.custom.startTime}) > 5000
      This will evaluate every 1000 ms and will return true when the popup has been open for 5000 ms.
  • Right-click on custom.timeout and select Edit Change Script. Add the script,
def valueChanged(self, previousValue, currentValue, origin, missedEvents):
	if currentValue.value:
		system.perspective.closePopup("")

Tips:
now(0) β†’ Runs once on load.
now() β†’ Runs at the default scan rate - typically 1000 ms.
now(4321) β†’ Runs every 4321 ms.

1 Like

Here's a crude example that changes label text, paste the JSON into a view.

{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "events": {
          "component": {
            "onActionPerformed": {
              "config": {
                "script": "\tself.getSibling(\"Label\").custom.textTime \u003d system.date.now()"
              },
              "scope": "G",
              "type": "script"
            }
          }
        },
        "meta": {
          "name": "Button"
        },
        "position": {
          "height": 34,
          "width": 80,
          "x": 133,
          "y": 140
        },
        "type": "ia.input.button"
      },
      {
        "custom": {
          "textTime": {
            "$": [
              "ts",
              192,
              1736096810562
            ],
            "$ts": 1736096810562
          }
        },
        "meta": {
          "name": "Label"
        },
        "position": {
          "height": 32,
          "width": 234,
          "x": 262,
          "y": 142
        },
        "propConfig": {
          "custom.textReset": {
            "binding": {
              "config": {
                "expression": "(secondsBetween({this.custom.textTime}, now()) \u003e\u003d 5)"
              },
              "type": "expr"
            },
            "onChange": {
              "enabled": null,
              "script": "\tif currentValue.value:\n\t\tself.props.text \u003d \u0027Text reset\u0027\n\telse:\n\t\tself.props.text \u003d \u0027Button was pushed\u0027"
            }
          }
        },
        "props": {
          "text": "Text reset"
        },
        "type": "ia.display.label"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}

This will work but I don't want to use tag browser for this operation due to architectural constraints. That's my last hope. I can use similar condition which You have given directly inside my class and check until the condition meets.

But I am more curious why the invokeAsynchronous does not execute the async_task in my above example? If I Call the async_task() directly it works. I mean it waits until the duration and closes the popup. But not with invokeAsynchronous. For toast implementation I don't want to make things complicate if the invokeAsynchronous works.

My solution does not use tags or the tag browser.

I see, I understood wrongly. But can you help me understand the previous reply please?

If you are referring to post #6, then I don't know. I've had no reason to use Toast or create a class for such a simple task. The expression bindings and change events are simple, built-in and use resources efficiently.

1 Like

I have implemented a class to enhance the code’s reusability. Although it is not a simple popup, we are utilizing popups for the toast functionality. Regrettably, there is no alternative to popups in Ignition. Nevertheless, I am employing toast to display information throughout the application. I have provided a snippet of my requirements for this question. In actuality, the requirements are quite intricate.

If you can see my first reply to your answer and help me understand why the invokeAsynchronous does not able to execute the async_task(). That will be helpful.

Did you see my example? While it isn't a popup it shows a timer example, instead of changing text you close the popup and instead of the button starting the timer it starts when you open the popup.