Completed
Push — master ( a61ec6...aa806c )
by Olivier
49s
created

UaModeler._paste()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 12
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
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
91
92
        self.refs_ui = RefsWidget(self.ui.refView)
93
        self.refs_ui.error.connect(self.show_error)
94
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
95
        self.attrs_ui.error.connect(self.show_error)
96
        self.attrs_ui.attr_written.connect(self._attr_written)
97
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
98
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
99
        self.nodesets_ui.error.connect(self.show_error)
100
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
101
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
102
103
        self.ui.treeView.activated.connect(self.show_refs)
104
        self.ui.treeView.clicked.connect(self.show_refs)
105
        self.ui.treeView.activated.connect(self.show_attrs)
106
        self.ui.treeView.clicked.connect(self.show_attrs)
107
108
        # fix icon stuff
109
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
110
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
111
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
112
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
113
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
114
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
115
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
116
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
117
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
118
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
119
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
120
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
121
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
122
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
123
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
124
125
        self.setup_context_menu_tree()
126
127
        # actions
128
        self.ui.actionNew.triggered.connect(self._new)
129
        self.ui.actionOpen.triggered.connect(self._open)
130
        self.ui.actionCopy.triggered.connect(self._copy)
131
        self.ui.actionPaste.triggered.connect(self._paste)
132
        self.ui.actionDelete.triggered.connect(self._delete)
133
        self.ui.actionImport.triggered.connect(self._import)
134
        self.ui.actionSave.triggered.connect(self._save)
135
        self.ui.actionSaveAs.triggered.connect(self._save_as)
136
        self.ui.actionCloseModel.triggered.connect(self._close_model)
137
        self.ui.actionAddObjectType.triggered.connect(self._add_object_type)
138
        self.ui.actionAddObject.triggered.connect(self._add_object)
139
        self.ui.actionAddFolder.triggered.connect(self._add_folder)
140
        self.ui.actionAddMethod.triggered.connect(self._add_method)
141
        self.ui.actionAddDataType.triggered.connect(self._add_data_type)
142
        self.ui.actionAddVariable.triggered.connect(self._add_variable)
143
        self.ui.actionAddVariableType.triggered.connect(self._add_variable_type)
144
        self.ui.actionAddProperty.triggered.connect(self._add_property)
145
146
        self._disable_actions()
147
148
    def _update_actions_state(self, current, previous):
149
        node = self.tree_ui.get_current_node(current)
150
        if not node or node == self.server.nodes.root:
151
            self._disable_add_actions()
152
            return
153
        path = node.get_path()
154
        nodeclass = node.get_node_class()
155
        print("PATH", path, nodeclass)
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
    def _delete(self):
211
        node = self.tree_ui.get_current_node()
212
        if node:
213
            nodes = get_node_children(node)
214
            for n in nodes:
215
                n.delete()
216
                if n in self._new_nodes:
217
                    self._new_nodes.remove(n)
218
            self.tree_ui.remove_current_item()
219
220
    def _copy(self):
221
        node = self.tree_ui.get_current_node()
222
        if node:
223
            self._copy_clipboard = node
224
225
    def _paste(self):
226
        if self._copy_clipboard:
227
            parent = self.tree_ui.get_current_node()
228
            try:
229
                added_nodes = copy_node(parent, self._copy_clipboard)
230
            except Exception as ex:
231
                self.show_error(ex)
232
                raise
233
            self._new_nodes.extend(added_nodes)
234
            self.tree_ui.reload_current()
235
            self.show_refs()
236
            self._modified = True
237
238
    def _attr_written(self, attr, dv):
239
        self._modified = True
240
        if attr == ua.AttributeIds.BrowseName:
241
            self.tree_ui.update_browse_name_current_item(dv.Value.Value)
242
        elif attr == ua.AttributeIds.DisplayName:
243
            self.tree_ui.update_display_name_current_item(dv.Value.Value)
244
      
245
    def _restore_state(self):
246
        self.resize(int(self.settings.value("main_window_width", 800)),
247
                    int(self.settings.value("main_window_height", 600)))
248
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
249
        self.restoreState(self.settings.value("main_window_state", bytearray()))
250
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
251
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
252
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
253
254
    def _disable_model_actions(self):
255
        self.ui.actionImport.setEnabled(False)
256
        self.ui.actionSave.setEnabled(False)
257
        self.ui.actionSaveAs.setEnabled(False)
258
259
    def _disable_actions(self):
260
        self._disable_add_actions()
261
        self._disable_model_actions()
262
263
    def _disable_add_actions(self):
264
        self.ui.actionAddObject.setEnabled(False)
265
        self.ui.actionAddFolder.setEnabled(False)
266
        self.ui.actionAddVariable.setEnabled(False)
267
        self.ui.actionAddProperty.setEnabled(False)
268
        self.ui.actionAddDataType.setEnabled(False)
