Completed
Push — master ( 5d68fd...587311 )
by Olivier
01:31
created

UaModeler._new_slot()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 2
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
13
from uawidgets import resources
14
from uawidgets.attrs_widget import AttrsWidget
15
from uawidgets.tree_widget import TreeWidget
16
from uawidgets.refs_widget import RefsWidget
17
from uawidgets.new_node_dialogs import NewNodeBaseDialog, NewUaObjectDialog, NewUaVariableDialog, NewUaMethodDialog
18
from uawidgets.utils import trycatchslot
19
from uawidgets.logger import QtHandler
20
21
from uamodeler.uamodeler_ui import Ui_UaModeler
22
from uamodeler.namespace_widget import NamespaceWidget
23
from uamodeler.refnodesets_widget import RefNodeSetsWidget
24
from uamodeler.model_manager import ModelManager
25
26
27
logger = logging.getLogger(__name__)
28
29
30
class BoldDelegate(QStyledItemDelegate):
31
32
    def __init__(self, parent, model, added_node_list):
33
        QStyledItemDelegate.__init__(self, parent)
34
        self.added_node_list = added_node_list
35
        self.model = model
36
37
    def paint(self, painter, option, idx):
38
        new_idx = idx.sibling(idx.row(), 0)
39
        item = self.model.itemFromIndex(new_idx)
40
        if item and item.data(Qt.UserRole) in self.added_node_list:
41
            option.font.setWeight(QFont.Bold)
42
        QStyledItemDelegate.paint(self, painter, option, idx)
43
44
45
class UaModeler(QMainWindow):
46
47
    def __init__(self):
48
        QMainWindow.__init__(self)
49
        self.ui = Ui_UaModeler()
50
        self.ui.setupUi(self)
51
        self.setWindowIcon(QIcon(":/network.svg"))
52
53
        # we only show statusbar in case of errors
54
        self.ui.statusBar.hide()
55
56
        # setup QSettings for application and get a settings object
57
        QCoreApplication.setOrganizationName("FreeOpcUa")
58
        QCoreApplication.setApplicationName("OpcUaModeler")
59
        self.settings = QSettings()
60
        self._last_dir = self.settings.value("last_dir", ".")
61
62
        self._restore_state()
63
        self._copy_clipboard = None
64
65
        self.tree_ui = TreeWidget(self.ui.treeView)
66
        self.tree_ui.error.connect(self.show_error)
67
68
        self.refs_ui = RefsWidget(self.ui.refView)
69
        self.refs_ui.error.connect(self.show_error)
70
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
71
        self.attrs_ui.error.connect(self.show_error)
72
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
73
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
74
        self.nodesets_ui.error.connect(self.show_error)
75
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
76
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
77
78
        self.ui.treeView.activated.connect(self.show_refs)
79
        self.ui.treeView.clicked.connect(self.show_refs)
80
        self.ui.treeView.activated.connect(self.show_attrs)
81
        self.ui.treeView.clicked.connect(self.show_attrs)
82
83
        self.model_mgr = ModelManager(self)
84
        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.new_nodes)
85
        self.ui.treeView.setItemDelegate(delegate)
86
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
87
88
        # fix icon stuff
89
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
90
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
91
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
92
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
93
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
94
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
95
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
96
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
97
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
98
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
99
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
100
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
101
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
102
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
103
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
104
105
        self.setup_context_menu_tree()
106
107
        # actions
108
        self.ui.actionNew.triggered.connect(self._new_slot)
109
        self.ui.actionOpen.triggered.connect(self._open_slot)
110
        self.ui.actionCopy.triggered.connect(self._copy)
111
        self.ui.actionPaste.triggered.connect(self._paste)
112
        self.ui.actionDelete.triggered.connect(self._delete)
113
        self.ui.actionImport.triggered.connect(self._import_slot)
114
        self.ui.actionSave.triggered.connect(self._save_slot)
115
        self.ui.actionSaveAs.triggered.connect(self._save_as_slot)
116
        self.ui.actionCloseModel.triggered.connect(self._close_model_slot)
117
        self.ui.actionAddObjectType.triggered.connect(self._add_object_type)
118
        self.ui.actionAddObject.triggered.connect(self._add_object)
