Completed
Push — master ( 6d5900...9dff57 )
by Olivier
59s
created

QtHandler.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
1
#! /usr/bin/env python3
2
3
import sys
4
import os
5
import logging
6
7
from PyQt5.QtCore import QTimer, QSettings, QModelIndex, Qt, QCoreApplication
8
from PyQt5.QtGui import QIcon, QFont
9
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QMessageBox, QStyledItemDelegate, QMenu
10
11
from opcua import ua
12
from opcua import Server
13
from opcua import copy_node
14
from opcua.common.ua_utils import get_node_children
15
from opcua.common.xmlexporter import XmlExporter
16
from opcua.common.instantiate import instantiate
17
18
from uawidgets import resources
19
from uawidgets.attrs_widget import AttrsWidget
20
from uawidgets.tree_widget import TreeWidget
21
from uawidgets.refs_widget import RefsWidget
22
from uawidgets.new_node_dialogs import NewNodeBaseDialog, NewUaObjectDialog, NewUaVariableDialog, NewUaMethodDialog
23
from uamodeler.uamodeler_ui import Ui_UaModeler
24
from uamodeler.namespace_widget import NamespaceWidget
25
from uamodeler.refnodesets_widget import RefNodeSetsWidget
26
from uawidgets.utils import trycatchslot
27
from uawidgets.logger import QtHandler
28
29
30
logger = logging.getLogger(__name__)
31
32
33
class BoldDelegate(QStyledItemDelegate):
34
35
    def __init__(self, parent, model, added_node_list):
36
        QStyledItemDelegate.__init__(self, parent)
37
        self.added_node_list = added_node_list
38
        self.model = model
39
40
    def paint(self, painter, option, idx):
41
        new_idx = idx.sibling(idx.row(), 0)
42
        item = self.model.itemFromIndex(new_idx)
43
        if item and item.data(Qt.UserRole) in self.added_node_list:
44
            option.font.setWeight(QFont.Bold)
45
        QStyledItemDelegate.paint(self, painter, option, idx)
46
47
48
class UaModeler(QMainWindow):
49
50
    def __init__(self):
51
        QMainWindow.__init__(self)
52
        self.ui = Ui_UaModeler()
53
        self.ui.setupUi(self)
54
        self.setWindowIcon(QIcon(":/network.svg"))
55
56
        # we only show statusbar in case of errors
57
        self.ui.statusBar.hide()
58
59
        # setup QSettings for application and get a settings object
60
        QCoreApplication.setOrganizationName("FreeOpcUa")
61
        QCoreApplication.setApplicationName("OpcUaModeler")
62
        self.settings = QSettings()
63
        self._last_dir = self.settings.value("last_dir", ".")
64
65
        self._restore_state()
66
67
        self.server = None
68
        self._new_nodes = []  # the added nodes we will save
69
        self._current_path = None
70
        self._modified = False
71
        self._copy_clipboard = None
72
73
        self.tree_ui = TreeWidget(self.ui.treeView)
74
        self.tree_ui.error.connect(self.show_error)
75
        delegate = BoldDelegate(self, self.tree_ui.model, self._new_nodes)
76
        self.ui.treeView.setItemDelegate(delegate)
77
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
78
79
        self.refs_ui = RefsWidget(self.ui.refView)
80
        self.refs_ui.error.connect(self.show_error)
81
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
82
        self.attrs_ui.error.connect(self.show_error)
83
        self.attrs_ui.attr_written.connect(self._attr_written)
84
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
85
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
86
        self.nodesets_ui.error.connect(self.show_error)
87
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
88
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
89
90
        self.ui.treeView.activated.connect(self.show_refs)
91
        self.ui.treeView.clicked.connect(self.show_refs)
92
        self.ui.treeView.activated.connect(self.show_attrs)
93
        self.ui.treeView.clicked.connect(self.show_attrs)
94
95
        # fix icon stuff
96
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
97
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
98
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
99
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
100
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
101
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
102
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
103
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
104
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
105
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
106
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
107
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
108
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
109
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
110
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
111
112
        self.setup_context_menu_tree()
113
114
        # actions
115
        self.ui.actionNew.triggered.connect(self._new)
116
        self.ui.actionOpen.triggered.connect(self._open)
117
        self.ui.actionCopy.triggered.connect(self._copy)
118
        self.ui.actionPaste.triggered.connect(self._paste)
119
        self.ui.actionDelete.triggered.connect(self._delete)
