MqttMonitor/MqttConnectionProfilesDialog.py

Thu, 06 Sep 2018 19:35:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 06 Sep 2018 19:35:43 +0200
branch
connection_profiles
changeset 23
0b23bd856e43
parent 19
889a7c3c0e63
child 26
ad232a5129cc
permissions
-rw-r--r--

MqttConnectionProfilesDialog: finished implementing the connections profile dialog.

# -*- coding: utf-8 -*-

# Copyright (c) 2018 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to edit the MQTT connection profiles.
"""

from __future__ import unicode_literals

import collections

from PyQt5.QtCore import pyqtSlot, Qt, QUuid
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
    QListWidgetItem, QInputDialog, QLineEdit

from E5Gui import E5MessageBox

from .Ui_MqttConnectionProfilesDialog import Ui_MqttConnectionProfilesDialog

import UI.PixmapCache
from Utilities.crypto import pwConvert


class MqttConnectionProfilesDialog(QDialog, Ui_MqttConnectionProfilesDialog):
    """
    Class implementing a dialog to edit the MQTT connection profiles.
    """
    def __init__(self, client, profiles, parent=None):
        """
        Constructor
        
        @param client reference to the MQTT client object
        @type MqttClient
        @param profiles dictionary containing dictionaries containing the
            connection parameters. Each entry must have the keys
            "BrokerAddress", "BrokerPort", "ClientId",
            "Keepalive", "CleanSession", "Username", "Password", "WillTopic",
            "WillMessage", "WillQos", "WillRetain".
        @type dict
        @param parent reference to the parent widget
        @type QWidget
        """
        super(MqttConnectionProfilesDialog, self).__init__(parent)
        self.setupUi(self)
        
        self.__client = client
        
        self.__profiles = collections.defaultdict(self.__defaultProfile)
        self.__profiles.update(profiles)
        self.__profilesChanged = False
        
        self.plusButton.setIcon(UI.PixmapCache.getIcon("plus.png"))
        self.minusButton.setIcon(UI.PixmapCache.getIcon("minus.png"))
        
        self.profileTabWidget.setCurrentIndex(0)
        
        if len(self.__profiles) == 0:
            self.minusButton.setEnabled(False)
        
        self.profileFrame.setEnabled(False)
        self.__populatingProfile = False
        self.__deletingProfile = False
        
        self.__populateProfilesList()
    
    @pyqtSlot(str)
    def on_profileEdit_textChanged(self, name):
        """
        Private slot to handle changes of the profile name.
        
        @param name name of the profile
        @type str
        """
        self.__updateApplyButton()
    
    @pyqtSlot(QAbstractButton)
    def on_profileButtonBox_clicked(self, button):
        """
        Private slot handling presses of the profile buttons.
        
        @param button reference to the pressed button
        @type QAbstractButton
        """
        if button == self.profileButtonBox.button(QDialogButtonBox.Apply):
            currentProfile = self.__applyProfile()
            self.__populateProfilesList(currentProfile)
        
        elif button == self.profileButtonBox.button(QDialogButtonBox.Reset):
            self.__resetProfile()
        
        elif button == self.profileButtonBox.button(
                QDialogButtonBox.RestoreDefaults):
            self.__populateProfileDefault()
    
    @pyqtSlot(QListWidgetItem, QListWidgetItem)
    def on_profilesList_currentItemChanged(self, current, previous):
        """
        Private slot to handle a change of the current profile.
        
        @param current new current item
        @type QListWidgetItem
        @param previous previous current item
        @type QListWidgetItem
        """
        self.minusButton.setEnabled(current is not None)
        
        if current is not previous:
            if not self.__deletingProfile and self.__isChangedProfile():
                # modified profile belongs to previous
                yes = E5MessageBox.yesNo(
                    self,
                    self.tr("Changed Connection Profile"),
                    self.tr("""The current profile has unsaved changes."""
                            """ Shall these be saved?"""),
                    icon=E5MessageBox.Warning,
                    yesDefault=True)
                if yes:
                    self.__applyProfile()
        
        if current:
            profileName = current.text()
            self.__populateProfile(profileName)
        else:
            self.__clearProfile()
    
    @pyqtSlot()
    def on_plusButton_clicked(self):
        """
        Private slot to add a new empty profile entry.
        """
        profileName, ok = QInputDialog.getText(
            self,
            self.tr("New Connection Profile"),
            self.tr("Enter name for the new Connection Profile:"),
            QLineEdit.Normal)
        if ok and bool(profileName) and profileName not in self.__profiles:
            itm = QListWidgetItem(profileName, self.profilesList)
            self.profilesList.setCurrentItem(itm)
            self.brokerAddressEdit.setFocus(Qt.OtherFocusReason)
    
    @pyqtSlot()
    def on_minusButton_clicked(self):
        """
        Private slot to delete the selected entry.
        """
        itm = self.profilesList.currentItem()
        if itm:
            profileName = itm.text()
            yes = E5MessageBox.yesNo(
                self,
                self.tr("Delete Connection Profile"),
                self.tr("""<p>Shall the Connection Profile <b>{0}</b>"""
                        """ really be deleted?</p>""").format(profileName)
            )
            if yes:
                self.__deletingProfile = True
                del self.__profiles[profileName]
                self.__profilesChanged = True
                self.__populateProfilesList()
                self.__deletingProfile = False
        
        self.profilesList.setFocus(Qt.OtherFocusReason)
    
    def getProfiles(self):
        """
        Public method to return a dictionary of profiles.
        
        @return dictionary containing dictionaries containing the defined
            connection profiles. Each entry have the keys "BrokerAddress",
            "BrokerPort", "ClientId", "Keepalive", "CleanSession", "Username",
            "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain".
        @rtype dict
        """
        profilesDict = {}
        profilesDict.update(self.__profiles)
        return profilesDict
    
    def __applyProfile(self):
        """
        Private method to apply the entered data to the list of profiles.
        
        @return name of the applied profile
        @rtype str
        """
        profileName = self.profileEdit.text()
        profile = {
            "BrokerAddress": self.brokerAddressEdit.text(),
            "BrokerPort": self.brokerPortSpinBox.value(),
            "ClientId": self.clientIdEdit.text(),
            "Keepalive": self.keepaliveSpinBox.value(),
            "CleanSession": self.cleanSessionCheckBox.isChecked(),
            "Username": self.usernameEdit.text(),
            "Password": pwConvert(self.passwordEdit.text(), encode=True),
            "WillTopic": self.willTopicEdit.text(),
            "WillMessage": self.willMessageEdit.toPlainText(),
            "WillQos": self.willQosSpinBox.value(),
            "WillRetain": self.willRetainCheckBox.isChecked(),
        }
        self.__profiles[profileName] = profile
        self.__profilesChanged = True
        
        return profileName
    
    def __defaultProfile(self):
        """
        Private method to populate non-existing profile items.
        
        @return default dictionary entry
        @rtype dict
        """
        defaultProfile = self.__client.defaultConnectionOptions()
        defaultProfile["BrokerAddress"] = ""
        defaultProfile["BrokerPort"] = 1883
        
        return defaultProfile
    
    def __populateProfilesList(self, currentProfile=""):
        """
        Private method to populate the list of defined profiles.
        
        @param currentProfile name of the current profile
        @type str
        """
        if not currentProfile:
            currentItem = self.profilesList.currentItem()
            if currentItem:
                currentProfile = currentItem.text()
        
        self.profilesList.clear()
        self.profilesList.addItems(sorted(self.__profiles.keys()))
        
        if currentProfile:
            items = self.profilesList.findItems(
                currentProfile, Qt.MatchExactly)
            if items:
                self.profilesList.setCurrentItem(items[0])
        
        if len(self.__profiles) == 0:
            self.profileFrame.setEnabled(False)
    
    def __populateProfile(self, profileName):
        """
        Private method to populate the profile data entry fields.
        
        @param profileName name of the profile to get data from
        @type str
        """
        if profileName:
            profile = self.__profiles[profileName]
        else:
            profile = self.__defaultProfile()
        
        self.__populatingProfile = True
        if profileName is not None:
            self.profileEdit.setText(profileName)
        self.brokerAddressEdit.setText(profile["BrokerAddress"])
        self.brokerPortSpinBox.setValue(profile["BrokerPort"])
        self.clientIdEdit.setText(profile["ClientId"])
        self.keepaliveSpinBox.setValue(profile["Keepalive"])
        self.cleanSessionCheckBox.setChecked(profile["CleanSession"])
        self.usernameEdit.setText(profile["Username"])
        self.passwordEdit.setText(pwConvert(profile["Password"], encode=False))
        self.willTopicEdit.setText(profile["WillTopic"])
        self.willMessageEdit.setPlainText(profile["WillMessage"])
        self.willQosSpinBox.setValue(profile["WillQos"])
        self.willRetainCheckBox.setChecked(profile["WillRetain"])
        self.__populatingProfile = False
        
        self.profileFrame.setEnabled(True)
        self.__updateApplyButton()
    
    def __clearProfile(self):
        """
        Private method to clear the profile data entry fields.
        """
        self.__populatingProfile = True
        self.profileEdit.setText("")
        self.brokerAddressEdit.setText("")
        self.brokerPortSpinBox.setValue(1883)
        self.clientIdEdit.setText("")
        self.keepaliveSpinBox.setValue(60)
        self.cleanSessionCheckBox.setChecked(True)
        self.usernameEdit.setText("")
        self.passwordEdit.setText("")
        self.willTopicEdit.setText("")
        self.willMessageEdit.setPlainText("")
        self.willQosSpinBox.setValue(0)
        self.willRetainCheckBox.setChecked(False)
        self.__populatingProfile = False
        
        self.profileFrame.setEnabled(False)
        self.__updateApplyButton()
    
    def __resetProfile(self):
        """
        Private method to reset the profile data entry fields to their stored
        values.
        """
        profileName = self.profileEdit.text()
        if profileName in self.__profiles:
            self.__populateProfile(profileName)
    
    def __populateProfileDefault(self):
        """
        Private method to populate the profile data entry fields with default
        profile values.
        """
        self.__populateProfile(None)
    
    def __isChangedProfile(self):
        """
        Private method to check, if the currently shown profile contains some
        changed data.
        
        @return flag indicating changed data
        @type bool
        """
        profileName = self.profileEdit.text()
        if profileName == "":
            return False
        
        elif profileName in self.__profiles:
            profile = self.__profiles[profileName]
            return (
                self.brokerAddressEdit.text() != profile["BrokerAddress"] or
                self.brokerPortSpinBox.value() != profile["BrokerPort"] or
                self.clientIdEdit.text() != profile["ClientId"] or
                self.keepaliveSpinBox.value() != profile["Keepalive"] or
                self.cleanSessionCheckBox.isChecked() !=
                profile["CleanSession"] or
                self.usernameEdit.text() != profile["Username"] or
                self.passwordEdit.text() !=
                pwConvert(profile["Password"], encode=False) or
                self.willTopicEdit.text() != profile["WillTopic"] or
                self.willMessageEdit.toPlainText() != profile["WillMessage"] or
                self.willQosSpinBox.value() != profile["WillQos"] or
                self.willRetainCheckBox.isChecked() != profile["WillRetain"]
            )
        
        else:
            return True
    
    def __updateApplyButton(self):
        """
        Private method to set the state of the Apply button.
        """
        # condition 1: profile name and broker address need to be given
        enable = (bool(self.profileEdit.text()) and
                  bool(self.brokerAddressEdit.text()))
        
        # condition 2: if client ID is empty, clean session must be selected
        if not self.__populatingProfile:
            if self.clientIdEdit.text() == "" and \
                    not self.cleanSessionCheckBox.isChecked():
                enable = False
                E5MessageBox.critical(
                    self,
                    self.tr("Invalid Connection Parameters"),
                    self.tr("An empty Client ID requires a clean session."))
        
        self.profileButtonBox.button(QDialogButtonBox.Apply).setEnabled(enable)
    
    @pyqtSlot(str)
    def on_brokerAddressEdit_textChanged(self, address):
        """
        Private slot handling a change of the broker address.
        
        @param address broker address
        @type str
        """
        self.__updateApplyButton()
    
    @pyqtSlot()
    def on_generateIdButton_clicked(self):
        """
        Private slot to generate a client ID.
        """
        uuid = QUuid.createUuid()
        self.clientIdEdit.setText(uuid.toString(QUuid.WithoutBraces))
    
    @pyqtSlot()
    def reject(self):
        """
        Public slot to reject the dialog changes.
        """
        if self.__isChangedProfile():
            button = E5MessageBox.warning(
                self,
                self.tr("Changed Connection Profile"),
                self.tr("""The current profile has unsaved changes. Shall"""
                        """ these be saved?"""),
                E5MessageBox.StandardButtons(
                    E5MessageBox.Discard |
                    E5MessageBox.Save),
                E5MessageBox.Save)
            if button == E5MessageBox.Save:
                self.__applyProfile()
                return
        
        if self.__profilesChanged:
            button = E5MessageBox.warning(
                self,
                self.tr("Changed Connection Profiles"),
                self.tr("""The list of connection profiles has unsaved"""
                        """ changes."""),
                E5MessageBox.StandardButtons(
                    E5MessageBox.Abort |
                    E5MessageBox.Discard |
                    E5MessageBox.Save),
                E5MessageBox.Save)
            if button == E5MessageBox.Save:
                super(MqttConnectionProfilesDialog, self).accept()
                return
            elif button == E5MessageBox.Abort:
                return
        
        super(MqttConnectionProfilesDialog, self).reject()
    
    @pyqtSlot()
    def accept(self):
        """
        Public slot to accept the dialog.
        """
        if self.__isChangedProfile():
            yes = E5MessageBox.yesNo(
                self,
                self.tr("Changed Connection Profile"),
                self.tr("""The current profile has unsaved changes. Shall"""
                        """ these be saved?"""),
                icon=E5MessageBox.Warning,
                yesDefault=True)
            if yes:
                self.__applyProfile()
        
        super(MqttConnectionProfilesDialog, self).accept()
    
    @pyqtSlot(str)
    def on_clientIdEdit_textChanged(self, clientId):
        """
        Private slot handling a change of the client ID string.
        
        @param clientId client ID
        @type str
        """
        self.__updateApplyButton()
    
    @pyqtSlot(bool)
    def on_cleanSessionCheckBox_clicked(self, checked):
        """
        Private slot to handle a change of the clean session selection.
        
        @param checked current state of the clean session selection
        @type bool
        """
        self.__updateApplyButton()

eric ide

mercurial