269
        self.ui.actionAddVariableType.setEnabled(False)
270
        self.ui.actionAddObjectType.setEnabled(False)
271
        self.ui.actionAddMethod.setEnabled(False)
272
273
    def _enable_model_actions(self):
274
        self.ui.actionImport.setEnabled(True)
275
        self.ui.actionSave.setEnabled(True)
276
        self.ui.actionSaveAs.setEnabled(True)
277
        #self.ui.actionAddObject.setEnabled(True)
278
        #self.ui.actionAddFolder.setEnabled(True)
279
        #self.ui.actionAddVariable.setEnabled(True)
280
        #self.ui.actionAddProperty.setEnabled(True)
281
        #self.ui.actionAddDataType.setEnabled(True)
282
        #self.ui.actionAddVariableType.setEnabled(True)
283
        #self.ui.actionAddObjectType.setEnabled(True)
284
285
    def _close_model(self):
286
        if not self.really_exit():
287
            return False
288
        self._disable_actions()
289
        self.tree_ui.clear()
290
        self.refs_ui.clear()
291
        self.attrs_ui.clear()
292
        self.idx_ui.clear()
293
        self.nodesets_ui.clear()
294
        self._current_path = None
295
        self._update_title()
296
        if self.server is not None:
297
            self.server.stop()
298
        self.server = None
299
        return True
300
301
    def _update_title(self):
302
        self.setWindowTitle("FreeOpcUa Modeler " + str(self._current_path))
303
304
    def _new(self):
305
        if not self._close_model():
306
            return
307
        self.server = Server()
308
        endpoint = "opc.tcp://0.0.0.0:48400/freeopcua/uamodeler/"
309
        print("Starting server on ", endpoint)
310
        self.server.set_endpoint(endpoint)
311
        self.server.set_server_name("OpcUa Modeler Server")
312
        # now remove freeopcua namespace, not necessary when modeling and
313
        # ensures correct idx for exported nodesets
314
        ns_node = self.server.get_node(ua.NodeId(ua.ObjectIds.Server_NamespaceArray))
315
        nss = ns_node.get_value()
316
        ns_node.set_value(nss[1:])
317
318
        del(self._new_nodes[:])  # empty list while keeping reference
319
320
        self.server.start()
321
        self.tree_ui.set_root_node(self.server.get_root_node())
322
        self.idx_ui.set_node(self.server.get_node(ua.ObjectIds.Server_NamespaceArray))
323
        self.nodesets_ui.set_server(self.server)
324
        self._modified = False
325
        self._enable_model_actions()
326
        self._current_path = "NoName"
327
        self._update_title()
328
        return True
329
330
    def _import(self):
331
        path, ok = QFileDialog.getOpenFileName(self, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
332
        if not ok:
333
            return None
334
        self._last_dir = os.path.dirname(path)
335
        try:
336
            new_nodes = self.server.import_xml(path)
337
            self._new_nodes.extend([self.server.get_node(node) for node in new_nodes])
338
            self._modified = True
339
        except Exception as ex:
340
            self.show_error(ex)
341
            raise
342
        # we maybe should only reload the imported nodes
343
        self.tree_ui.reload()
344
        self.idx_ui.reload()
345
        return path
346
347
    def _open(self):
348
        if self._new():
349
            path = self._import()
350
            if path:
351
                self._modified = False
352
                self._current_path = path
353
                self._update_title()
354
            else:
355
                self._close_model()
356
357
    def _save_as(self):
358
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
359
        if ok:
360
            self._current_path = path
361
            self._update_title()
362
            self._save()
363
364
    def _save(self):
365
        if not self._current_path or self._current_path == "NoName":
366
            path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
367
            self._current_path = path
368
            if not ok:
369
                return
370
        print("Saving to", self._current_path)
371
        print("Exporting  {} nodes: {}".format(len(self._new_nodes), self._new_nodes))
372
        print("and namespaces: ", self.server.get_namespace_array()[1:])
373
        exp = XmlExporter(self.server)
374
        uris = self.server.get_namespace_array()[1:]
375
        exp.build_etree(self._new_nodes, uris=uris)
376
        try:
377
            exp.write_xml(self._current_path)
378
        except Exception as ex:
379
            self.show_error(ex)
380
            raise
381
        self._modified = False
382
        self._update_title()
383
        self.show_msg(self._current_path + " saved")
384
385
    def really_exit(self):
386
        if self._modified:
387
            reply = QMessageBox.question(
388
                self,
389
                "OPC UA Modeler",
390
                "Model is modified, do you really want to close model?",
391
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
392
            )
393
            if reply != QMessageBox.Yes:
394
                return False
395
396
        return True
397
398
    def _after_add(self, new_nodes):
399
        if isinstance(new_nodes, (list, tuple)):
400
            self._new_nodes.extend(new_nodes)
401
        else:
402
            self._new_nodes.append(new_nodes)
403
        self.tree_ui.reload_current()
404
        self.show_refs()
405
        self._modified = True
406
407
    @trycatchslot
408
    def _add_method(self):
409
        parent = self.tree_ui.get_current_node()
410
        args, ok = NewUaMethodDialog.getArgs(self, "Add Method", self.server)
411
        if ok:
412
            new_nodes = []
413
            new_node = parent.add_method(*args)
414
            new_nodes.append(new_node)
415
            new_nodes.extend(new_node.get_children())
416
            self._after_add(new_nodes)
417
418
    @trycatchslot
419
    def _add_object_type(self):
420
        parent = self.tree_ui.get_current_node()
421
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Object Type", self.server)
422
        if ok:
423
            new_node = parent.add_object_type(*args)
424
            self._after_add(new_node)
425
426
    @trycatchslot
427
    def _add_folder(self):
428
        parent = self.tree_ui.get_current_node()
429
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Folder", self.server)
430
        if ok:
431
            new_node = parent.add_folder(*args)
432
            self._after_add(new_node)
433
434
    @trycatchslot
435
    def _add_object(self):
436
        parent = self.tree_ui.get_current_node()
437
        args, ok = NewUaObjectDialog.getArgs(self, "Add Object", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseObjectType))