120
        self.ui.actionImport.triggered.connect(self._import_slot)
121
        self.ui.actionSave.triggered.connect(self._save_slot)
122
        self.ui.actionSaveAs.triggered.connect(self._save_as)
123
        self.ui.actionCloseModel.triggered.connect(self._close_model_slot)
124
        self.ui.actionAddObjectType.triggered.connect(self._add_object_type)
125
        self.ui.actionAddObject.triggered.connect(self._add_object)
126
        self.ui.actionAddFolder.triggered.connect(self._add_folder)
127
        self.ui.actionAddMethod.triggered.connect(self._add_method)
128
        self.ui.actionAddDataType.triggered.connect(self._add_data_type)
129
        self.ui.actionAddVariable.triggered.connect(self._add_variable)
130
        self.ui.actionAddVariableType.triggered.connect(self._add_variable_type)
131
        self.ui.actionAddProperty.triggered.connect(self._add_property)
132
133
        self._disable_actions()
134
135
    def _update_actions_state(self, current, previous):
136
        self._disable_add_actions()
137
        node = self.tree_ui.get_current_node(current)
138
        if not node or node in (self.server.nodes.root, 
139
                                self.server.nodes.types, 
140
                                self.server.nodes.event_types, 
141
                                self.server.nodes.object_types, 
142
                                self.server.nodes.reference_types, 
143
                                self.server.nodes.variable_types, 
144
                                self.server.nodes.data_types):
145
            return
146
        path = node.get_path()
147
        nodeclass = node.get_node_class()
148
149
        self.ui.actionAddFolder.setEnabled(True)
150
        self.ui.actionCopy.setEnabled(True)
151
        self.ui.actionPaste.setEnabled(True)
152
        self.ui.actionDelete.setEnabled(True)
153
154
        if self.server.nodes.base_object_type in path:
155
            self.ui.actionAddObjectType.setEnabled(True)
156
157
        if self.server.nodes.base_variable_type in path:
158
            self.ui.actionAddVariableType.setEnabled(True)
159
160
        if self.server.nodes.base_data_type in path:
161
            self.ui.actionAddDataType.setEnabled(True)
162
            if self.server.nodes.enum_data_type in path:
163
                self.ui.actionAddProperty.setEnabled(True)
164
            return  # not other nodes should be added here
165
166
        if nodeclass != ua.NodeClass.Variable:
167
            self.ui.actionAddFolder.setEnabled(True)
168
            self.ui.actionAddObject.setEnabled(True)
169
            self.ui.actionAddVariable.setEnabled(True)
170
            self.ui.actionAddProperty.setEnabled(True)
171
            self.ui.actionAddMethod.setEnabled(True)
172
173
    def setup_context_menu_tree(self):
174
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
175
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
176
        self._contextMenu = QMenu()
177
178
        # tree view menu
179
        self._contextMenu.addAction(self.ui.actionCopy)
180
        self._contextMenu.addAction(self.ui.actionPaste)
181
        self._contextMenu.addAction(self.ui.actionDelete)
182
        self._contextMenu.addSeparator()
183
        self._contextMenu.addAction(self.ui.actionAddFolder)
184
        self._contextMenu.addAction(self.ui.actionAddObject)
185
        self._contextMenu.addAction(self.ui.actionAddVariable)
186
        self._contextMenu.addAction(self.ui.actionAddProperty)
187
        self._contextMenu.addAction(self.ui.actionAddMethod)
188
        self._contextMenu.addAction(self.ui.actionAddObjectType)
189
        self._contextMenu.addAction(self.ui.actionAddVariableType)
190
        self._contextMenu.addAction(self.ui.actionAddDataType)
191
192
    def _show_context_menu_tree(self, position):
193
        node = self.tree_ui.get_current_node()
194
        if node:
195
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
196
197
    @trycatchslot
198
    def _delete(self):
199
        node = self.tree_ui.get_current_node()
200
        if node:
201
            nodes = get_node_children(node)
202
            for n in nodes:
203
                n.delete()
204
                if n in self._new_nodes:
205
                    self._new_nodes.remove(n)
206
            self.tree_ui.remove_current_item()
207
208
    @trycatchslot
209
    def _copy(self):
210
        node = self.tree_ui.get_current_node()
211
        if node:
212
213
            self._copy_clipboard = node
214
215
    @trycatchslot
216
    def _paste(self):
217
        if self._copy_clipboard:
