4.4.5. UI Implementation (cut_rad_hello/src/cut_rad_hello/applicationwindow.py)

4.4.5.1. Imports

Lines 3 and 10 change. We have added QProcess to handle the start of the hellocii application, and QEventLoop for special circumstance on closing of the application.

On line 10 we add the StdCmdsFacade class we have just programmed.

 1import time
 2
 3from PySide2.QtCore import QObject, QMetaObject, Slot, Signal, SIGNAL, SLOT, QTimer, QProcess, QEventLoop
 4from PySide2.QtWidgets import QMainWindow
 5from PySide2.QtGui import QWhatsThis
 6
 7from elt.cut.task import Task
 8from taurus.core.util.log import Logger, TraceIt
 9from taurus import Manager
10from cut_rad_hello.stdcmdsfacade import StdCmdsFacade
11from cut_rad_hello.mainwindow import Ui_MainWindow # WAF will automatically generated this

4.4.5.2. Initialization

We create a new object called self.server_facade. This is a an object from the StdCmdsFacade class. In order to properly construct it, we need to pass it the URI of the hellocii endpoint.

We have also added a new self.server_process object from the QProcess class. This will keep track of a process for us while offering Signal/Slots in order to do so.

 1def __init__(self):
 2    QMainWindow.__init__(self)
 3    Logger.__init__(self,name=qApp.applicationName())
 4    # Construction of UI
 5    self.ui = Ui_MainWindow()
 6    self.ui.setupUi(self)
 7    MY_SERVER_URL = qApp.get_command_line_options().host
 8    MY_SERVER_PORT = qApp.get_command_line_options().port
 9    uri = 'zpb.rr://' + str(MY_SERVER_URL) + ':' +\
10          str(MY_SERVER_PORT) + '/StdCmds'
11    self.server_facade = StdCmdsFacade(uri)
12    self.server_process = QProcess(self)
13    self._init_gui()

In the _init_gui method we have added a bit of customization to our GUI, and recommended, we put it in this method. We start the application by setting some default values. First of all, the self.ui.centralwidget (line 8) is set to be disabled. This will disable interaction of most of the GUI. Particularly useful when the facade is not connected to the server, which is the starting case.

We also disable the actionServerExit QAction, which automatically disables its entry in the Menu. This goes in the same line of thought as the previous change. You cannot execute an Exit command on a server you are not yet connected to.

On line 10, we connect the Signal self.server_facede.connectionChanged(bool) to Slot self.on_server_connection_changed(bool). This will handle the enabling and disabling of widgets and menu entries when the connection status is detected to have change.

On line 12 and 13, we connect started and finished Signals to slots we implement that handled again, enabling and disabled menu entries to avoid presenting inconsistent options to the user.

 1def _init_gui(self):
 2    '''
 3    In some cases, widgets are not ready for modification until
 4     __init__ is done. This can be workaround by invoking another
 5     method at the end of __init__.
 6
 7    '''
 8    self.ui.centralwidget.setEnabled(False)
 9    self.ui.actionServerExit.setEnabled(False)
10    QObject.connect( self.server_facade, SIGNAL('connectionChanged(bool)'),
11                     self, SLOT('on_server_connection_changed(bool)'))
12    self.server_process.started.connect(self.on_server_process_started)
13    self.server_process.finished.connect(self.on_server_process_finished)
14    QTimer.singleShot(250, lambda: self.on_actionServerConnect_triggered())
15    pass

At the end of this, we use QTimer to automatically call a slot when a timer has ended. This will start after our GUI has been created and shown to the user. We avoid starting the connection while initializing the GUI to present it promptly to the user.

4.4.5.3. closeEvent()

We override the usual behavior defined by Qt library by redefining this method by adding more instructions before invoking the parent class closeEvent. This will ensure that the normal behaviour is not lost (otherwise, the window will not close).

We start a new QEventLoop, and set a timeout of 1 second. At that point, the Event Loop will quit. When closing windows we reach a very particular condition, where we cannot keep a QTimer tied to this class, because it can be destroyed while closing the window. In fact, as usual, while destroying or closing windows, one should avoid the creation of object tied to the lifecycle of the very same object being destroyed.