438
        if ok:
439
            nodeid, bname, otype = args
440
            new_nodes = instantiate(parent, otype, bname=bname, nodeid=nodeid)
441
            self._after_add(new_nodes)
442
443
    @trycatchslot
444
    def _add_data_type(self):
445
        parent = self.tree_ui.get_current_node()
446
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Data Type", self.server)
447
        if ok:
448
            new_node = parent.add_data_type(*args)
449
            self._after_add(new_node)
450
    
451
    @trycatchslot
452
    def _add_variable(self):
453
        parent = self.tree_ui.get_current_node()
454
        args, ok = NewUaVariableDialog.getArgs(self, "Add Variable", self.server, default_value=9.99)
455
        if ok:
456
            new_node = parent.add_variable(*args)
457
            self._after_add(new_node)
458
459
    @trycatchslot
460
    def _add_property(self):
461
        parent = self.tree_ui.get_current_node()
462
        args, ok = NewUaVariableDialog.getArgs(self, "Add Property", self.server, default_value=9.99)
463
        if ok:
464
            new_node = parent.add_property(*args)
465
            self._after_add(new_node)
466
467
    @trycatchslot
468
    def _add_variable_type(self):
469
        parent = self.tree_ui.get_current_node()
470
        args, ok = NewUaObjectDialog.getArgs(self, "Add Variable Type", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseVariableType))
471
        if ok:
472
            nodeid, bname, datatype = args
473
            new_node = parent.add_variable_type(nodeid, bname, datatype.nodeid)
474
            self._after_add(new_node)
475
476
    @trycatchslot
477
    def show_refs(self, idx=None):
478
        node = self.get_current_node(idx)
479
        self.refs_ui.show_refs(node)
480
481
    @trycatchslot
482
    def show_attrs(self, idx=None):
483
        if not isinstance(idx, QModelIndex):
484
            idx = None
485
        node = self.get_current_node(idx)
486
        self.attrs_ui.show_attrs(node)
487
488
    def show_error(self, msg):
489
        logger.warning("showing error: %s", msg)
490
        self.ui.statusBar.show()
491
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
492
        self.ui.statusBar.showMessage(str(msg))
493
        QTimer.singleShot(1500, self.ui.statusBar.hide)
494
495
    def show_msg(self, msg):
496
        self.ui.statusBar.show()
497
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
498
        self.ui.statusBar.showMessage(str(msg))
499
        QTimer.singleShot(1500, self.ui.statusBar.hide)
500
501
    def get_current_node(self, idx=None):
502
        return self.tree_ui.get_current_node(idx)
503
    
504
    def nodesets_change(self, data):
505
        self.idx_ui.reload()
506
        self.tree_ui.reload()
507
        self.refs_ui.clear()
508
        self.attrs_ui.clear()
509
510
    def closeEvent(self, event):
511
        if not self._close_model():
512
            event.ignore()
513
            return
514
        self.attrs_ui.save_state()
515
        self.refs_ui.save_state()
516
        self.settings.setValue("last_dir", self._last_dir)
517
        self.settings.setValue("main_window_width", self.size().width())
518
        self.settings.setValue("main_window_height", self.size().height())
519
        self.settings.setValue("main_window_state", self.saveState())
520
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
521
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
522
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
523
        if self.server:
524
            self.server.stop()
525
        event.accept()
526
527
528
def main():
529
    app = QApplication(sys.argv)
530
    modeler = UaModeler()
531
    modeler.show()
532
    sys.exit(app.exec_())
533
534
535
if __name__ == "__main__":
536
    main()
537