Author Topic: [SOLVED] How to make a reactive user interface  (Read 734 times)

I'm showing a progress bar while my script is generating a graph. Although, generating the graph take a longtime and I have 2 issues :
- the progress bar animation is not moving
- we can't click on "Cancel" on the progress dialog

To hack this, I tried to use:
Code: [Select]
QCoreApplication.processEvents()But it doesn't work very well and it's not advised.

After searching online, the advise I see everywhere is to run the long process on another thread. Although, the Substance documentation say that can't use the Substance API from another thread.

So what can I do ?
Last Edit: October 22, 2020, 08:27:04 am

Short of spawning a subprocess and using IPC, you don't have many, if any, options. The Qt gui loop and the sd functions need the main thread, if one takes a long running task, that's that.

One way to accomplish the IPC, is to use the QLocalServer and QLocalSocket that come with QtCore. The following was scratched out here and by no means will work out of the box, but it might get you started

Code: [Select]
# your sd plugin
import subprocess
from PySide2 import QtCore, QtWidgets

class MyServer(QtCore.QObject):

    connected = QtCore.Signal()

    def __init__(self):
        self._sockets = []
        self.wants_to_quit = False
        self._server = QtCore.QLocalServer(QtWidgets.QApplication.instance())
        self._server.listen("my-pipe") # Named pipe to communicate, might need to make unique if multiple servers
        self._server.newConnection.connect(self._new_connection)

    def _new_connection(self, socket):
        self._sockets.append(socket)
        socket.readyRead.connect(self._read_from_socket)
        self.connected.emit()

    def _read_from_socket(self, socket):
        message = socket.readAll().decode('utf-8')
        if message = 'CANCEL':
            self.wants_to_quit = True

    def write_to_sockets(self, message):
        for s in self._sockets:
            s.write(message)
            s.flush()

SOCKET_SERVER = MyServer()

# -- In the plugin, for some long running task
def long_running_task(self):

    # Use an event loop to wait while the process starts before running the task
    timer = QtCore.QTimer()
    timer.setInterval(3000) # 3 second timeout
    loop = QtCore.QEventLoop()
    SOCKET_SERVER.connected.connect(loop.quit)
    timer.timeout.connect(loop.quit)
    loop.quit.connect(timer.stop)

    # Finally, start the rocess
    subprocess.Popen(["python", "/path/to/progress/script.py", "my-pipe"])
    loop.exec_() # Run the event loop until connected

    # -- start work, periodically send a message of progress
    SOCKET_SERVER.write_to_sockets("progress:10")
    if SOCKET_SERVER.wants_to_quit:
        SOCKET_SERVER.deleteLater() # Deleting the qobject should clean up connections and sockets
        return


Then, in the client, you open a socket connection to that main process

Code: [Select]
# /path/to/progress/script.py
import sys
from PySide import QtCore, QtWidgets

app = QtWidgets.QApplication(sys.argv)
PIPE_NAME = sys.argv[-1]

class MyProgress(QtWidgets.QDialog):
    # ... Layout

    def process_message(self, message):
        message = message.decode('utf-8') # Comes in as bytes
        if 'progress' in message:
            self._progress = int(message.replace('progress:', '')

    def on_cancel_clicked(self):
        self.socket.write("CANCEL") # Send back to the server that we want to cancel
        self.socket.flush()
        self.reject() # Clean up this dialog, let the process die and clean up the environment

my_progress = MyProgress()
socket = QtCore.QLocalSocket(app)
my_progress.socket = socket

socket.connectToServer(PIPE_NAME)
if (socket.waitForConnected(1000)):
    # We're connected, we can receive information from the server now
    socket.readyRead.connect(my_process.process_message)

my_process.exec_() # Show out dialog

This looks nice ! Thanks a lot, I will give it a try  :)

Hi,

I think that in your long running code, you can call in the main thread from time to time
QCoreApplication.processEvents() and that should work ok. Personally, I usually call it in a loop 5 times or so
and that is usually enough to get some responsiveness. Also, make sure to call it frequently.

If you are doing most of the work in another thread, you can check the method here to perform actions on the main thread:
https://docs.substance3d.com/sddoc/using-threads-172825676.html

Est.

Hi,

I think that in your long running code, you can call in the main thread from time to time
QCoreApplication.processEvents() and that should work ok. Personally, I usually call it in a loop 5 times or so
and that is usually enough to get some responsiveness. Also, make sure to call it frequently.

If you are doing most of the work in another thread, you can check the method here to perform actions on the main thread:
https://docs.substance3d.com/sddoc/using-threads-172825676.html

Est.

Calling QCoreApplication.processEvents() is what I ended up doing and it's good enough. Thanks :)