We need the timer in order to give the on_actionServerDisconnect_triggered method to perform its duties, but still having a timeout for the operation. This gives us that.

You can see that we start another Event Loop by the name of the loop.exec_() method.

 1def closeEvent(self, event):
 2    '''
 3    Not to be confused with actionExit, this method is special. Instead
 4    of using signals to know when an application is closed, we can
 5    override the closeEvent method from the QApplication class, and
 6    determine here what will happen when the window is closed.
 7    In this case, we are just forwarding the event to its parent class.
 8    This is useful when disconnection from server or prevention of
 9    current work needs to be implemented.
10
11    :param event: Event received from Qt event pump
12    :type event: QtCore.QEvent
13
14    '''
15    self.info('Application shutting down...')
16    loop = QEventLoop()
17    self.server_facade.connectionChanged.connect(loop.quit)
18    self.on_actionServerDisconnect_triggered()
19    QTimer.singleShot(1*1000, lambda: loop.quit())
20    self.info('Waiting for Hellocii client to shutdown or timeout of 1 second...')
21    loop.exec_()
22    QMainWindow.closeEvent(self, event)

4.4.5.4. Server (dis)connection Slots

The first and second Slot in this code snippet reach to the Action defined in the UI. The first starts with the server_facade.setConnection(True) method call a request to connect to the hellocii server. The second one does the opposite. Both also take care of enable/disable certain widgets/action of the UI so that erroneous operations are avoided.

@TraceIt()
@Slot()
def on_actionServerConnect_triggered(self):
    '''
    Slot that auto-connects to the QAction actionServerConnect.
    actionServerConnect is not declared in the code, but in the mainwindow.ui.
    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
    '''
    self.server_facade.setConnection(True)
    self.ui.actionServerConnect.setEnabled(False)
    self.ui.actionServerDisconnect.setEnabled(True)
    self.ui.actionServerExit.setEnabled(True)

@Slot()
def on_actionServerDisconnect_triggered(self):
    '''
    Slot that auto-connects to the QAction actionServerConnect.
    actionServerConnect is not declared in the code, but in the mainwindow.ui.
    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
    '''
    self.server_facade.setConnection(False)
    self.ui.actionServerConnect.setEnabled(True)
    self.ui.actionServerDisconnect.setEnabled(False)
    self.ui.actionServerExit.setEnabled(False)


@TraceIt()
@Slot(bool)
def on_server_connection_changed(self, state):
    '''
    Slot that auto-connects to the QAction actionServerConnect.
    actionServerConnect is not declared in the code, but in the mainwindow.ui.
    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
    '''
    self.ui.centralwidget.setEnabled(state)
    self.ui.actionServerConnect.setEnabled(not state)
    self.ui.actionServerDisconnect.setEnabled(state)
    self.ui.actionServerExit.setEnabled(state)

The third method has been connected during the initialization to the connectionChanged Signal. This means that every time the connection state is True (connected) or False (disconnected), this Slot is called with that boolean. It then proceeds to set different state for Action/Widgets so that erroneous operations are avoided.

4.4.5.5. Slots for methods in the interface

