Completed
Push — master ( c71436...5d68fd )
by Olivier
59s
created

UaModeler.enable_model_actions()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
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)
109
        self.ui.actionOpen.triggered.connect(self._open)
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)
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(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 not self.really_exit():
221
            return False
222
        self.model_mgr.close_model()
223
        return True
224
225
    def _get_xml(self):
226
        path, ok = QFileDialog.getOpenFileName(self, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
227
        if ok:
228
            self._last_dir = os.path.dirname(path)
229
        return path, ok
230
231
    @trycatchslot
232
    def _open(self):
233
        if not self._close_model():
234
            return
235
        path, ok = self._get_xml()
236
        if not ok:
237
            return
238
        self.modee_mgr.open_model(path)
239
240
    @trycatchslot
241
    def _import_slot(self):
242
        path, ok = self._get_xml()
243
        if not ok:
244
            return None
245
        self.model_mgr.import_xml(path)
246
247
    @trycatchslot
248
    def _save_as_slot(self):
249
        self._save_as()
250
251
    def _save_as(self):
252
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
253
        if ok:
254
            if os.path.isfile(path):
255
                reply = QMessageBox.question(
256
                    self,
257
                    "OPC UA Modeler",
258
                    "File already exit, do you really want to save to this file?",
259
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
260
                )
261
                if reply != QMessageBox.Yes:
262
                    return
263
            self.model_mgr.save_model(path)
264
265
    @trycatchslot
266
    def _save_slot(self):
267
        if not self.model_mgr.current_path:
268
            self.save_as(self)
269
        else:
270
            self.model_mgr.save_model()
271
272
    def _restore_state(self):
273
        self.resize(int(self.settings.value("main_window_width", 800)),
274
                    int(self.settings.value("main_window_height", 600)))
275
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
276
        self.restoreState(self.settings.value("main_window_state", bytearray()))
277
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
278
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
279
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
280
281
    def disable_model_actions(self):
282
        self.ui.actionImport.setEnabled(False)
283
        self.ui.actionSave.setEnabled(False)
284
        self.ui.actionSaveAs.setEnabled(False)
285
286
    def disable_actions(self):
287
        self.disable_add_actions()
288
        self.disable_model_actions()
289
290
    def disable_add_actions(self):
291
        self.ui.actionPaste.setEnabled(False)
292
        self.ui.actionCopy.setEnabled(False)
293
        self.ui.actionDelete.setEnabled(False)
294
        self.ui.actionAddObject.setEnabled(False)
295
        self.ui.actionAddFolder.setEnabled(False)
296
        self.ui.actionAddVariable.setEnabled(False)
297
        self.ui.actionAddProperty.setEnabled(False)
298
        self.ui.actionAddDataType.setEnabled(False)
299
        self.ui.actionAddVariableType.setEnabled(False)
300
        self.ui.actionAddObjectType.setEnabled(False)
301
        self.ui.actionAddMethod.setEnabled(False)
302
303
    def enable_model_actions(self):
304
        self.ui.actionImport.setEnabled(True)
305
        self.ui.actionSave.setEnabled(True)
306
        self.ui.actionSaveAs.setEnabled(True)
307
        #self.ui.actionAddObject.setEnabled(True)
308
        #self.ui.actionAddFolder.setEnabled(True)
309
        #self.ui.actionAddVariable.setEnabled(True)
310
        #self.ui.actionAddProperty.setEnabled(True)
311
        #self.ui.actionAddDataType.setEnabled(True)
312
        #self.ui.actionAddVariableType.setEnabled(True)
313
        #self.ui.actionAddObjectType.setEnabled(True)
314
315
    def update_title(self, path):
316
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
317
318
    def really_exit(self):
319
        if self.model_mgr.modified:
320
            reply = QMessageBox.question(
321
                self,
322
                "OPC UA Modeler",
323
                "Model is modified, do you really want to close model?",
324
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
325
            )
326
            if reply != QMessageBox.Yes:
327
                return False
328
329
        return True
330
331
    @trycatchslot
332
    def _add_method(self):
333
        args, ok = NewUaMethodDialog.getArgs(self, "Add Method", self.model_mgr.server_mgr)
334
        if ok:
335
            self.model_mgr.add_method(args)
336
337
    @trycatchslot
338
    def _add_object_type(self):
339
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Object Type", self.model_mgr.server_mgr)
340
        if ok:
341
            self.model_mgr.add_object_type(args)
342
343
    @trycatchslot
344
    def _add_folder(self):
345
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Folder", self.model_mgr.server_mgr)
346
        if ok:
347
            self.model_mgr.add_folder(args)
348
349
    @trycatchslot
350
    def _add_object(self):
351
        args, ok = NewUaObjectDialog.getArgs(self, "Add Object", self.model_mgr.server_mgr, base_node_type=self.model_mgr.server_mgr.nodes.base_object_type)
352
        if ok:
353
            self.model_mgr.add_object(args)
354
355
    @trycatchslot
356
    def _add_data_type(self):
357
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Data Type", self.model_mgr.server_mgr)
358
        if ok:
359
            self.model_mgr.add_data_type(args)
360
    
361
    @trycatchslot
362
    def _add_variable(self):
363
        dtype = self.settings.value("last_datatype", None)
364
        args, ok = NewUaVariableDialog.getArgs(self, "Add Variable", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
365
        if ok:
366
            self.model_mgr.add_variable(args)
367
            self.settings.setValue("last_datatype", args[4])
368
369
    @trycatchslot
370
    def _add_property(self):
371
        dtype = self.settings.value("last_datatype", None)
372
        args, ok = NewUaVariableDialog.getArgs(self, "Add Property", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
373
        if ok:
374
            self.model_mgr.add_property(args)
375
376
    @trycatchslot
377
    def _add_variable_type(self):
378
        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))
379
        if ok:
380
            self.model_mgr.add_variable_type(args)
381
382
    @trycatchslot
383
    def show_refs(self, idx=None):
384
        node = self.get_current_node(idx)
385
        self.refs_ui.show_refs(node)
386
387
    @trycatchslot
388
    def show_attrs(self, idx=None):
389
        if not isinstance(idx, QModelIndex):
390
            idx = None
391
        node = self.get_current_node(idx)
392
        self.attrs_ui.show_attrs(node)
393
394
    def show_error(self, msg):
395
        self.ui.statusBar.show()
396
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
397
        self.ui.statusBar.showMessage(str(msg))
398
        QTimer.singleShot(2500, self.ui.statusBar.hide)
399
400
    def show_msg(self, msg):
401
        self.ui.statusBar.show()
402
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
403
        self.ui.statusBar.showMessage(str(msg))
404
        QTimer.singleShot(1500, self.ui.statusBar.hide)
405
406
    def get_current_node(self, idx=None):
407
        return self.tree_ui.get_current_node(idx)
408
    
409
    def nodesets_change(self, data):
410
        self.idx_ui.reload()
411
        self.tree_ui.reload()
412
        self.refs_ui.clear()
413
        self.attrs_ui.clear()
414
415
    def closeEvent(self, event):
416
        if not self.model_mgr.close_model():
417
            event.ignore()
418
            return
419
        self.attrs_ui.save_state()
420
        self.refs_ui.save_state()
421
        self.settings.setValue("last_dir", self._last_dir)
422
        self.settings.setValue("main_window_width", self.size().width())
423
        self.settings.setValue("main_window_height", self.size().height())
424
        self.settings.setValue("main_window_state", self.saveState())
425
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
426
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
427
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
428
        self.model_mgr.server_mgr.close()
429
        event.accept()
430
431
432
def main():
433
    app = QApplication(sys.argv)
434
    modeler = UaModeler()
435
    handler = QtHandler(modeler.ui.logTextEdit)
436
    logging.getLogger().addHandler(handler)
437
    logging.getLogger("uamodeler").setLevel(logging.INFO)
438
    logging.getLogger("uawidgets").setLevel(logging.INFO)
439
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
440
    modeler.show()
441
    sys.exit(app.exec_())
442
443
444
if __name__ == "__main__":
445
    main()
446