p3lib 1.1.108__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- p3lib/__init__.py +0 -0
- p3lib/ate.py +108 -0
- p3lib/bokeh_auth.py +363 -0
- p3lib/bokeh_gui.py +845 -0
- p3lib/boot_manager.py +420 -0
- p3lib/conduit.py +145 -0
- p3lib/database_if.py +289 -0
- p3lib/file_io.py +154 -0
- p3lib/gnome_desktop_app.py +146 -0
- p3lib/helper.py +420 -0
- p3lib/json_networking.py +239 -0
- p3lib/login.html +98 -0
- p3lib/mqtt_rpc.py +240 -0
- p3lib/netif.py +226 -0
- p3lib/netplotly.py +223 -0
- p3lib/ngt.py +841 -0
- p3lib/pconfig.py +874 -0
- p3lib/ssh.py +935 -0
- p3lib/table_plot.py +675 -0
- p3lib/uio.py +574 -0
- p3lib-1.1.108.dist-info/LICENSE +21 -0
- p3lib-1.1.108.dist-info/METADATA +34 -0
- p3lib-1.1.108.dist-info/RECORD +24 -0
- p3lib-1.1.108.dist-info/WHEEL +4 -0
p3lib/ngt.py
ADDED
@@ -0,0 +1,841 @@
|
|
1
|
+
# !/usr/bin/env python3
|
2
|
+
|
3
|
+
"""NiceGui Tools
|
4
|
+
Responsible for providing helper classes for nicegui interfaces
|
5
|
+
aimed at reducing coding required for a GUI.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import traceback
|
9
|
+
import os
|
10
|
+
import platform
|
11
|
+
|
12
|
+
from time import sleep
|
13
|
+
from queue import Queue
|
14
|
+
from time import time, strftime, localtime
|
15
|
+
|
16
|
+
from nicegui import ui
|
17
|
+
|
18
|
+
class TabbedNiceGui(object):
|
19
|
+
"""@brief Responsible for starting the providing a tabbed GUI.
|
20
|
+
This class is designed to ease the creation of a tabbed GUI interface.
|
21
|
+
The contents of each tab can be defined in the subclass.
|
22
|
+
The GUI includes a message log area below each tab. Tasks can send messages
|
23
|
+
to this log area.
|
24
|
+
If a subclass sets the self._logFile attributed then all messages sent to the
|
25
|
+
log area are written to a log file with timestamps."""
|
26
|
+
|
27
|
+
# This can be used in the markdown text for a TAB description to give slightly larger text
|
28
|
+
# than normal.
|
29
|
+
DESCRIP_STYLE = '<span style="font-size:1.2em;">'
|
30
|
+
ENABLE_BUTTONS = "ENABLE_BUTTONS"
|
31
|
+
NOTIFY_DIALOG = "NOTIFY_DIALOG"
|
32
|
+
UPDATE_SECONDS = "UPDATE_SECONDS"
|
33
|
+
INFO_MESSAGE = "INFO: "
|
34
|
+
WARN_MESSAGE = "WARN: "
|
35
|
+
ERROR_MESSAGE = "ERROR: "
|
36
|
+
DEBUG_MESSAGE = "DEBUG: "
|
37
|
+
MAX_PROGRESS_VALUE = 100
|
38
|
+
DEFAULT_SERVER_PORT = 9812
|
39
|
+
GUI_TIMER_SECONDS = 0.1
|
40
|
+
PROGRESS_TIMER_SECONDS = 1.0
|
41
|
+
UPDATE_SECONDS = "UPDATE_SECONDS"
|
42
|
+
DEFAULT_GUI_RESPONSE_TIMEOUT= 30.0
|
43
|
+
POETRY_CONFIG_FILE = "pyproject.toml"
|
44
|
+
LOCAL_PATH = os.path.dirname(os.path.abspath(__file__))
|
45
|
+
|
46
|
+
@staticmethod
|
47
|
+
def GetDateTimeStamp():
|
48
|
+
"""@return The log file date/time stamp """
|
49
|
+
return strftime("%Y%m%d%H%M%S", localtime()).lower()
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
def GetInstallFolder():
|
53
|
+
"""@return The folder where the apps are installed."""
|
54
|
+
installFolder = os.path.dirname(__file__)
|
55
|
+
if not os.path.isdir(installFolder):
|
56
|
+
raise Exception(f"{installFolder} folder not found.")
|
57
|
+
return installFolder
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def GetLogFileName(logFilePrefix):
|
61
|
+
"""@param logFilePrefix The text in the log file name before the timestamp.
|
62
|
+
@return The name of the logfile including datetime stamp."""
|
63
|
+
dateTimeStamp = TabbedNiceGui.GetDateTimeStamp()
|
64
|
+
logFileName = f"{logFilePrefix}_{dateTimeStamp}.log"
|
65
|
+
return logFileName
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
def CheckPort(port):
|
69
|
+
"""@brief Check the server port.
|
70
|
+
@param port The server port."""
|
71
|
+
if port < 1024:
|
72
|
+
raise Exception("The minimum TCP port that you can bind the GUI server to is 1024.")
|
73
|
+
if port > 65535:
|
74
|
+
raise Exception("The maximum TCP port that you can bind the GUI server to is 65535.")
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
def GetProgramVersion():
|
78
|
+
"""@brief Get the program version from the poetry pyproject.toml file.
|
79
|
+
@return The version of the installed program (string value)."""
|
80
|
+
poetryConfigFile = os.path.join(TabbedNiceGui.LOCAL_PATH, TabbedNiceGui.POETRY_CONFIG_FILE)
|
81
|
+
if not os.path.isfile(poetryConfigFile):
|
82
|
+
poetryConfigFile = os.path.join(TabbedNiceGui.LOCAL_PATH, ".." + os.sep + TabbedNiceGui.POETRY_CONFIG_FILE)
|
83
|
+
poetryConfigFile2 = poetryConfigFile
|
84
|
+
if not os.path.isfile(poetryConfigFile):
|
85
|
+
cwd = os.getcwd()
|
86
|
+
poetryConfigFile = os.path.join(cwd, TabbedNiceGui.POETRY_CONFIG_FILE)
|
87
|
+
if not os.path.isfile(poetryConfigFile):
|
88
|
+
raise Exception(f"{poetryConfigFile}, {poetryConfigFile2} and {poetryConfigFile} not found.")
|
89
|
+
|
90
|
+
programVersion = None
|
91
|
+
with open(poetryConfigFile, 'r') as fd:
|
92
|
+
lines = fd.readlines()
|
93
|
+
for line in lines:
|
94
|
+
line=line.strip("\r\n")
|
95
|
+
if line.startswith('version'):
|
96
|
+
elems = line.split("=")
|
97
|
+
if len(elems) == 2:
|
98
|
+
programVersion = elems[1].strip('" ')
|
99
|
+
break
|
100
|
+
if programVersion is None:
|
101
|
+
raise Exception(f"Failed to extract program version from '{line}' line of {poetryConfigFile} file.")
|
102
|
+
return programVersion
|
103
|
+
|
104
|
+
def __init__(self, debugEnabled, logPath=None):
|
105
|
+
"""@brief Constructor
|
106
|
+
@param debugEnabled True if debugging is enabled.
|
107
|
+
@param logPath The path to store log files. If left as None then no log files are created."""
|
108
|
+
self._debugEnabled = debugEnabled
|
109
|
+
self._logFile = None # This must be defined in subclass if logging to a file is required.
|
110
|
+
self._buttonList = []
|
111
|
+
self._logMessageCount = 0
|
112
|
+
self._updateProgressOnTimer = False
|
113
|
+
self._progressStepValue = 0
|
114
|
+
self._progressBarStartMessage = ""
|
115
|
+
self._progressBarExpectedMessageList = []
|
116
|
+
self._expectedProgressBarMessageIndex = 0
|
117
|
+
self._expectedProgressBarMsgCount = 0
|
118
|
+
self._programVersion = TabbedNiceGui.GetProgramVersion()
|
119
|
+
|
120
|
+
self._logPath = None
|
121
|
+
if logPath:
|
122
|
+
self._logPath = os.path.join(os.path.expanduser('~'), logPath)
|
123
|
+
self._ensureLogPathExists()
|
124
|
+
|
125
|
+
self._isWindows = platform.system() == "Windows"
|
126
|
+
self._installFolder = TabbedNiceGui.GetInstallFolder()
|
127
|
+
|
128
|
+
# Make the install folder our current dir
|
129
|
+
os.chdir(self._installFolder)
|
130
|
+
|
131
|
+
# This queue is used to send commands from any thread to the GUI thread.
|
132
|
+
self._toGUIQueue = Queue()
|
133
|
+
# This queue is for the GUI thread to send messages to other threads
|
134
|
+
self._fromGUIQueue = Queue()
|
135
|
+
|
136
|
+
def _ensureLogPathExists(self):
|
137
|
+
"""@brief Ensure that the log path exists."""
|
138
|
+
if not os.path.isdir(self._logPath):
|
139
|
+
os.makedirs(self._logPath)
|
140
|
+
|
141
|
+
def getLogPath(self):
|
142
|
+
"""@return the Log file path if defined."""
|
143
|
+
return self._logPath
|
144
|
+
|
145
|
+
# Start ------------------------------
|
146
|
+
# Methods that allow the GUI to display standard UIO messages
|
147
|
+
# This allows the GUI to be used with code that was written
|
148
|
+
# to be used on the command line using UIO class instances
|
149
|
+
#
|
150
|
+
def info(self, msg):
|
151
|
+
"""@brief Send a info message to be displayed in the GUI.
|
152
|
+
This can be called from outside the GUI thread.
|
153
|
+
@param msg The message to be displayed."""
|
154
|
+
msgDict = {TabbedNiceGui.INFO_MESSAGE: str(msg)}
|
155
|
+
self.updateGUI(msgDict)
|
156
|
+
|
157
|
+
def warn(self, msg):
|
158
|
+
"""@brief Send a warning message to be displayed in the GUI.
|
159
|
+
This can be called from outside the GUI thread.
|
160
|
+
@param msg The message to be displayed."""
|
161
|
+
msgDict = {TabbedNiceGui.WARN_MESSAGE: str(msg)}
|
162
|
+
self.updateGUI(msgDict)
|
163
|
+
|
164
|
+
def error(self, msg):
|
165
|
+
"""@brief Send a error message to be displayed in the GUI.
|
166
|
+
This can be called from outside the GUI thread.
|
167
|
+
@param msg The message to be displayed."""
|
168
|
+
msgDict = {TabbedNiceGui.ERROR_MESSAGE: str(msg)}
|
169
|
+
self.updateGUI(msgDict)
|
170
|
+
|
171
|
+
def infoDialog(self, msg):
|
172
|
+
"""@brief Display an info level dialog.
|
173
|
+
@param msg The message dialog."""
|
174
|
+
msgDict = {TabbedNiceGui.NOTIFY_DIALOG: str(msg)}
|
175
|
+
self.updateGUI(msgDict)
|
176
|
+
|
177
|
+
def debug(self, msg):
|
178
|
+
"""@brief Send a debug message to be displayed in the GUI.
|
179
|
+
This can be called from outside the GUI thread.
|
180
|
+
@param msg The message to be displayed."""
|
181
|
+
if self._debugEnabled:
|
182
|
+
msgDict = {TabbedNiceGui.DEBUG_MESSAGE: str(msg)}
|
183
|
+
self.updateGUI(msgDict)
|
184
|
+
|
185
|
+
async def getInput(self, prompt):
|
186
|
+
"""@brief Allow the user to enter some text.
|
187
|
+
This can be called from outside the GUI thread.
|
188
|
+
@param prompt The user prompt."""
|
189
|
+
with ui.dialog() as dialog, ui.card():
|
190
|
+
inputObj = ui.input(label=prompt)
|
191
|
+
with ui.row():
|
192
|
+
ui.button('OK', on_click=lambda: dialog.submit('OK'))
|
193
|
+
ui.button('Cancel', on_click=lambda: dialog.submit('Cancel'))
|
194
|
+
|
195
|
+
result = await dialog
|
196
|
+
if result != 'OK':
|
197
|
+
returnText = None
|
198
|
+
else:
|
199
|
+
returnText = inputObj.value
|
200
|
+
return returnText
|
201
|
+
|
202
|
+
def reportException(self, exception):
|
203
|
+
"""@brief Report an exception.
|
204
|
+
If debug is enabled a full stack trace is displayed.
|
205
|
+
If not then the exception message is displayed.
|
206
|
+
@param exception The exception instance."""
|
207
|
+
if self._debugEnabled:
|
208
|
+
lines = traceback.format_exc().split("\n")
|
209
|
+
for line in lines:
|
210
|
+
self.error(line)
|
211
|
+
if len(exception.args) > 0:
|
212
|
+
self.error(exception.args[0])
|
213
|
+
|
214
|
+
def _sendEnableAllButtons(self, state):
|
215
|
+
"""@brief Send a message to the GUI to enable/disable all the GUI buttons.
|
216
|
+
This can be called from outside the GUI thread.
|
217
|
+
@param state If True enable the buttons, else disable them."""
|
218
|
+
msgDict = {TabbedNiceGui.ENABLE_BUTTONS: state}
|
219
|
+
self.updateGUI(msgDict)
|
220
|
+
|
221
|
+
def updateGUI(self, msgDict):
|
222
|
+
"""@brief Send a message to the GUI so that it updates itself.
|
223
|
+
@param msgDict A dict containing details of how to update the GUI."""
|
224
|
+
# Record the seconds when we received the message
|
225
|
+
msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
|
226
|
+
self._toGUIQueue.put(msgDict)
|
227
|
+
|
228
|
+
def showTable(self, table, rowSeparatorChar = "-", colSeparatorChar = "|"):
|
229
|
+
"""@brief Show the contents of a table to the user.
|
230
|
+
@param table This must be a list. Each list element must be a table row (list).
|
231
|
+
Each element in each row must be a string.
|
232
|
+
@param rowSeparatorChar The character used for horizontal lines to separate table rows.
|
233
|
+
@param colSeparatorChar The character used to separate table columns."""
|
234
|
+
columnWidths = []
|
235
|
+
# Check we have a table to display
|
236
|
+
if len(table) == 0:
|
237
|
+
raise Exception("No table rows to display")
|
238
|
+
|
239
|
+
# Check all rows have the same number of columns in the table
|
240
|
+
colCount = len(table[0])
|
241
|
+
for row in table:
|
242
|
+
if len(row) != colCount:
|
243
|
+
raise Exception(f"{str(row)} column count different from first row ({colCount})")
|
244
|
+
|
245
|
+
for row in table:
|
246
|
+
for col in row:
|
247
|
+
if not isinstance(col, str):
|
248
|
+
raise Exception(f"Table column is not a string: {col} in {row}")
|
249
|
+
|
250
|
+
# Get the max width for each column
|
251
|
+
for col in range(0,colCount):
|
252
|
+
maxWidth=0
|
253
|
+
for row in table:
|
254
|
+
if len(row[col]) > maxWidth:
|
255
|
+
maxWidth = len(row[col])
|
256
|
+
columnWidths.append(maxWidth)
|
257
|
+
|
258
|
+
tableWidth = 1
|
259
|
+
for columnWidth in columnWidths:
|
260
|
+
tableWidth += columnWidth + 3 # Space each side of the column + a column divider character
|
261
|
+
|
262
|
+
# Add the top line of the table
|
263
|
+
self.info(rowSeparatorChar*tableWidth)
|
264
|
+
|
265
|
+
# The starting row index
|
266
|
+
for rowIndex in range(0, len(table)):
|
267
|
+
rowText = colSeparatorChar
|
268
|
+
colIndex = 0
|
269
|
+
for col in table[rowIndex]:
|
270
|
+
colWidth = columnWidths[colIndex]
|
271
|
+
rowText = rowText + " " + f"{col:>{colWidth}s}" + " " + colSeparatorChar
|
272
|
+
colIndex += 1
|
273
|
+
self.info(rowText)
|
274
|
+
# Add the row separator line
|
275
|
+
self.info(rowSeparatorChar*tableWidth)
|
276
|
+
|
277
|
+
def logAll(self, enabled):
|
278
|
+
pass
|
279
|
+
|
280
|
+
def setLogFile(self, logFile):
|
281
|
+
pass
|
282
|
+
|
283
|
+
# End ------------------------------
|
284
|
+
|
285
|
+
def _saveLogMsg(self, msg):
|
286
|
+
"""@brief Save the message to a log file.
|
287
|
+
@param msg The message text to be stored in the log file."""
|
288
|
+
# If a log file has been set
|
289
|
+
if self._logFile:
|
290
|
+
# If the log file does not exist
|
291
|
+
if not os.path.isfile(self._logFile):
|
292
|
+
with open(self._logFile, 'w') as fd:
|
293
|
+
pass
|
294
|
+
# Update the log file
|
295
|
+
with open(self._logFile, 'a') as fd:
|
296
|
+
dateTimeStamp = TabbedNiceGui.GetDateTimeStamp()
|
297
|
+
fd.write(dateTimeStamp + ": " + msg + '\n')
|
298
|
+
|
299
|
+
def _getDisplayMsg(self, msg, prefix):
|
300
|
+
"""@brief Get the msg to display. If the msg does not already have a msg level we add one.
|
301
|
+
@param msg The source msg.
|
302
|
+
@param prefix The message prefix (level indcator) to add."""
|
303
|
+
if msg.startswith(TabbedNiceGui.INFO_MESSAGE) or \
|
304
|
+
msg.startswith(TabbedNiceGui.WARN_MESSAGE) or \
|
305
|
+
msg.startswith(TabbedNiceGui.ERROR_MESSAGE) or \
|
306
|
+
msg.startswith(TabbedNiceGui.DEBUG_MESSAGE):
|
307
|
+
_msg = msg
|
308
|
+
else:
|
309
|
+
_msg = prefix + msg
|
310
|
+
return _msg
|
311
|
+
|
312
|
+
def _handleMsg(self, msg):
|
313
|
+
"""@brief Log a message.
|
314
|
+
@param msg the message to the log window and the log file."""
|
315
|
+
self._log.push(msg)
|
316
|
+
self._saveLogMsg(msg)
|
317
|
+
self._logMessageCount += 1
|
318
|
+
# We've received a log message so update progress bar if required.
|
319
|
+
self._updateProgressBar(msg)
|
320
|
+
|
321
|
+
def _infoGT(self, msg):
|
322
|
+
"""@brief Update an info level message. This must be called from the GUI thread.
|
323
|
+
@param msg The message to display."""
|
324
|
+
_msg = self._getDisplayMsg(msg, TabbedNiceGui.INFO_MESSAGE)
|
325
|
+
self._handleMsg(_msg)
|
326
|
+
|
327
|
+
def _warnGT(self, msg):
|
328
|
+
"""@brief Update an warning level message. This must be called from the GUI thread.
|
329
|
+
@param msg The message to display."""
|
330
|
+
_msg = self._getDisplayMsg(msg, TabbedNiceGui.WARN_MESSAGE)
|
331
|
+
self._handleMsg(_msg)
|
332
|
+
|
333
|
+
def _errorGT(self, msg):
|
334
|
+
"""@brief Update an error level message. This must be called from the GUI thread.
|
335
|
+
@param msg The message to display."""
|
336
|
+
_msg = self._getDisplayMsg(msg, TabbedNiceGui.ERROR_MESSAGE)
|
337
|
+
self._handleMsg(_msg)
|
338
|
+
|
339
|
+
def _debugGT(self, msg):
|
340
|
+
"""@brief Update an debug level message. This must be called from the GUI thread.
|
341
|
+
@param msg The message to display."""
|
342
|
+
_msg = self._getDisplayMsg(msg, TabbedNiceGui.DEBUG_MESSAGE)
|
343
|
+
self._handleMsg(_msg)
|
344
|
+
|
345
|
+
def _clearMessages(self):
|
346
|
+
"""@brief Clear all messages from the log."""
|
347
|
+
self._log.clear()
|
348
|
+
self._logMessageCount = 0
|
349
|
+
|
350
|
+
def _getLogMessageCount(self):
|
351
|
+
"""@return the number of messages written to the log window/file"""
|
352
|
+
return self._logMessageCount
|
353
|
+
|
354
|
+
def _enableAllButtons(self, enabled):
|
355
|
+
"""@brief Enable/Disable all buttons.
|
356
|
+
@param enabled True if button is enabled."""
|
357
|
+
if enabled:
|
358
|
+
for button in self._buttonList:
|
359
|
+
button.enable()
|
360
|
+
# No buttons are enabled, any executed task must be complete therefor hide the progress bar.
|
361
|
+
self._stopProgress()
|
362
|
+
|
363
|
+
else:
|
364
|
+
for button in self._buttonList:
|
365
|
+
button.disable()
|
366
|
+
# If the caller has defined the number of log messages for normal completion
|
367
|
+
if self._progressStepValue > 0:
|
368
|
+
self._progress.set_visibility(True)
|
369
|
+
|
370
|
+
def guiTimerCallback(self):
|
371
|
+
"""@called periodically (quickly) to allow updates of the GUI."""
|
372
|
+
while not self._toGUIQueue.empty():
|
373
|
+
rxMessage = self._toGUIQueue.get()
|
374
|
+
if isinstance(rxMessage, dict):
|
375
|
+
self._processRXDict(rxMessage)
|
376
|
+
|
377
|
+
def initGUI(self, tabNameList, tabMethodInitList, reload=True, address="0.0.0.0", port=DEFAULT_SERVER_PORT, pageTitle="NiceGUI"):
|
378
|
+
"""@brief Init the tabbed GUI.
|
379
|
+
@param tabNameList A list of the names of each tab to be created.
|
380
|
+
@param tabMethodInitList A list of the methods to be called to init each of the above tabs.
|
381
|
+
The two lists must be the same size.
|
382
|
+
@param reload If reload is set False then changes to python files will not cause the server to be restarted.
|
383
|
+
@param address The address to bind the server to.
|
384
|
+
@param The TCP port to bind the server to.
|
385
|
+
@param pageTitle The page title that appears in the browser."""
|
386
|
+
# A bit of defensive programming.
|
387
|
+
if len(tabNameList) != len(tabMethodInitList):
|
388
|
+
raise Exception(f"initGUI: BUG: tabNameList ({len(tabNameList)}) and tabMethodInitList ({len(tabMethodInitList)}) are not the same length.")
|
389
|
+
tabObjList = []
|
390
|
+
with ui.row():
|
391
|
+
with ui.tabs().classes('w-full') as tabs:
|
392
|
+
for tabName in tabNameList:
|
393
|
+
tabObj = ui.tab(tabName)
|
394
|
+
tabObjList.append(tabObj)
|
395
|
+
|
396
|
+
with ui.tab_panels(tabs, value=tabObjList[0]).classes('w-full'):
|
397
|
+
for tabObj in tabObjList:
|
398
|
+
with ui.tab_panel(tabObj):
|
399
|
+
tabIndex = tabObjList.index(tabObj)
|
400
|
+
tabMethodInitList[tabIndex]()
|
401
|
+
|
402
|
+
guiLogLevel = "warning"
|
403
|
+
if self._debugEnabled:
|
404
|
+
guiLogLevel = "debug"
|
405
|
+
|
406
|
+
ui.label("Message Log")
|
407
|
+
self._progress = ui.slider(min=0,max=TabbedNiceGui.MAX_PROGRESS_VALUE,step=1)
|
408
|
+
self._progress.set_visibility(False)
|
409
|
+
self._progress.min = 0
|
410
|
+
# Don't allow user to adjust progress bar thumb
|
411
|
+
self._progress.disable()
|
412
|
+
self._log = ui.log(max_lines=2000)
|
413
|
+
self._log.set_visibility(True)
|
414
|
+
|
415
|
+
with ui.row():
|
416
|
+
ui.button('Clear Log', on_click=self._clearLog)
|
417
|
+
ui.button('Log Message Count', on_click=self._showLogMsgCount)
|
418
|
+
ui.button('Quit', on_click=self.close)
|
419
|
+
|
420
|
+
with ui.row():
|
421
|
+
ui.label(f"Software Version: {self._programVersion}")
|
422
|
+
|
423
|
+
ui.timer(interval=TabbedNiceGui.GUI_TIMER_SECONDS, callback=self.guiTimerCallback)
|
424
|
+
ui.timer(interval=TabbedNiceGui.PROGRESS_TIMER_SECONDS, callback=self.progressTimerCallback)
|
425
|
+
ui.run(host=address, port=port, title=pageTitle, dark=True, uvicorn_logging_level=guiLogLevel, reload=reload)
|
426
|
+
|
427
|
+
def progressTimerCallback(self):
|
428
|
+
"""@brief Time to update the progress bar. We run the timer all the time because there appears to be a
|
429
|
+
bug in the ui.timer instance. Calling cancel() does not appear to cancel the timer."""
|
430
|
+
if self._updateProgressOnTimer and self._progress.visible:
|
431
|
+
# Increment the progress bar
|
432
|
+
self._progress.set_value( self._progress.value + self._progressStepValue )
|
433
|
+
|
434
|
+
def _startProgress(self, durationSeconds=0, startMessage=None, expectedMsgList=[], expectedMsgCount=0):
|
435
|
+
"""@brief Start a timer that will update the progress bar.
|
436
|
+
The progress bar can simply update on a timer every second with durationSeconds set to the expected length
|
437
|
+
of the task.
|
438
|
+
|
439
|
+
If startMessage is set to a text string the progress time will not start until the log message area contains
|
440
|
+
the start message.
|
441
|
+
|
442
|
+
Alternatively if expectedMsgList contains a list of strings we expect to receive then the progress bar is
|
443
|
+
updated as each message is received. The messages may be the entire line of a log message or parts of a
|
444
|
+
log message line.
|
445
|
+
|
446
|
+
Alternatively if expectedMsgCount is set to a value > 0 then the progress bar is updated as each message is
|
447
|
+
added to the log and reaches 100% when the number of messages added to the log file reaches the expectedMsgCount.
|
448
|
+
|
449
|
+
@param startMessage The text of the log message we expect to receive to trigger the progress bar timer start.
|
450
|
+
@param expectedMsgList A list of the expected log file messages.
|
451
|
+
@param expectedMsgCount A int value that defines the number of log messages we expect to receive for normal progress
|
452
|
+
completion."""
|
453
|
+
self._progressValue = 0
|
454
|
+
self._progressBarStartMessage = ""
|
455
|
+
self._progressBarExpectedMessageList = []
|
456
|
+
self._expectedProgressBarMessageIndex = 0
|
457
|
+
self._expectedProgressBarMsgCount = 0
|
458
|
+
self._updateProgressOnTimer = False
|
459
|
+
self._progress.set_value( self._progressValue )
|
460
|
+
# If the caller wants to the progress bar to update as the log file message count increases.
|
461
|
+
if expectedMsgCount > 0:
|
462
|
+
self._expectedProgressBarMsgCount = expectedMsgCount
|
463
|
+
self._progressStepValue = TabbedNiceGui.MAX_PROGRESS_VALUE/float(self._expectedProgressBarMsgCount)
|
464
|
+
|
465
|
+
# If the caller wants to update the progress bar on expected messages.
|
466
|
+
elif len(expectedMsgList):
|
467
|
+
#Use the text of log messages to increment the progress bar.
|
468
|
+
self._expectedProgressBarMessageIndex = 0
|
469
|
+
self._progressBarExpectedMessageList = expectedMsgList
|
470
|
+
self._progressStepValue = TabbedNiceGui.MAX_PROGRESS_VALUE/float(len(expectedMsgList))
|
471
|
+
|
472
|
+
elif durationSeconds > 0:
|
473
|
+
# Calc the step size required to give the required duration
|
474
|
+
self._progressStepValue = TabbedNiceGui.MAX_PROGRESS_VALUE/float(durationSeconds)
|
475
|
+
if startMessage:
|
476
|
+
self._progressBarStartMessage = startMessage
|
477
|
+
else:
|
478
|
+
# Start updating the progress bar now.
|
479
|
+
self._updateProgressOnTimer = True
|
480
|
+
|
481
|
+
else:
|
482
|
+
raise Exception("BUG: _startProgressTimer() called. len(expectedMsgList)=0 and durationSeconds<=0.")
|
483
|
+
|
484
|
+
self._progress.set_visibility(True)
|
485
|
+
|
486
|
+
def _stopProgress(self):
|
487
|
+
"""@brief Stop the progress bar being updated and hide it."""
|
488
|
+
self._updateProgressOnTimer = False
|
489
|
+
self._progress.set_visibility(False)
|
490
|
+
|
491
|
+
def _updateProgressBar(self, msg):
|
492
|
+
"""@brief Update the progress bar if required when a log message is received. This is called as each message is added to the log.
|
493
|
+
@param msg The log message received."""
|
494
|
+
# If we update the progress bar as each message is received until we have a log with self._expectedProgressBarMsgCount many messages.
|
495
|
+
if self._expectedProgressBarMsgCount > 0:
|
496
|
+
self._progressValue = self._progressValue + self._progressStepValue
|
497
|
+
self._progress.set_value( self._progressValue )
|
498
|
+
|
499
|
+
# If we have a list of log messages to update the progress bar.
|
500
|
+
elif len(self._progressBarExpectedMessageList) > 0:
|
501
|
+
if self._expectedProgressBarMessageIndex < len(self._progressBarExpectedMessageList):
|
502
|
+
# Get the message we expect to receive next
|
503
|
+
expectedMsg = self._progressBarExpectedMessageList[self._expectedProgressBarMessageIndex]
|
504
|
+
if msg.find(expectedMsg) != -1:
|
505
|
+
self._progressValue = self._progressValue + self._progressStepValue
|
506
|
+
self._progress.set_value( self._progressValue )
|
507
|
+
self._expectedProgressBarMessageIndex += 1
|
508
|
+
|
509
|
+
# If we have a message that we expect to receive to start the progress bar timer.
|
510
|
+
elif self._progressBarStartMessage and len(self._progressBarStartMessage):
|
511
|
+
# If we found the start message in the message received.
|
512
|
+
if msg.find(self._progressBarStartMessage) != -1:
|
513
|
+
# Start updating the progress bar now on the timer.
|
514
|
+
self._updateProgressOnTimer = True
|
515
|
+
|
516
|
+
def _initTask(self):
|
517
|
+
"""@brief Should be called before a task is started."""
|
518
|
+
self._enableAllButtons(False)
|
519
|
+
self._clearMessages()
|
520
|
+
|
521
|
+
def _clearLog(self):
|
522
|
+
"""@brief Clear the log text"""
|
523
|
+
if self._log:
|
524
|
+
self._log.clear()
|
525
|
+
|
526
|
+
def _showLogMsgCount(self):
|
527
|
+
"""@brief Show the number of log messages"""
|
528
|
+
ui.notify(f"{self._getLogMessageCount()} messages in the log.")
|
529
|
+
|
530
|
+
def close(self):
|
531
|
+
"""@brief Close down the app server."""
|
532
|
+
ui.notify("Press 'CTRL C' at command line or close the terminal window to quit.")
|
533
|
+
# A subclass close() method can call
|
534
|
+
# app.shutdown()
|
535
|
+
# if reload=False on ui.run()
|
536
|
+
|
537
|
+
def _appendButtonList(self, button):
|
538
|
+
"""@brief Add to the button list. These buttons are disabled during the progress of a task.
|
539
|
+
@param button The button instance."""
|
540
|
+
self._buttonList.append(button)
|
541
|
+
|
542
|
+
def _processRXDict(self, rxDict):
|
543
|
+
"""@brief Process the dicts received from the GUI message queue.
|
544
|
+
@param rxDict The dict received from the GUI message queue."""
|
545
|
+
if TabbedNiceGui.INFO_MESSAGE in rxDict:
|
546
|
+
msg = rxDict[TabbedNiceGui.INFO_MESSAGE]
|
547
|
+
self._infoGT(msg)
|
548
|
+
|
549
|
+
elif TabbedNiceGui.WARN_MESSAGE in rxDict:
|
550
|
+
msg = rxDict[TabbedNiceGui.WARN_MESSAGE]
|
551
|
+
self._warnGT(msg)
|
552
|
+
|
553
|
+
elif TabbedNiceGui.ERROR_MESSAGE in rxDict:
|
554
|
+
msg = rxDict[TabbedNiceGui.ERROR_MESSAGE]
|
555
|
+
self._errorGT(msg)
|
556
|
+
|
557
|
+
elif TabbedNiceGui.DEBUG_MESSAGE in rxDict:
|
558
|
+
msg = rxDict[TabbedNiceGui.DEBUG_MESSAGE]
|
559
|
+
self._debugGT(msg)
|
560
|
+
|
561
|
+
elif TabbedNiceGui.ENABLE_BUTTONS in rxDict:
|
562
|
+
state = rxDict[TabbedNiceGui.ENABLE_BUTTONS]
|
563
|
+
self._enableAllButtons(state)
|
564
|
+
|
565
|
+
elif TabbedNiceGui.NOTIFY_DIALOG in rxDict:
|
566
|
+
message = rxDict[TabbedNiceGui.NOTIFY_DIALOG]
|
567
|
+
ui.notify(message, close_button='OK', type="positive", position="center")
|
568
|
+
|
569
|
+
else:
|
570
|
+
|
571
|
+
self._handleGUIUpdate(rxDict)
|
572
|
+
|
573
|
+
def _updateGUI(self, msgDict):
|
574
|
+
"""@brief Send a message to the GUI so that it updates itself.
|
575
|
+
@param msgDict A dict containing details of how to update the GUI."""
|
576
|
+
# Record the seconds when we received the message
|
577
|
+
msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
|
578
|
+
self._toGUIQueue.put(msgDict)
|
579
|
+
|
580
|
+
def _updateExeThread(self, msgDict):
|
581
|
+
"""@brief Send a message from the GUI thread to an external (non GUI thread).
|
582
|
+
@param msgDict A dict containing messages to be sent to the external thread."""
|
583
|
+
# Record the seconds when we received the message
|
584
|
+
msgDict[TabbedNiceGui.UPDATE_SECONDS]=time()
|
585
|
+
self._fromGUIQueue.put(msgDict)
|
586
|
+
|
587
|
+
def _updateGUIAndWaitForResponse(self, msgDict, timeout=DEFAULT_GUI_RESPONSE_TIMEOUT):
|
588
|
+
"""@brief Send a message to the GUI and wait for a response.
|
589
|
+
@param msgDict The message dictionary to be sent to the GUI.
|
590
|
+
@param timeout The number of seconds to wait for a response.
|
591
|
+
@return The return dict."""
|
592
|
+
timeoutT = time()+timeout
|
593
|
+
rxDict = None
|
594
|
+
self._updateGUI(msgDict)
|
595
|
+
while True:
|
596
|
+
if not self._fromGUIQueue.empty():
|
597
|
+
rxMessage = self._fromGUIQueue.get()
|
598
|
+
if isinstance(rxMessage, dict):
|
599
|
+
rxDict = rxMessage
|
600
|
+
break
|
601
|
+
|
602
|
+
elif time() >= timeoutT:
|
603
|
+
raise Exception(f"{timeout} second GUI response timeout.")
|
604
|
+
|
605
|
+
else:
|
606
|
+
# Don't spin to fast
|
607
|
+
sleep(0.1)
|
608
|
+
|
609
|
+
return rxDict
|
610
|
+
|
611
|
+
def _handleGUIUpdate(self, rxDict):
|
612
|
+
"""@brief Process the dicts received from the GUI message queue
|
613
|
+
that were not handled by the parent class.
|
614
|
+
@param rxDict The dict received from the GUI message queue."""
|
615
|
+
raise NotImplementedError("_handleGUIUpdate() is not implemented. Implement this method in a subclass of TabbedNiceGUI")
|
616
|
+
|
617
|
+
|
618
|
+
class YesNoDialog(object):
|
619
|
+
"""@brief Responsible for displaying a dialog box to the user with a boolean (I.E yes/no, ok/cancel) response."""
|
620
|
+
TEXT_INPUT_FIELD_TYPE = 1
|
621
|
+
NUMBER_INPUT_FIELD_TYPE = 2
|
622
|
+
SWITCH_INPUT_FIELD_TYPE = 3
|
623
|
+
DROPDOWN_INPUT_FIELD = 4
|
624
|
+
COLOR_INPUT_FIELD = 5
|
625
|
+
DATE_INPUT_FIELD = 6
|
626
|
+
TIME_INPUT_FIELD = 7
|
627
|
+
KNOB_INPUT_FIELD = 8
|
628
|
+
HOUR_MIN_INPUT_FIELD_TYPE = 9
|
629
|
+
VALID_FIELD_TYPE_LIST = (TEXT_INPUT_FIELD_TYPE,
|
630
|
+
NUMBER_INPUT_FIELD_TYPE,
|
631
|
+
SWITCH_INPUT_FIELD_TYPE,
|
632
|
+
DROPDOWN_INPUT_FIELD,
|
633
|
+
COLOR_INPUT_FIELD,
|
634
|
+
DATE_INPUT_FIELD,
|
635
|
+
TIME_INPUT_FIELD,
|
636
|
+
KNOB_INPUT_FIELD,
|
637
|
+
HOUR_MIN_INPUT_FIELD_TYPE)
|
638
|
+
|
639
|
+
FIELD_TYPE_KEY = "FIELD_TYPE_KEY" # The type of field to be displayed.
|
640
|
+
VALUE_KEY = "VALUE_KEY" # The value to be displayed in the field when the dialog is displayed.
|
641
|
+
MIN_NUMBER_KEY = "MIN_NUMBER_KEY" # If the type is NUMBER_INPUT_FIELD_TYPE, the min value that can be entered.
|
642
|
+
MAX_NUMBER_KEY = "MAX_NUMBER_KEY" # If the type is NUMBER_INPUT_FIELD_TYPE, the max value that can be entered.
|
643
|
+
WIDGET_KEY = "WIDGET_KEY" # The key to the GUI widget (E.G ui.input, ui.number etc)
|
644
|
+
OPTIONS_KEY = "OPTIONS_KEY" # Some input fields require a list of options (E.G DROPDOWN_INPUT_FIELD).
|
645
|
+
STEP_KEY = "STEP_KEY" # The step size for numerical input fields
|
646
|
+
|
647
|
+
def __init__(self,
|
648
|
+
prompt,
|
649
|
+
successMethod,
|
650
|
+
failureMethod=None,
|
651
|
+
successButtonText="Yes",
|
652
|
+
failureButtonText="No"):
|
653
|
+
"""@brief Constructor"""
|
654
|
+
self._dialog = None
|
655
|
+
self._selectedFile = None
|
656
|
+
self._successButtonText = None # The dialogs success button text
|
657
|
+
self._failureButtonText = None # The dialogs failure button text
|
658
|
+
self._prompt = None # The prompt to be displayed in the dialog
|
659
|
+
self._successMethod = None # The method to be called when the success button is selected.
|
660
|
+
self._failureMethod = None # The method to be called when the failure button is selected.
|
661
|
+
self._inputFieldDict = {} # A dict of input field details to be included in the dialog. Can be left as an empty dict if no input fields are required.
|
662
|
+
# The key in this dict is the name of the input field that the user sees.
|
663
|
+
# The value in this dict is another dict containing details of the input field which may be
|
664
|
+
|
665
|
+
self.setPrompt(prompt)
|
666
|
+
self.setSuccessMethod(successMethod)
|
667
|
+
self.setFailureMethod(failureMethod)
|
668
|
+
self.setSuccessButtonLabel(successButtonText)
|
669
|
+
self.setFailureButtonLabel(failureButtonText)
|
670
|
+
|
671
|
+
|
672
|
+
def addField(self, name, fieldType, value=None, minNumber=None, maxNumber=None, options=None, step=1):
|
673
|
+
"""@brief Add a field to the dialog.
|
674
|
+
@param name The name of the field to be added.
|
675
|
+
@param fieldType The type of field to be entered.
|
676
|
+
@param value The optional initial value for the field when the dialog is displayed.
|
677
|
+
@param minNumber The optional min value if the fieldType = NUMBER_INPUT_FIELD_TYPE.
|
678
|
+
@param maxNumber The optional max value if the fieldType = NUMBER_INPUT_FIELD_TYPE.
|
679
|
+
@param step The step size for numerical input fields.
|
680
|
+
"""
|
681
|
+
if name and len(name) > 0:
|
682
|
+
if fieldType in YesNoDialog.VALID_FIELD_TYPE_LIST:
|
683
|
+
self._inputFieldDict[name] = {YesNoDialog.FIELD_TYPE_KEY: fieldType,
|
684
|
+
YesNoDialog.VALUE_KEY: value,
|
685
|
+
YesNoDialog.MIN_NUMBER_KEY: minNumber,
|
686
|
+
YesNoDialog.MAX_NUMBER_KEY: maxNumber,
|
687
|
+
YesNoDialog.OPTIONS_KEY: options,
|
688
|
+
YesNoDialog.STEP_KEY: step}
|
689
|
+
|
690
|
+
else:
|
691
|
+
raise Exception(f"YesNoDialog.addField() {fieldType} is an invalid field type.")
|
692
|
+
|
693
|
+
else:
|
694
|
+
raise Exception("YesNoDialog.addField() name not set.")
|
695
|
+
|
696
|
+
def _init(self):
|
697
|
+
"""@brief Init the dialog."""
|
698
|
+
with ui.dialog() as self._dialog, ui.card():
|
699
|
+
ui.label(self._prompt)
|
700
|
+
for fieldName in self._inputFieldDict:
|
701
|
+
fieldType = self._inputFieldDict[fieldName][YesNoDialog.FIELD_TYPE_KEY]
|
702
|
+
if fieldType == YesNoDialog.TEXT_INPUT_FIELD_TYPE:
|
703
|
+
widget = ui.input(label=fieldName).style('width: 200px;')
|
704
|
+
|
705
|
+
elif fieldType == YesNoDialog.NUMBER_INPUT_FIELD_TYPE:
|
706
|
+
value = self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY]
|
707
|
+
min = self._inputFieldDict[fieldName][YesNoDialog.MIN_NUMBER_KEY]
|
708
|
+
max = self._inputFieldDict[fieldName][YesNoDialog.MAX_NUMBER_KEY]
|
709
|
+
step = self._inputFieldDict[fieldName][YesNoDialog.STEP_KEY]
|
710
|
+
widget = ui.number(label=fieldName,
|
711
|
+
value=value,
|
712
|
+
min=min,
|
713
|
+
max=max,
|
714
|
+
step=step).style('width: 200px;')
|
715
|
+
|
716
|
+
elif fieldType == YesNoDialog.SWITCH_INPUT_FIELD_TYPE:
|
717
|
+
widget = ui.switch(fieldName)
|
718
|
+
|
719
|
+
elif fieldType == YesNoDialog.DROPDOWN_INPUT_FIELD:
|
720
|
+
#ui.label(fieldName)
|
721
|
+
options = self._inputFieldDict[fieldName][YesNoDialog.OPTIONS_KEY]
|
722
|
+
if options:
|
723
|
+
widget = ui.select(options)
|
724
|
+
widget.tooltip(fieldName)
|
725
|
+
else:
|
726
|
+
raise Exception("BUG: DROPDOWN_INPUT_FIELD defined without defining the options.")
|
727
|
+
|
728
|
+
elif fieldType == YesNoDialog.COLOR_INPUT_FIELD:
|
729
|
+
widget = ui.color_input(label=fieldName)
|
730
|
+
|
731
|
+
elif fieldType == YesNoDialog.DATE_INPUT_FIELD:
|
732
|
+
widget = ui.date()
|
733
|
+
widget.tooltip(fieldName)
|
734
|
+
|
735
|
+
elif fieldType == YesNoDialog.TIME_INPUT_FIELD:
|
736
|
+
widget = ui.time()
|
737
|
+
widget.tooltip(fieldName)
|
738
|
+
|
739
|
+
elif fieldType == YesNoDialog.KNOB_INPUT_FIELD:
|
740
|
+
widget = ui.knob(show_value=True)
|
741
|
+
widget.tooltip(fieldName)
|
742
|
+
|
743
|
+
elif fieldType == YesNoDialog.HOUR_MIN_INPUT_FIELD_TYPE:
|
744
|
+
widget = self._get_input_time_field(fieldName)
|
745
|
+
widget.tooltip(fieldName)
|
746
|
+
|
747
|
+
# Save a ref to the widet in the field dict
|
748
|
+
self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY] = widget
|
749
|
+
|
750
|
+
# If we have an initial value then set it
|
751
|
+
value = self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY]
|
752
|
+
if value:
|
753
|
+
widget.value = value
|
754
|
+
|
755
|
+
with ui.row():
|
756
|
+
ui.button(self._successButtonText, on_click=self._internalSuccessMethod)
|
757
|
+
ui.button(self._failureButtonText, on_click=self._internalFailureMethod)
|
758
|
+
|
759
|
+
def _get_input_time_field(self, label):
|
760
|
+
"""@brief Add a control to allow the user to enter the time as an hour and min.
|
761
|
+
@param label The label for the time field.
|
762
|
+
@return The input field containing the hour and minute entered."""
|
763
|
+
# Put this off the bottom of the mobile screen as most times it will not be needed
|
764
|
+
# and there is not enough room on the mobile screen above the plot pane.
|
765
|
+
with ui.row().classes('w-full'):
|
766
|
+
ui.label(label)
|
767
|
+
with ui.row().classes('w-full'):
|
768
|
+
time_input = ui.input("Time (HH:MM)")
|
769
|
+
with time_input as time:
|
770
|
+
with ui.menu().props('no-parent-event') as menu:
|
771
|
+
with ui.time().bind_value(time):
|
772
|
+
with ui.row().classes('justify-end'):
|
773
|
+
ui.button('Close', on_click=menu.close).props('flat')
|
774
|
+
with time.add_slot('append'):
|
775
|
+
ui.icon('access_time').on('click', menu.open).classes('cursor-pointer')
|
776
|
+
return time_input
|
777
|
+
|
778
|
+
def setPrompt(self, prompt):
|
779
|
+
"""@brief Set the user prompt.
|
780
|
+
@param prompt The user prompt."""
|
781
|
+
self._prompt = prompt
|
782
|
+
|
783
|
+
def setSuccessMethod(self, successMethod):
|
784
|
+
"""@brief Set the text of the success button.
|
785
|
+
@param successMethod The method called when the user selects the success button."""
|
786
|
+
self._successMethod = successMethod
|
787
|
+
|
788
|
+
def setFailureMethod(self, failureMethod):
|
789
|
+
"""@brief Set the text of the success button.
|
790
|
+
@param failureMethod The method called when the user selects the failure button."""
|
791
|
+
self._failureMethod = failureMethod
|
792
|
+
|
793
|
+
def setSuccessButtonLabel(self, label):
|
794
|
+
"""@brief Set the text of the success button.
|
795
|
+
@param label The success button text."""
|
796
|
+
self._successButtonText = label
|
797
|
+
|
798
|
+
def setFailureButtonLabel(self, label):
|
799
|
+
"""@brief Set the text of the failure button.
|
800
|
+
@param label The failure button text."""
|
801
|
+
self._failureButtonText = label
|
802
|
+
|
803
|
+
def show(self):
|
804
|
+
"""@brief Allow the user to select yes/no, ok/cancel etc in response to a question."""
|
805
|
+
self._init()
|
806
|
+
self._dialog.open()
|
807
|
+
|
808
|
+
def getValue(self, fieldName):
|
809
|
+
"""@brief Get the value entered by the user.
|
810
|
+
@param fieldName The name of the field entered."""
|
811
|
+
value = None
|
812
|
+
widget = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY]
|
813
|
+
if hasattr(widget, 'value'):
|
814
|
+
value = widget.value
|
815
|
+
|
816
|
+
elif isinstance(widget, ui.upload):
|
817
|
+
value = self._selectedFile
|
818
|
+
|
819
|
+
return value
|
820
|
+
|
821
|
+
def _internalSuccessMethod(self):
|
822
|
+
"""@brief Called when the user selects the success button."""
|
823
|
+
self.close()
|
824
|
+
# Save the entered values for all fields
|
825
|
+
for fieldName in self._inputFieldDict:
|
826
|
+
widget = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY]
|
827
|
+
if hasattr(widget, 'value'):
|
828
|
+
self._inputFieldDict[fieldName][YesNoDialog.VALUE_KEY] = self._inputFieldDict[fieldName][YesNoDialog.WIDGET_KEY].value
|
829
|
+
# If defined call the method
|
830
|
+
if self._successMethod:
|
831
|
+
self._successMethod()
|
832
|
+
|
833
|
+
def _internalFailureMethod(self):
|
834
|
+
"""@brief Called when the user selects the failure button."""
|
835
|
+
self.close()
|
836
|
+
if self._failureMethod:
|
837
|
+
self._failureMethod()
|
838
|
+
|
839
|
+
def close(self):
|
840
|
+
"""@brief Close the boolean dialog."""
|
841
|
+
self._dialog.close()
|