218
            parent = self.tree_ui.get_current_node()
219
            try:
220
                added_nodes = copy_node(parent, self._copy_clipboard)
221
            except Exception as ex:
222
                self.show_error(ex)
223
                raise
224
            self._new_nodes.extend(added_nodes)
225
            self.tree_ui.reload_current()
226
            self.show_refs()
227
            self._modified = True
228
229
    def _attr_written(self, attr, dv):
230
        self._modified = True
231
        if attr == ua.AttributeIds.BrowseName:
232
            self.tree_ui.update_browse_name_current_item(dv.Value.Value)
233
        elif attr == ua.AttributeIds.DisplayName:
234
            self.tree_ui.update_display_name_current_item(dv.Value.Value)
235
      
236
    def _restore_state(self):
237
        self.resize(int(self.settings.value("main_window_width", 800)),
238
                    int(self.settings.value("main_window_height", 600)))
239
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
240
        self.restoreState(self.settings.value("main_window_state", bytearray()))
241
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
242
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
243
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
244
245
    def _disable_model_actions(self):
246
        self.ui.actionImport.setEnabled(False)
247
        self.ui.actionSave.setEnabled(False)
248
        self.ui.actionSaveAs.setEnabled(False)
249
250
    def _disable_actions(self):
251
        self._disable_add_actions()
252
        self._disable_model_actions()
253
254
    def _disable_add_actions(self):
255
        self.ui.actionPaste.setEnabled(False)
256
        self.ui.actionCopy.setEnabled(False)
257
        self.ui.actionDelete.setEnabled(False)
258
        self.ui.actionAddObject.setEnabled(False)
259
        self.ui.actionAddFolder.setEnabled(False)
260
        self.ui.actionAddVariable.setEnabled(False)
261
        self.ui.actionAddProperty.setEnabled(False)
262
        self.ui.actionAddDataType.setEnabled(False)
263
        self.ui.actionAddVariableType.setEnabled(False)
264
        self.ui.actionAddObjectType.setEnabled(False)
265
        self.ui.actionAddMethod.setEnabled(False)
266
267
    def _enable_model_actions(self):
268
        self.ui.actionImport.setEnabled(True)
269
        self.ui.actionSave.setEnabled(True)
270
        self.ui.actionSaveAs.setEnabled(True)
271
        #self.ui.actionAddObject.setEnabled(True)
272
        #self.ui.actionAddFolder.setEnabled(True)
273
        #self.ui.actionAddVariable.setEnabled(True)
274
        #self.ui.actionAddProperty.setEnabled(True)
275
        #self.ui.actionAddDataType.setEnabled(True)
276
        #self.ui.actionAddVariableType.setEnabled(True)
277
        #self.ui.actionAddObjectType.setEnabled(True)
278
279
    @trycatchslot
280
    def _close_model_slot(self):
281
        self._close_model()
282
283
    def _close_model(self):
284
        if not self.really_exit():
285
            return False
286
        self._disable_actions()
287
        self.tree_ui.clear()
288
        self.refs_ui.clear()
289
        self.attrs_ui.clear()
290
        self.idx_ui.clear()
291
        self.nodesets_ui.clear()
292
        self._current_path = None
293
        self._modified = False
294
        self._update_title()
295
        if self.server is not None:
296
            self.server.stop()
297
        self.server = None
298
        return True
299
300
    def _update_title(self):
301
        self.setWindowTitle("FreeOpcUa Modeler " + str(self._current_path))
302
303
    @trycatchslot
304
    def _new(self):
305
        if not self._close_model():
306
            return
307
        self._create_new_model()
308
309
    def _create_new_model(self):
310
        self.server = Server()
311
        endpoint = "opc.tcp://0.0.0.0:48400/freeopcua/uamodeler/"
312
        logger.info("Starting server on %s", endpoint)
313
        self.server.set_endpoint(endpoint)
314
        self.server.set_server_name("OpcUa Modeler Server")
315
        # now remove freeopcua namespace, not necessary when modeling and
316
        # ensures correct idx for exported nodesets
317
        ns_node = self.server.get_node(ua.NodeId(ua.ObjectIds.Server_NamespaceArray))
318
        nss = ns_node.get_value()
319
        ns_node.set_value(nss[1:])
320
321
        del(self._new_nodes[:])  # empty list while keeping reference
322
323
        self.server.start()
324
        self.tree_ui.set_root_node(self.server.get_root_node())