There are 8 Slots that react to buttons being pressed, and one for an Action being triggered. They are quite simple, and just invoke the appropriate method in the server_facade object.

  1@TraceIt()
  2@Slot()
  3def on_initPushButton_clicked(self):
  4    '''
  5    Slot that auto-connects to the QPushButton actionServerConnect.
  6    actionServerConnect is not declared in the code, but in the mainwindow.ui.
  7    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
  8    '''
  9    self.server_facade.init()
 10
 11@TraceIt()
 12@Slot()
 13def on_resetPushButton_clicked(self):
 14    '''
 15    Slot that auto-connects to the QPushButton actionServerConnect.
 16    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 17    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 18    '''
 19    self.server_facade.reset()
 20
 21@TraceIt()
 22@Slot()
 23def on_enablePushButton_clicked(self):
 24    '''
 25    Slot that auto-connects to the QPushButton actionServerConnect.
 26    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 27    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 28    '''
 29    self.server_facade.enable()
 30
 31@TraceIt()
 32@Slot()
 33def on_disablePushButton_clicked(self):
 34    '''
 35    Slot that auto-connects to the QPushButton actionServerConnect.
 36    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 37    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 38    '''
 39    self.server_facade.disable()
 40    pass
 41
 42@TraceIt()
 43@Slot()
 44def on_getStatePushButton_clicked(self):
 45    '''
 46    Slot that auto-connects to the QPushButton actionServerConnect.
 47    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 48    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 49    '''
 50    self.server_facade.get_state(result_slot=lambda x: self.info("Return: {}".format(x)))
 51    pass
 52
 53@TraceIt()
 54@Slot()
 55def on_getStatusPushButton_clicked(self):
 56    '''
 57    Slot that auto-connects to the QPushButton actionServerConnect.
 58    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 59    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 60    '''
 61    self.server_facade.get_status(result_slot=lambda x: self.info("Return: {}".format(x)))
 62    pass
 63
 64@TraceIt()
 65@Slot()
 66def on_getVersionPushButton_clicked(self):
 67    '''
 68    Slot that auto-connects to the QPushButton actionServerConnect.
 69    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 70    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 71    '''
 72    self.server_facade.get_version(result_slot=lambda x: self.info("Return: {}".format(x)))
 73    pass
 74
 75@TraceIt()
 76@Slot()
 77def on_stopPushButton_clicked(self):
 78    '''
 79    Slot that auto-connects to the QPushButton actionServerConnect.
 80    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 81    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 82    '''
 83    self.server_facade.stop()
 84    pass
 85
 86@TraceIt()
 87@Slot()
 88def on_actionServerExit_triggered(self):
 89    '''
 90    Slot that auto-connects to the QAction actionServerConnect.
 91    actionServerConnect is not declared in the code, but in the mainwindow.ui.
 92    NOTE: This is different from others slots. It uses a QAction instead of a QPushButton
 93    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
 94    '''
 95    self.server_facade.exit()
 96    pass
 97
 98@TraceIt()
 99@Slot()
100def on_setLogLevelPushButton_clicked(self):
101    '''
102    Slot that auto-connects to the QPushButton actionServerConnect.
103    actionServerConnect is not declared in the code, but in the mainwindow.ui.
104    See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
105    '''
106    self.server_facade.set_log_level(
107        self.ui.levelComboBox.currentText(),
108        self.ui.loggerComboBox.currentText(),
109        result_slot=lambda x: self.info("Return: {}".format(x)))
110    pass

The on_actionServerExit_triggered is auto-connected to a QAction instead of a QPushButton.

on_setLogLevelPushButton_clicked is special, as it needs to collect arguments for the method. It is important to establish a line between UI and domain/middleware. Here is an example where it is drawn. This file tried to concern itself only with UI elements. We pass the level and logger as strings to the self.server_facade method.

4.4.5.6. Starting Hellocii process

This are auxiliary methods that will make our like more easy. The first one is an auto-connected slot, that react to a QAction (also located in the Top-Level Menubar Entry Server ‣ Start Server Process). This will start a process using the QProcess object we created during initialization.

Also during initialization, we have connected two signals from the QProcess object, to the last two Slots. When the process is detected to be started, and when it finishes, the slots will enable Actions so that erroneous operations are avoided.

 1@TraceIt()
 2@Slot()
 3def on_actionServerStart_triggered(self):
 4
 5    if (self.server_process.state() == QProcess.NotRunning):
 6        self.warning("Hellocii will be started up in a xfce4-terminal" )
 7        self.server_process.start("xfce4-terminal",["-x","hellocii","-c", "hellocii/config.yaml", "-l", "DEBUG"])
 8    else:
 9        self.warning("Hellocii already running" )
10
11@TraceIt()
12@Slot()
13def on_server_process_started(self):
14    self.ui.actionServerStart.setEnabled(False)
15    self.ui.actionServerExit.setEnabled(True)
16
17@TraceIt()
18def on_server_process_finished(self, code):
19    self.ui.actionServerStart.setEnabled(True)
20    pass