MqttMonitor/MqttConnectionProfilesDialog.py

changeset 28
0f02baed8308
parent 26
ad232a5129cc
child 30
17ef10819773
equal deleted inserted replaced
27:aeb276d76ec7 28:0f02baed8308
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2018 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to edit the MQTT connection profiles.
8 """
9
10 from __future__ import unicode_literals
11
12 import collections
13
14 from PyQt5.QtCore import pyqtSlot, Qt, QUuid
15 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \
16 QListWidgetItem, QInputDialog, QLineEdit
17
18 from E5Gui import E5MessageBox
19 from E5Gui.E5PathPicker import E5PathPickerModes
20
21 from .Ui_MqttConnectionProfilesDialog import Ui_MqttConnectionProfilesDialog
22
23 import UI.PixmapCache
24 from Utilities.crypto import pwConvert
25
26
27 class MqttConnectionProfilesDialog(QDialog, Ui_MqttConnectionProfilesDialog):
28 """
29 Class implementing a dialog to edit the MQTT connection profiles.
30 """
31 def __init__(self, client, profiles, parent=None):
32 """
33 Constructor
34
35 @param client reference to the MQTT client object
36 @type MqttClient
37 @param profiles dictionary containing dictionaries containing the
38 connection parameters. Each entry must have the keys
39 "BrokerAddress", "BrokerPort", "ClientId",
40 "Keepalive", "CleanSession", "Username", "Password", "WillTopic",
41 "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert",
42 "TlsClientCert", "TlsClientKey".
43 @type dict
44 @param parent reference to the parent widget
45 @type QWidget
46 """
47 super(MqttConnectionProfilesDialog, self).__init__(parent)
48 self.setupUi(self)
49
50 self.__client = client
51
52 self.__profiles = collections.defaultdict(self.__defaultProfile)
53 self.__profiles.update(profiles)
54 self.__profilesChanged = False
55
56 self.plusButton.setIcon(UI.PixmapCache.getIcon("plus.png"))
57 self.copyButton.setIcon(UI.PixmapCache.getIcon("editCopy.png"))
58 self.minusButton.setIcon(UI.PixmapCache.getIcon("minus.png"))
59
60 self.tlsCertsFilePicker.setMode(E5PathPickerModes.OpenFileMode)
61 self.tlsCertsFilePicker.setFilters(
62 self.tr("Certificate Files (*.crt *.pem);;All Files (*)"))
63 self.tlsSelfSignedCertsFilePicker.setMode(
64 E5PathPickerModes.OpenFileMode)
65 self.tlsSelfSignedCertsFilePicker.setFilters(
66 self.tr("Certificate Files (*.crt *.pem);;All Files (*)"))
67 self.tlsSelfSignedClientCertFilePicker.setMode(
68 E5PathPickerModes.OpenFileMode)
69 self.tlsSelfSignedClientCertFilePicker.setFilters(
70 self.tr("Certificate Files (*.crt *.pem);;All Files (*)"))
71 self.tlsSelfSignedClientKeyFilePicker.setMode(
72 E5PathPickerModes.OpenFileMode)
73 self.tlsSelfSignedClientKeyFilePicker.setFilters(
74 self.tr("Key Files (*.key *.pem);;All Files (*)"))
75
76 self.profileTabWidget.setCurrentIndex(0)
77
78 if len(self.__profiles) == 0:
79 self.minusButton.setEnabled(False)
80 self.copyButton.setEnabled(False)
81
82 self.profileFrame.setEnabled(False)
83 self.__populatingProfile = False
84 self.__deletingProfile = False
85
86 self.__populateProfilesList()
87
88 @pyqtSlot(str)
89 def on_profileEdit_textChanged(self, name):
90 """
91 Private slot to handle changes of the profile name.
92
93 @param name name of the profile
94 @type str
95 """
96 self.__updateApplyButton()
97
98 @pyqtSlot(QAbstractButton)
99 def on_profileButtonBox_clicked(self, button):
100 """
101 Private slot handling presses of the profile buttons.
102
103 @param button reference to the pressed button
104 @type QAbstractButton
105 """
106 if button == self.profileButtonBox.button(QDialogButtonBox.Apply):
107 currentProfile = self.__applyProfile()
108 self.__populateProfilesList(currentProfile)
109
110 elif button == self.profileButtonBox.button(QDialogButtonBox.Reset):
111 self.__resetProfile()
112
113 elif button == self.profileButtonBox.button(
114 QDialogButtonBox.RestoreDefaults):
115 self.__populateProfileDefault()
116
117 @pyqtSlot(QListWidgetItem, QListWidgetItem)
118 def on_profilesList_currentItemChanged(self, current, previous):
119 """
120 Private slot to handle a change of the current profile.
121
122 @param current new current item
123 @type QListWidgetItem
124 @param previous previous current item
125 @type QListWidgetItem
126 """
127 self.minusButton.setEnabled(current is not None)
128 self.copyButton.setEnabled(current is not None)
129
130 if current is not previous:
131 if not self.__deletingProfile and self.__isChangedProfile():
132 # modified profile belongs to previous
133 yes = E5MessageBox.yesNo(
134 self,
135 self.tr("Changed Connection Profile"),
136 self.tr("""The current profile has unsaved changes."""
137 """ Shall these be saved?"""),
138 icon=E5MessageBox.Warning,
139 yesDefault=True)
140 if yes:
141 self.__applyProfile()
142
143 if current:
144 profileName = current.text()
145 self.__populateProfile(profileName)
146 else:
147 self.__clearProfile()
148
149 @pyqtSlot()
150 def on_plusButton_clicked(self):
151 """
152 Private slot to add a new empty profile entry.
153 """
154 profileName, ok = QInputDialog.getText(
155 self,
156 self.tr("New Connection Profile"),
157 self.tr("Enter name for the new Connection Profile:"),
158 QLineEdit.Normal)
159 if ok and bool(profileName):
160 if profileName in self.__profiles:
161 E5MessageBox.warning(
162 self,
163 self.tr("New Connection Profile"),
164 self.tr("""<p>A connection named <b>{0}</b> exists"""
165 """ already. Aborting...</p>""").format(
166 profileName))
167 else:
168 itm = QListWidgetItem(profileName, self.profilesList)
169 self.profilesList.setCurrentItem(itm)
170 self.brokerAddressEdit.setFocus(Qt.OtherFocusReason)
171
172 @pyqtSlot()
173 def on_copyButton_clicked(self):
174 """
175 Private slot to copy the selected profile entry.
176 """
177 itm = self.profilesList.currentItem()
178 if itm:
179 profileName = itm.text()
180 newProfileName, ok = QInputDialog.getText(
181 self,
182 self.tr("Copy Connection Profile"),
183 self.tr("Enter name for the copied Connection Profile:"),
184 QLineEdit.Normal)
185 if ok and bool(newProfileName):
186 if newProfileName in self.__profiles:
187 E5MessageBox.warning(
188 self,
189 self.tr("Copy Connection Profile"),
190 self.tr("""<p>A connection named <b>{0}</b> exists"""
191 """ already. Aborting...</p>""").format(
192 newProfileName))
193 else:
194 profile = self.__defaultProfile()
195 profile.update(self.__profiles[profileName])
196 self.__profiles[newProfileName] = profile
197
198 itm = QListWidgetItem(newProfileName, self.profilesList)
199 self.profilesList.setCurrentItem(itm)
200 self.brokerAddressEdit.setFocus(Qt.OtherFocusReason)
201
202 @pyqtSlot()
203 def on_minusButton_clicked(self):
204 """
205 Private slot to delete the selected entry.
206 """
207 itm = self.profilesList.currentItem()
208 if itm:
209 profileName = itm.text()
210 yes = E5MessageBox.yesNo(
211 self,
212 self.tr("Delete Connection Profile"),
213 self.tr("""<p>Shall the Connection Profile <b>{0}</b>"""
214 """ really be deleted?</p>""").format(profileName)
215 )
216 if yes:
217 self.__deletingProfile = True
218 del self.__profiles[profileName]
219 self.__profilesChanged = True
220 self.__populateProfilesList()
221 self.__deletingProfile = False
222
223 self.profilesList.setFocus(Qt.OtherFocusReason)
224
225 def getProfiles(self):
226 """
227 Public method to return a dictionary of profiles.
228
229 @return dictionary containing dictionaries containing the defined
230 connection profiles. Each entry have the keys "BrokerAddress",
231 "BrokerPort", "ClientId", "Keepalive", "CleanSession", "Username",
232 "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain",
233 "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey".
234 @rtype dict
235 """
236 profilesDict = {}
237 profilesDict.update(self.__profiles)
238 return profilesDict
239
240 def __applyProfile(self):
241 """
242 Private method to apply the entered data to the list of profiles.
243
244 @return name of the applied profile
245 @rtype str
246 """
247 profileName = self.profileEdit.text()
248 profile = {
249 "BrokerAddress": self.brokerAddressEdit.text(),
250 "BrokerPort": self.brokerPortSpinBox.value(),
251 "ClientId": self.clientIdEdit.text(),
252 "Keepalive": self.keepaliveSpinBox.value(),
253 "CleanSession": self.cleanSessionCheckBox.isChecked(),
254 "Username": self.usernameEdit.text(),
255 "Password": pwConvert(self.passwordEdit.text(), encode=True),
256 "WillTopic": self.willTopicEdit.text(),
257 "WillMessage": self.willMessageEdit.toPlainText(),
258 "WillQos": self.willQosSpinBox.value(),
259 "WillRetain": self.willRetainCheckBox.isChecked(),
260 "TlsEnable": self.tlsGroupBox.isChecked(),
261 "TlsCaCert": "",
262 "TlsClientCert": "",
263 "TlsClientKey": "",
264 }
265 if profile["TlsEnable"]:
266 if self.tlsCertsFileButton.isChecked():
267 profile["TlsCaCert"] = self.tlsCertsFilePicker.text()
268 elif self.tlsSelfSignedCertsButton.isChecked():
269 profile["TlsCaCert"] = \
270 self.tlsSelfSignedCertsFilePicker.text()
271 profile["TlsClientCert"] = \
272 self.tlsSelfSignedClientCertFilePicker.text()
273 profile["TlsClientKey"] = \
274 self.tlsSelfSignedClientKeyFilePicker.text()
275
276 self.__profiles[profileName] = profile
277 self.__profilesChanged = True
278
279 return profileName
280
281 def __defaultProfile(self):
282 """
283 Private method to populate non-existing profile items.
284
285 @return default dictionary entry
286 @rtype dict
287 """
288 defaultProfile = self.__client.defaultConnectionOptions()
289 defaultProfile["BrokerAddress"] = ""
290 if defaultProfile["TlsEnable"]:
291 defaultProfile["BrokerPort"] = 8883
292 else:
293 defaultProfile["BrokerPort"] = 1883
294
295 return defaultProfile
296
297 def __populateProfilesList(self, currentProfile=""):
298 """
299 Private method to populate the list of defined profiles.
300
301 @param currentProfile name of the current profile
302 @type str
303 """
304 if not currentProfile:
305 currentItem = self.profilesList.currentItem()
306 if currentItem:
307 currentProfile = currentItem.text()
308
309 self.profilesList.clear()
310 self.profilesList.addItems(sorted(self.__profiles.keys()))
311
312 if currentProfile:
313 items = self.profilesList.findItems(
314 currentProfile, Qt.MatchExactly)
315 if items:
316 self.profilesList.setCurrentItem(items[0])
317
318 if len(self.__profiles) == 0:
319 self.profileFrame.setEnabled(False)
320
321 def __populateProfile(self, profileName):
322 """
323 Private method to populate the profile data entry fields.
324
325 @param profileName name of the profile to get data from
326 @type str
327 """
328 profile = self.__defaultProfile()
329 if profileName:
330 profile.update(self.__profiles[profileName])
331
332 self.__populatingProfile = True
333 if profileName is not None:
334 self.profileEdit.setText(profileName)
335 self.brokerAddressEdit.setText(profile["BrokerAddress"])
336 self.brokerPortSpinBox.setValue(profile["BrokerPort"])
337 self.clientIdEdit.setText(profile["ClientId"])
338 self.keepaliveSpinBox.setValue(profile["Keepalive"])
339 self.cleanSessionCheckBox.setChecked(profile["CleanSession"])
340 self.usernameEdit.setText(profile["Username"])
341 self.passwordEdit.setText(pwConvert(profile["Password"], encode=False))
342 self.willTopicEdit.setText(profile["WillTopic"])
343 self.willMessageEdit.setPlainText(profile["WillMessage"])
344 self.willQosSpinBox.setValue(profile["WillQos"])
345 self.willRetainCheckBox.setChecked(profile["WillRetain"])
346 self.tlsGroupBox.setChecked(profile["TlsEnable"])
347 if profile["TlsCaCert"] and profile["TlsClientCert"]:
348 self.tlsSelfSignedCertsButton.setChecked(True)
349 self.tlsSelfSignedCertsFilePicker.setText(profile["TlsCaCert"])
350 self.tlsSelfSignedClientCertFilePicker.setText(
351 profile["TlsClientCert"])
352 self.tlsSelfSignedClientKeyFilePicker.setText(
353 profile["TlsClientKey"])
354 elif profile["TlsCaCert"]:
355 self.tlsCertsFileButton.setChecked(True)
356 self.tlsCertsFilePicker.setText(profile["TlsCaCert"])
357 else:
358 self.tlsDefaultCertsButton.setChecked(True)
359 self.__populatingProfile = False
360
361 self.profileFrame.setEnabled(True)
362 self.__updateApplyButton()
363
364 def __clearProfile(self):
365 """
366 Private method to clear the profile data entry fields.
367 """
368 self.__populatingProfile = True
369 self.profileEdit.setText("")
370 self.brokerAddressEdit.setText("")
371 self.brokerPortSpinBox.setValue(1883)
372 self.clientIdEdit.setText("")
373 self.keepaliveSpinBox.setValue(60)
374 self.cleanSessionCheckBox.setChecked(True)
375 self.usernameEdit.setText("")
376 self.passwordEdit.setText("")
377 self.willTopicEdit.setText("")
378 self.willMessageEdit.setPlainText("")
379 self.willQosSpinBox.setValue(0)
380 self.willRetainCheckBox.setChecked(False)
381 self.tlsGroupBox.setChecked(False)
382 self.tlsDefaultCertsButton.setChecked(True)
383 self.tlsCertsFileButton.setChecked(True)
384 self.tlsCertsFilePicker.setText("")
385 self.tlsSelfSignedCertsButton.setChecked(False)
386 self.tlsSelfSignedCertsFilePicker.setText("")
387 self.tlsSelfSignedClientCertFilePicker.setText("")
388 self.tlsSelfSignedClientKeyFilePicker.setText("")
389 self.__populatingProfile = False
390
391 self.profileFrame.setEnabled(False)
392 self.__updateApplyButton()
393
394 def __resetProfile(self):
395 """
396 Private method to reset the profile data entry fields to their stored
397 values.
398 """
399 profileName = self.profileEdit.text()
400 if profileName in self.__profiles:
401 self.__populateProfile(profileName)
402
403 def __populateProfileDefault(self):
404 """
405 Private method to populate the profile data entry fields with default
406 profile values.
407 """
408 self.__populateProfile(None)
409
410 def __isChangedProfile(self):
411 """
412 Private method to check, if the currently shown profile contains some
413 changed data.
414
415 @return flag indicating changed data
416 @type bool
417 """
418 profileName = self.profileEdit.text()
419 if profileName == "":
420 return False
421
422 elif profileName in self.__profiles:
423 profile = self.__defaultProfile()
424 profile.update(self.__profiles[profileName])
425 changed = (
426 self.brokerAddressEdit.text() != profile["BrokerAddress"] or
427 self.brokerPortSpinBox.value() != profile["BrokerPort"] or
428 self.clientIdEdit.text() != profile["ClientId"] or
429 self.keepaliveSpinBox.value() != profile["Keepalive"] or
430 self.cleanSessionCheckBox.isChecked() !=
431 profile["CleanSession"] or
432 self.usernameEdit.text() != profile["Username"] or
433 self.passwordEdit.text() !=
434 pwConvert(profile["Password"], encode=False) or
435 self.willTopicEdit.text() != profile["WillTopic"] or
436 self.willMessageEdit.toPlainText() != profile["WillMessage"] or
437 self.willQosSpinBox.value() != profile["WillQos"] or
438 self.willRetainCheckBox.isChecked() != profile["WillRetain"] or
439 self.tlsGroupBox.isChecked() != profile["TlsEnable"]
440 )
441 # check TLS stuff only, if not yet changed
442 if not changed:
443 if self.tlsCertsFileButton.isChecked():
444 changed |= (
445 self.tlsCertsFilePicker.text() != profile["TlsCaCert"]
446 )
447 elif self.tlsSelfSignedCertsButton.isChecked():
448 changed |= (
449 self.tlsSelfSignedCertsFilePicker.text() !=
450 profile["TlsCaCert"] or
451 self.tlsSelfSignedClientCertFilePicker.text() !=
452 profile["TlsClientCert"] or
453 self.tlsSelfSignedClientKeyFilePicker.text() !=
454 profile["TlsClientKey"]
455 )
456 return changed
457
458 else:
459 return True
460
461 def __updateApplyButton(self):
462 """
463 Private method to set the state of the Apply button.
464 """
465 # condition 1: profile name and broker address need to be given
466 enable = (bool(self.profileEdit.text()) and
467 bool(self.brokerAddressEdit.text()))
468
469 # condition 2: if client ID is empty, clean session must be selected
470 if not self.__populatingProfile:
471 if self.clientIdEdit.text() == "" and \
472 not self.cleanSessionCheckBox.isChecked():
473 enable = False
474 E5MessageBox.critical(
475 self,
476 self.tr("Invalid Connection Parameters"),
477 self.tr("An empty Client ID requires a clean session."))
478
479 if self.tlsGroupBox.isChecked():
480 if self.tlsCertsFileButton.isChecked():
481 # condition 3a: if CA certificates file shall be used, it must
482 # be given
483 enable &= bool(self.tlsCertsFilePicker.text())
484 elif self.tlsSelfSignedCertsButton.isChecked():
485 # condition 3b: if client certificates shall be used, all files
486 # must be given
487 enable &= (
488 bool(self.tlsSelfSignedCertsFilePicker.text()) and
489 bool(self.tlsSelfSignedClientCertFilePicker.text()) and
490 bool(self.tlsSelfSignedClientKeyFilePicker.text())
491 )
492
493 self.profileButtonBox.button(QDialogButtonBox.Apply).setEnabled(enable)
494
495 @pyqtSlot(str)
496 def on_brokerAddressEdit_textChanged(self, address):
497 """
498 Private slot handling a change of the broker address.
499
500 @param address broker address
501 @type str
502 """
503 self.__updateApplyButton()
504
505 @pyqtSlot()
506 def on_generateIdButton_clicked(self):
507 """
508 Private slot to generate a client ID.
509 """
510 uuid = QUuid.createUuid()
511 self.clientIdEdit.setText(uuid.toString(QUuid.WithoutBraces))
512
513 @pyqtSlot(str)
514 def on_clientIdEdit_textChanged(self, clientId):
515 """
516 Private slot handling a change of the client ID string.
517
518 @param clientId client ID
519 @type str
520 """
521 self.__updateApplyButton()
522
523 @pyqtSlot(bool)
524 def on_cleanSessionCheckBox_clicked(self, checked):
525 """
526 Private slot to handle a change of the clean session selection.
527
528 @param checked current state of the clean session selection
529 @type bool
530 """
531 self.__updateApplyButton()
532
533 @pyqtSlot(str)
534 def on_tlsCertsFilePicker_textChanged(self, path):
535 """
536 Private slot handling a change of the TLS CA certificates file.
537
538 @param path file path
539 @type str
540 """
541 self.__updateApplyButton()
542
543 @pyqtSlot(str)
544 def on_tlsSelfSignedCertsFilePicker_textChanged(self, path):
545 """
546 Private slot handling a change of the TLS CA certificates file.
547
548 @param path file path
549 @type str
550 """
551 self.__updateApplyButton()
552
553 @pyqtSlot(str)
554 def on_tlsSelfSignedClientCertFilePicker_textChanged(self, path):
555 """
556 Private slot handling a change of the TLS client certificate file.
557
558 @param path file path
559 @type str
560 """
561 self.__updateApplyButton()
562
563 @pyqtSlot(str)
564 def on_tlsSelfSignedClientKeyFilePicker_textChanged(self, path):
565 """
566 Private slot handling a change of the TLS client key file.
567
568 @param path file path
569 @type str
570 """
571 self.__updateApplyButton()
572
573 @pyqtSlot(bool)
574 def on_tlsGroupBox_toggled(self, checked):
575 """
576 Private slot handling the selection of TLS mode.
577
578 @param checked state of the selection
579 @type bool
580 """
581 if checked and self.brokerPortSpinBox.value() == 1883:
582 # port is still standard non-TLS port
583 yes = E5MessageBox.yesNo(
584 self,
585 self.tr("SSL/TLS Enabled"),
586 self.tr(
587 """Encrypted connection using SSL/TLS has been enabled."""
588 """ However, the broker port is still the default"""
589 """ unencrypted port (port 1883). Shall this be"""
590 """ changed?"""),
591 icon=E5MessageBox.Warning,
592 yesDefault=True)
593 if yes:
594 self.brokerPortSpinBox.setValue(8883)
595 elif not checked and self.brokerPortSpinBox.value() == 8883:
596 # port is still standard TLS port
597 yes = E5MessageBox.yesNo(
598 self,
599 self.tr("SSL/TLS Disabled"),
600 self.tr(
601 """Encrypted connection using SSL/TLS has been disabled."""
602 """ However, the broker port is still the default"""
603 """ encrypted port (port 8883). Shall this be"""
604 """ changed?"""),
605 icon=E5MessageBox.Warning,
606 yesDefault=True)
607 if yes:
608 self.brokerPortSpinBox.setValue(1883)
609
610 self.__updateApplyButton()
611
612 @pyqtSlot(bool)
613 def on_tlsDefaultCertsButton_toggled(self, checked):
614 """
615 Private slot handling the selection of using the default
616 certificates file.
617
618 @param checked state of the selection
619 @type bool
620 """
621 self.__updateApplyButton()
622
623 @pyqtSlot(bool)
624 def on_tlsCertsFileButton_toggled(self, checked):
625 """
626 Private slot handling the selection of using a non-default
627 certificates file.
628
629 @param checked state of the selection
630 @type bool
631 """
632 self.__updateApplyButton()
633
634 @pyqtSlot(bool)
635 def on_tlsSelfSignedCertsButton_toggled(self, checked):
636 """
637 Private slot handling the selection of using self signed
638 client certificate and key files.
639
640 @param checked state of the selection
641 @type bool
642 """
643 self.__updateApplyButton()
644
645 @pyqtSlot()
646 def reject(self):
647 """
648 Public slot to reject the dialog changes.
649 """
650 if self.__isChangedProfile():
651 button = E5MessageBox.warning(
652 self,
653 self.tr("Changed Connection Profile"),
654 self.tr("""The current profile has unsaved changes. Shall"""
655 """ these be saved?"""),
656 E5MessageBox.StandardButtons(
657 E5MessageBox.Discard |
658 E5MessageBox.Save),
659 E5MessageBox.Save)
660 if button == E5MessageBox.Save:
661 self.__applyProfile()
662 return
663
664 if self.__profilesChanged:
665 button = E5MessageBox.warning(
666 self,
667 self.tr("Changed Connection Profiles"),
668 self.tr("""The list of connection profiles has unsaved"""
669 """ changes."""),
670 E5MessageBox.StandardButtons(
671 E5MessageBox.Abort |
672 E5MessageBox.Discard |
673 E5MessageBox.Save),
674 E5MessageBox.Save)
675 if button == E5MessageBox.Save:
676 super(MqttConnectionProfilesDialog, self).accept()
677 return
678 elif button == E5MessageBox.Abort:
679 return
680
681 super(MqttConnectionProfilesDialog, self).reject()
682
683 @pyqtSlot()
684 def accept(self):
685 """
686 Public slot to accept the dialog.
687 """
688 if self.__isChangedProfile():
689 yes = E5MessageBox.yesNo(
690 self,
691 self.tr("Changed Connection Profile"),
692 self.tr("""The current profile has unsaved changes. Shall"""
693 """ these be saved?"""),
694 icon=E5MessageBox.Warning,
695 yesDefault=True)
696 if yes:
697 self.__applyProfile()
698
699 super(MqttConnectionProfilesDialog, self).accept()

eric ide

mercurial