Completed
Push — master ( 922ee3...207079 )
by Olivier
01:00
created

UaModeler._import_slot()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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