Completed
Push — master ( 665f11...a61ec6 )
by Olivier
51s
created

trycatchslot()   A

Complexity

Conditions 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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