119
        self.ui.actionAddFolder.triggered.connect(self._add_folder)
120
        self.ui.actionAddMethod.triggered.connect(self._add_method)
121
        self.ui.actionAddDataType.triggered.connect(self._add_data_type)
122
        self.ui.actionAddVariable.triggered.connect(self._add_variable)
123
        self.ui.actionAddVariableType.triggered.connect(self._add_variable_type)
124
        self.ui.actionAddProperty.triggered.connect(self._add_property)
125
126
        self.disable_actions()
127
128
    def get_current_server(self):
129
        return self.model_mgr.server_mgr
130
131
    def _update_actions_state(self, current, previous):
132
        self.disable_add_actions()
133
        node = self.tree_ui.get_current_node(current)
134
        if not node or node in (self.model_mgr.server_mgr.nodes.root, 
135
                                self.model_mgr.server_mgr.nodes.types, 
136
                                self.model_mgr.server_mgr.nodes.event_types, 
137
                                self.model_mgr.server_mgr.nodes.object_types, 
138
                                self.model_mgr.server_mgr.nodes.reference_types, 
139
                                self.model_mgr.server_mgr.nodes.variable_types, 
140
                                self.model_mgr.server_mgr.nodes.data_types):
141
            return
142
        path = node.get_path()
143
        nodeclass = node.get_node_class()
144
145
        self.ui.actionAddFolder.setEnabled(True)
146
        self.ui.actionCopy.setEnabled(True)
147
        self.ui.actionPaste.setEnabled(True)
148
        self.ui.actionDelete.setEnabled(True)
149
150
        if self.model_mgr.server_mgr.nodes.base_object_type in path:
151
            self.ui.actionAddObjectType.setEnabled(True)
152
153
        if self.model_mgr.server_mgr.nodes.base_variable_type in path:
154
            self.ui.actionAddVariableType.setEnabled(True)
155
156
        if self.model_mgr.server_mgr.nodes.base_data_type in path:
157
            self.ui.actionAddDataType.setEnabled(True)
158
            if self.model_mgr.server_mgr.nodes.enum_data_type in path:
159
                self.ui.actionAddProperty.setEnabled(True)
160
            return  # not other nodes should be added here
161
162
        if nodeclass != ua.NodeClass.Variable:
163
            self.ui.actionAddFolder.setEnabled(True)
164
            self.ui.actionAddObject.setEnabled(True)
165
            self.ui.actionAddVariable.setEnabled(True)
166
            self.ui.actionAddProperty.setEnabled(True)
167
            self.ui.actionAddMethod.setEnabled(True)
168
169
    def setup_context_menu_tree(self):
170
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
171
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
172
        self._contextMenu = QMenu()
173
174
        # tree view menu
175
        self._contextMenu.addAction(self.ui.actionCopy)
176
        self._contextMenu.addAction(self.ui.actionPaste)
177
        self._contextMenu.addAction(self.ui.actionDelete)
178
        self._contextMenu.addSeparator()
179
        self._contextMenu.addAction(self.ui.actionAddFolder)
180
        self._contextMenu.addAction(self.ui.actionAddObject)
181
        self._contextMenu.addAction(self.ui.actionAddVariable)
182
        self._contextMenu.addAction(self.ui.actionAddProperty)
183
        self._contextMenu.addAction(self.ui.actionAddMethod)
184
        self._contextMenu.addAction(self.ui.actionAddObjectType)
185
        self._contextMenu.addAction(self.ui.actionAddVariableType)
186
        self._contextMenu.addAction(self.ui.actionAddDataType)
187
188
    def _show_context_menu_tree(self, position):
189
        node = self.tree_ui.get_current_node()
190
        if node:
191
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
192
193
    @trycatchslot
194
    def _new_slot(self):
195
        if not self._close_model():
196
            return
197
        self.model_mgr.new_model()
198
199
    @trycatchslot
200
    def _delete(self):
201
        node = self.tree_ui.get_current_node()
202
        self.model_mgr.delete_node(node)
203
204
    @trycatchslot
205
    def _copy(self):
206
        node = self.tree_ui.get_current_node()
207
        if node:
208
            self._copy_clipboard = node