325
        self.idx_ui.set_node(self.server.get_node(ua.ObjectIds.Server_NamespaceArray))
326
        self.nodesets_ui.set_server(self.server)
327
        self._modified = False
328
        self._enable_model_actions()
329
        self._current_path = "NoName"
330
        self._update_title()
331
        return True
332
333
    @trycatchslot
334
    def _import_slot(self):
335
        self._import()
336
337
    def _import(self, path=None):
338
        if not path:
339
            path, ok = self._get_xml()
340
            if not ok:
341
                return None
342
        self._last_dir = os.path.dirname(path)
343
        try:
344
            new_nodes = self.server.import_xml(path)
345
            self._new_nodes.extend([self.server.get_node(node) for node in new_nodes])
346
            self._modified = True
347
        except Exception as ex:
348
            self.show_error(ex)
349
            raise
350
        # we maybe should only reload the imported nodes
351
        self.tree_ui.reload()
352
        self.idx_ui.reload()
353
        return path
354
355
    def _get_xml(self):
356
        return QFileDialog.getOpenFileName(self, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
357
358
    @trycatchslot
359
    def _open(self):
360
        if not self._close_model():
361
            return
362
        path, ok = self._get_xml()
363
        if not ok:
364
            return
365
        self._create_new_model()
366
        try:
367
            path = self._import(path)
368
        except:
369
            self._close_model()
370
            raise
371
        self._modified = False
372
        self._current_path = path
373
        self._update_title()
374
375
    @trycatchslot
376
    def _save_as(self):
377
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
378
        if ok:
379
            self._current_path = path
380
            self._update_title()
381
            self._save()
382
383
    @trycatchslot
384
    def _save_slot(self):
385
        self._save()
386
387
    def _save(self):
388
        if not self._current_path or self._current_path == "NoName":
389
            path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
390
            self._current_path = path
391
            if not ok:
392
                return
393
        logger.info("Saving to %s", self._current_path)
394
        logger.info("Exporting  %s nodes: %s", len(self._new_nodes), self._new_nodes)
395
        logger.info("and namespaces: %s ", self.server.get_namespace_array()[1:])
396
        exp = XmlExporter(self.server)
397
        uris = self.server.get_namespace_array()[1:]
398
        exp.build_etree(self._new_nodes, uris=uris)
399
        try:
400
            exp.write_xml(self._current_path)
401
        except Exception as ex:
402
            self.show_error(ex)
403
            raise
404
        self._modified = False
405
        self._update_title()
406
        self.show_msg(self._current_path + " saved")
407
408
    def really_exit(self):
409
        if self._modified:
410
            reply = QMessageBox.question(
411
                self,
412
                "OPC UA Modeler",
413
                "Model is modified, do you really want to close model?",
414
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
415
            )
416
            if reply != QMessageBox.Yes:
417
                return False
418
419
        return True
420
421
    def _after_add(self, new_nodes):
422
        if isinstance(new_nodes, (list, tuple)):
423
            self._new_nodes.extend(new_nodes)
424
        else:
425
            self._new_nodes.append(new_nodes)
426
        self.tree_ui.reload_current()
427
        self.show_refs()
428
        self._modified = True
429
430
    @trycatchslot
431
    def _add_method(self):
432
        parent = self.tree_ui.get_current_node()
433
        args, ok = NewUaMethodDialog.getArgs(self, "Add Method", self.server)
434
        if ok:
435
            logger.info("Creating method type with args: %s", args)
436
            new_nodes = []
437
            new_node = parent.add_method(*args)
438
            new_nodes.append(new_node)
439
            new_nodes.extend(new_node.get_children())
440
            self._after_add(new_nodes)
441
442
    @trycatchslot
443
    def _add_object_type(self):
444
        parent = self.tree_ui.get_current_node()
445
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Object Type", self.server)
446
        if ok:
447
            logger.info("Creating object type with args: %s", args)
448
            new_node = parent.add_object_type(*args)
449
            self._after_add(new_node)
450
451
    @trycatchslot
452
    def _add_folder(self):
453
        parent = self.tree_ui.get_current_node()
454
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Folder", self.server)
455
        if ok:
456
            logger.info("Creating folder with args: %s", args)
457
            new_node = parent.add_folder(*args)
458
            self._after_add(new_node)
459
460
    @trycatchslot
461
    def _add_object(self):
462
        parent = self.tree_ui.get_current_node()
463
        args, ok = NewUaObjectDialog.getArgs(self, "Add Object", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseObjectType))
