Completed
Push — master ( 5619b4...665f11 )
by Olivier
57s
created

UaModeler._attr_written()   A

Complexity

Conditions 3

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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