209
210
    @trycatchslot
211
    def _paste(self):
212
        if self._copy_clipboard:
213
            self.model_mgr.paste_node(self._copy_clipboard)
214
215
    @trycatchslot
216
    def _close_model_slot(self):
217
        self._close_model()
218
219
    def _close_model(self):
220
        if self.model_mgr.modified:
221
            reply = QMessageBox.question(
222
                self,
223
                "OPC UA Modeler",
224
                "Model is modified, do you really want to close model?",
225
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
226
            )
227
            if reply != QMessageBox.Yes:
228
                return False
229
        self.model_mgr.close_model(force=True)
230
        return True
231
232
    def _get_xml(self):
233
        path, ok = QFileDialog.getOpenFileName(self, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
234
        if ok:
235
            self._last_dir = os.path.dirname(path)
236
        return path, ok
237
238
    @trycatchslot
239
    def _open_slot(self):
240
        if not self._close_model():
241
            return
242
        path, ok = self._get_xml()
243
        if not ok:
244
            return
245
        self.model_mgr.open_model(path)
246
247
    @trycatchslot
248
    def _import_slot(self):
249
        path, ok = self._get_xml()
250
        if not ok:
251
            return None
252
        self.model_mgr.import_xml(path)
253
254
    @trycatchslot
255
    def _save_as_slot(self):
256
        self._save_as()
257
258
    def _save_as(self):
259
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
260
        if ok:
261
            if os.path.isfile(path):
262
                reply = QMessageBox.question(
263
                    self,
264
                    "OPC UA Modeler",
265
                    "File already exit, do you really want to save to this file?",
266
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
267
                )
268
                if reply != QMessageBox.Yes:
269
                    return
270
            self.model_mgr.save_model(path)
271
272
    @trycatchslot
273
    def _save_slot(self):
274
        if not self.model_mgr.current_path:
275
            self.save_as(self)
276
        else:
277
            self.model_mgr.save_model()
278
279
    def _restore_state(self):
280
        self.resize(int(self.settings.value("main_window_width", 800)),
281
                    int(self.settings.value("main_window_height", 600)))
282
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
283
        self.restoreState(self.settings.value("main_window_state", bytearray()))
284
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
285
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
286
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
287
288
    def disable_model_actions(self):
289
        self.ui.actionImport.setEnabled(False)
290
        self.ui.actionSave.setEnabled(False)
291
        self.ui.actionSaveAs.setEnabled(False)
292
293
    def disable_actions(self):
294
        self.disable_add_actions()
295
        self.disable_model_actions()
296
297
    def disable_add_actions(self):
298
        self.ui.actionPaste.setEnabled(False)
299
        self.ui.actionCopy.setEnabled(False)
300
        self.ui.actionDelete.setEnabled(False)
301
        self.ui.actionAddObject.setEnabled(False)
302
        self.ui.actionAddFolder.setEnabled(False)
303
        self.ui.actionAddVariable.setEnabled(False)
304
        self.ui.actionAddProperty.setEnabled(False)
305
        self.ui.actionAddDataType.setEnabled(False)
306
        self.ui.actionAddVariableType.setEnabled(False)
307
        self.ui.actionAddObjectType.setEnabled(False)
308
        self.ui.actionAddMethod.setEnabled(False)
309
310
    def enable_model_actions(self):
311
        self.ui.actionImport.setEnabled(True)
312
        self.ui.actionSave.setEnabled(True)
313
        self.ui.actionSaveAs.setEnabled(True)
314
        #self.ui.actionAddObject.setEnabled(True)
315
        #self.ui.actionAddFolder.setEnabled(True)
316
        #self.ui.actionAddVariable.setEnabled(True)
317
        #self.ui.actionAddProperty.setEnabled(True)
318
        #self.ui.actionAddDataType.setEnabled(True)
319
        #self.ui.actionAddVariableType.setEnabled(True)
320
        #self.ui.actionAddObjectType.setEnabled(True)
321
322
    def update_title(self, path):
323
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
324
325
    @trycatchslot
326
    def _add_method(self):
327
        args, ok = NewUaMethodDialog.getArgs(self, "Add Method", self.model_mgr.server_mgr)
328
        if ok:
329
            self.model_mgr.add_method(*args)
330
331
    @trycatchslot
332
    def _add_object_type(self):
333
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Object Type", self.model_mgr.server_mgr)
334
        if ok:
335
            self.model_mgr.add_object_type(*args)
336
337
    @trycatchslot
338
    def _add_folder(self):
339
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Folder", self.model_mgr.server_mgr)
340
        if ok:
341
            self.model_mgr.add_folder(*args)
342
343
    @trycatchslot
344
    def _add_object(self):
345
        args, ok = NewUaObjectDialog.getArgs(self, "Add Object", self.model_mgr.server_mgr, base_node_type=self.model_mgr.server_mgr.nodes.base_object_type)
346
        if ok:
347
            self.model_mgr.add_object(*args)
348
349
    @trycatchslot
350
    def _add_data_type(self):
351
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Data Type", self.model_mgr.server_mgr)
352
        if ok:
353
            self.model_mgr.add_data_type(*args)
354
    
355
    @trycatchslot
356
    def _add_variable(self):
357
        dtype = self.settings.value("last_datatype", None)
358
        args, ok = NewUaVariableDialog.getArgs(self, "Add Variable", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
359
        if ok:
360
            self.model_mgr.add_variable(*args)
361
            self.settings.setValue("last_datatype", args[4])
362
363
    @trycatchslot
364
    def _add_property(self):
365
        dtype = self.settings.value("last_datatype", None)
366
        args, ok = NewUaVariableDialog.getArgs(self, "Add Property", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
367
        if ok:
368
            self.model_mgr.add_property(*args)
369
370
    @trycatchslot
371
    def _add_variable_type(self):
372
        args, ok = NewUaObjectDialog.getArgs(self, "Add Variable Type", self.model_mgr.server_mgr, base_node_type=self.model_mgr.server_mgr.get_node(ua.ObjectIds.BaseVariableType))
373
        if ok:
374
            self.model_mgr.add_variable_type(*args)
375
376
    @trycatchslot
377
    def show_refs(self, idx=None):
378
        node = self.get_current_node(idx)
379
        self.refs_ui.show_refs(node)
380
381
    @trycatchslot
382
    def show_attrs(self, idx=None):
383
        if not isinstance(idx, QModelIndex):
384
            idx = None
385
        node = self.get_current_node(idx)
386
        self.attrs_ui.show_attrs(node)
387
388
    def show_error(self, msg):
389
        self.ui.statusBar.show()
390
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
391
        self.ui.statusBar.showMessage(str(msg))
392
        QTimer.singleShot(2500, self.ui.statusBar.hide)
393
394
    def show_msg(self, msg):
395
        self.ui.statusBar.show()
396
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
397
        self.ui.statusBar.showMessage(str(msg))
398
        QTimer.singleShot(1500, self.ui.statusBar.hide)
399
400
    def get_current_node(self, idx=None):
401
        return self.tree_ui.get_current_node(idx)
402
    
403
    def nodesets_change(self, data):
404
        self.idx_ui.reload()
405
        self.tree_ui.reload()
406
        self.refs_ui.clear()
407
        self.attrs_ui.clear()
408
409
    def closeEvent(self, event):
410
        if not self._close_model():
411
            event.ignore()
412
            return
413
        self.attrs_ui.save_state()
414
        self.refs_ui.save_state()
415
        self.settings.setValue("last_dir", self._last_dir)
416
        self.settings.setValue("main_window_width", self.size().width())
417
        self.settings.setValue("main_window_height", self.size().height())
418
        self.settings.setValue("main_window_state", self.saveState())
419
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
420
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
421
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
422
        self.model_mgr.server_mgr.close()
423
        event.accept()
424
425
426
def main():
427
    app = QApplication(sys.argv)
428
    modeler = UaModeler()
429
    handler = QtHandler(modeler.ui.logTextEdit)
430
    logging.getLogger().addHandler(handler)
431
    logging.getLogger("uamodeler").setLevel(logging.INFO)
432
    logging.getLogger("uawidgets").setLevel(logging.INFO)
433
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
434
    modeler.show()
435
    sys.exit(app.exec_())
436
437
438
if __name__ == "__main__":
439
    main()
440