464
        if ok:
465
            logger.info("Creating object with args: %s", args)
466
            nodeid, bname, otype = args
467
            new_nodes = instantiate(parent, otype, bname=bname, nodeid=nodeid)
468
            self._after_add(new_nodes)
469
470
    @trycatchslot
471
    def _add_data_type(self):
472
        parent = self.tree_ui.get_current_node()
473
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Data Type", self.server)
474
        if ok:
475
            logger.info("Creating data type with args: %s", args)
476
            new_node = parent.add_data_type(*args)
477
            self._after_add(new_node)
478
    
479
    @trycatchslot
480
    def _add_variable(self):
481
        parent = self.tree_ui.get_current_node()
482
        dtype = self.settings.value("last_datatype", None)
483
        args, ok = NewUaVariableDialog.getArgs(self, "Add Variable", self.server, default_value=9.99, dtype=dtype)
484
        if ok:
485
            logger.info("Creating variable with args: %s", args)
486
            self.settings.setValue("last_datatype", args[4])
487
            new_node = parent.add_variable(*args)
488
            self._after_add(new_node)
489
490
    @trycatchslot
491
    def _add_property(self):
492
        parent = self.tree_ui.get_current_node()
493
        dtype = self.settings.value("last_datatype", None)
494
        args, ok = NewUaVariableDialog.getArgs(self, "Add Property", self.server, default_value=9.99, dtype=dtype)
495
        if ok:
496
            logger.info("Creating property with args: %s", args)
497
            self.settings.setValue("last_datatype", args[4])
498
            new_node = parent.add_property(*args)
499
            self._after_add(new_node)
500
501
    @trycatchslot
502
    def _add_variable_type(self):
503
        parent = self.tree_ui.get_current_node()
504
        args, ok = NewUaObjectDialog.getArgs(self, "Add Variable Type", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseVariableType))
505
        if ok:
506
            logger.info("Creating variable type with args: %s", args)
507
            nodeid, bname, datatype = args
508
            new_node = parent.add_variable_type(nodeid, bname, datatype.nodeid)
509
            self._after_add(new_node)
510
511
    @trycatchslot
512
    def show_refs(self, idx=None):
513
        node = self.get_current_node(idx)
514
        self.refs_ui.show_refs(node)
515
516
    @trycatchslot
517
    def show_attrs(self, idx=None):
518
        if not isinstance(idx, QModelIndex):
519
            idx = None
520
        node = self.get_current_node(idx)
521
        self.attrs_ui.show_attrs(node)
522
523
    def show_error(self, msg):
524
        self.ui.statusBar.show()
525
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
526
        self.ui.statusBar.showMessage(str(msg))
527
        QTimer.singleShot(2500, self.ui.statusBar.hide)
528
529
    def show_msg(self, msg):
530
        self.ui.statusBar.show()
531
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
532
        self.ui.statusBar.showMessage(str(msg))
533
        QTimer.singleShot(1500, self.ui.statusBar.hide)
534
535
    def get_current_node(self, idx=None):
536
        return self.tree_ui.get_current_node(idx)
537
    
538
    def nodesets_change(self, data):
539
        self.idx_ui.reload()
540
        self.tree_ui.reload()
541
        self.refs_ui.clear()
542
        self.attrs_ui.clear()
543
544
    def closeEvent(self, event):
545
        if not self._close_model():
546
            event.ignore()
547
            return
548
        self.attrs_ui.save_state()
549
        self.refs_ui.save_state()
550
        self.settings.setValue("last_dir", self._last_dir)
551
        self.settings.setValue("main_window_width", self.size().width())
552
        self.settings.setValue("main_window_height", self.size().height())
553
        self.settings.setValue("main_window_state", self.saveState())
554
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
555
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
556
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
557
        if self.server:
558
            self.server.stop()
559
        event.accept()
560
561
562
def main():
563
    app = QApplication(sys.argv)
564
    modeler = UaModeler()
565
    handler = QtHandler(modeler.ui.logTextEdit)
566
    logging.getLogger().addHandler(handler)
567
    logging.getLogger("uamodeler").setLevel(logging.INFO)
568
    logging.getLogger("uawidgets").setLevel(logging.INFO)
569
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
570
    modeler.show()
571
    sys.exit(app.exec_())
572
573
574
if __name__ == "__main__":
575
    main()
576