Completed
Push — master ( 1e66b3...ddc428 )
by Olivier
59s
created

UaModeler   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 154
Duplicated Lines 0 %

Importance

Changes 50
Bugs 5 Features 7
Metric Value
wmc 18
c 50
b 5
f 7
dl 0
loc 154
rs 10

15 Methods

Rating   Name   Duplication   Size   Complexity  
A get_current_node() 0 2 1
B __init__() 0 44 1
A setup_context_menu_tree() 0 20 1
A get_current_server() 0 5 1
A show_msg() 0 5 1
A show_attrs() 0 6 2
A nodesets_change() 0 5 1
A _restore_ui_geometri() 0 8 1
A clear_all_widgets() 0 6 1
A show_refs() 0 4 1
A update_title() 0 2 1
A _show_context_menu_tree() 0 5 2
A show_error() 0 5 1
A _update_actions_state() 0 4 1
A closeEvent() 0 14 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, QObject, pyqtSignal
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 ActionsManager(object):
46
    """
47
    Manage actions of Modeler
48
    """
49
50
    def __init__(self, ui, model_mgr):
51
        self.ui = ui
52
        self.model_mgr = model_mgr
53
54
        self._fix_icons()
55
        # actions
56
        self.ui.actionNew.triggered.connect(self.model_mgr.new)
57
        self.ui.actionOpen.triggered.connect(self.model_mgr.open)
58
        self.ui.actionCopy.triggered.connect(self.model_mgr.copy)
59
        self.ui.actionPaste.triggered.connect(self.model_mgr.paste)
60
        self.ui.actionDelete.triggered.connect(self.model_mgr.delete)
61
        self.ui.actionImport.triggered.connect(self.model_mgr.import_xml)
62
        self.ui.actionSave.triggered.connect(self.model_mgr.save)
63
        self.ui.actionSaveAs.triggered.connect(self.model_mgr.save_as)
64
        self.ui.actionCloseModel.triggered.connect(self.model_mgr.close_model)
65
        self.ui.actionAddObjectType.triggered.connect(self.model_mgr.add_object_type)
66
        self.ui.actionAddObject.triggered.connect(self.model_mgr.add_object)
67
        self.ui.actionAddFolder.triggered.connect(self.model_mgr.add_folder)
68
        self.ui.actionAddMethod.triggered.connect(self.model_mgr.add_method)
69
        self.ui.actionAddDataType.triggered.connect(self.model_mgr.add_data_type)
70
        self.ui.actionAddVariable.triggered.connect(self.model_mgr.add_variable)
71
        self.ui.actionAddVariableType.triggered.connect(self.model_mgr.add_variable_type)
72
        self.ui.actionAddProperty.triggered.connect(self.model_mgr.add_property)
73
74
        self.disable_all_actions()
75
76
    def _fix_icons(self):
77
        # fix icon stuff
78
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
79
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
80
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
81
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
82
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
83
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
84
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
85
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
86
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
87
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
88
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
89
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
90
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
91
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
92
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
93
94
    def update_actions_states(self, node):
95
        self.disable_add_actions()
96
        if not node or node in (self.model_mgr.get_current_server().nodes.root, 
97
                                self.model_mgr.get_current_server().nodes.types, 
98
                                self.model_mgr.get_current_server().nodes.event_types, 
99
                                self.model_mgr.get_current_server().nodes.object_types, 
100
                                self.model_mgr.get_current_server().nodes.reference_types, 
101
                                self.model_mgr.get_current_server().nodes.variable_types, 
102
                                self.model_mgr.get_current_server().nodes.data_types):
103
            return
104
        path = node.get_path()
105
        nodeclass = node.get_node_class()
106
107
        self.ui.actionCopy.setEnabled(True)
108
        self.ui.actionDelete.setEnabled(True)
109
110
        if nodeclass == ua.NodeClass.Variable:
111
            return
112
113
        self.ui.actionPaste.setEnabled(True)
114
115
        if self.model_mgr.get_current_server().nodes.base_object_type in path:
116
            self.ui.actionAddObjectType.setEnabled(True)
117
118
        if self.model_mgr.get_current_server().nodes.base_variable_type in path:
119
            self.ui.actionAddVariableType.setEnabled(True)
120
121
        if self.model_mgr.get_current_server().nodes.base_data_type in path:
122
            self.ui.actionAddDataType.setEnabled(True)
123
            if self.model_mgr.get_current_server().nodes.enum_data_type in path:
124
                self.ui.actionAddProperty.setEnabled(True)
125
            return  # not other nodes should be added here
126
127
        self.ui.actionAddFolder.setEnabled(True)
128
        self.ui.actionAddObject.setEnabled(True)
129
        self.ui.actionAddVariable.setEnabled(True)
130
        self.ui.actionAddProperty.setEnabled(True)
131
        self.ui.actionAddMethod.setEnabled(True)
132
133
    def disable_model_actions(self):
134
        self.ui.actionImport.setEnabled(False)
135
        self.ui.actionSave.setEnabled(False)
136
        self.ui.actionSaveAs.setEnabled(False)
137
138
    def disable_all_actions(self):
139
        self.disable_add_actions()
140
        self.disable_model_actions()
141
142
    def disable_add_actions(self):
143
        self.ui.actionPaste.setEnabled(False)
144
        self.ui.actionCopy.setEnabled(False)
145
        self.ui.actionDelete.setEnabled(False)
146
        self.ui.actionAddObject.setEnabled(False)
147
        self.ui.actionAddFolder.setEnabled(False)
148
        self.ui.actionAddVariable.setEnabled(False)
149
        self.ui.actionAddProperty.setEnabled(False)
150
        self.ui.actionAddDataType.setEnabled(False)
151
        self.ui.actionAddVariableType.setEnabled(False)
152
        self.ui.actionAddObjectType.setEnabled(False)
153
        self.ui.actionAddMethod.setEnabled(False)
154
155
    def enable_model_actions(self):
156
        self.ui.actionImport.setEnabled(True)
157
        self.ui.actionSave.setEnabled(True)
158
        self.ui.actionSaveAs.setEnabled(True)
159
160
161
class ModelManagerUI(QObject):
162
    """
163
    Interface to ModelMgr that displays dialogs to interact with users.
164
    Logic is inside ModelManager, this class only handle the UI and dialogs
165
    """
166
167
    error = pyqtSignal(Exception)
168
169
    def __init__(self, modeler):
170
        QObject.__init__(self)
171
        self.modeler = modeler
172
        self._model_mgr = ModelManager(modeler)
173
        self._model_mgr.error.connect(self.error)
174
        self.settings = QSettings()
175
        self._last_model_dir = self.settings.value("last_model_dir", ".")
176
        self._copy_clipboard = None
177
178
    def get_current_server(self):
179
        return self._model_mgr.server_mgr
180
181
    def get_new_nodes(self):
182
        return self._model_mgr.new_nodes
183
184
    @trycatchslot
185
    def new(self):
186
        if not self.try_close_model():
187
            return
188
        self._model_mgr.new_model()
189
190
    @trycatchslot
191
    def delete(self):
192
        node = self.modeler.get_current_node()
193
        self._model_mgr.delete_node(node)
194
195
    @trycatchslot
196
    def copy(self):
197
        node = self.modeler.get_current_node()
198
        if node:
199
            self._copy_clipboard = node
200
201
    @trycatchslot
202
    def paste(self):
203
        if self._copy_clipboard:
204
            self._model_mgr.paste_node(self._copy_clipboard)
205
206
    @trycatchslot
207
    def close_model(self):
208
        self.try_close_model()
209
210
    def try_close_model(self):
211
        if self._model_mgr.modified:
212
            reply = QMessageBox.question(
213
                self.modeler,
214
                "OPC UA Modeler",
215
                "Model is modified, do you really want to close model?",
216
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
217
            )
218
            if reply != QMessageBox.Yes:
219
                return False
220
        self._model_mgr.close_model(force=True)
221
        return True
222
223 View Code Duplication
    @trycatchslot
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
224
    def open(self):
225
        if not self.try_close_model():
226
            return
227
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_model_dir)
228
        if not ok:
229
            return
230
        if self._last_model_dir != os.path.dirname(path):
231
            self._last_model_dir = os.path.dirname(path)
232
            self.settings.setValue("last_model_dir", self._last_model_dir)
233
        self._model_mgr.open_model(path)
234
235
    @trycatchslot
236
    def import_xml(self):
237
        last_import_dir = self.settings.value("last_import_dir", ".")
238
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Import reference OPC UA XML", filter="XML Files (*.xml *.XML)", directory=last_import_dir)
239
        if not ok:
240
            return None
241
        self.settings.setValue("last_import_dir", last_import_dir)
242
        self._model_mgr.import_xml(path)
243
244
    @trycatchslot
245
    def save_as(self):
246
        self._save_as()
247
248 View Code Duplication
    def _save_as(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
249
        path, ok = QFileDialog.getSaveFileName(self.modeler, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
250
        if ok:
251
            if os.path.isfile(path):
252
                reply = QMessageBox.question(
253
                    self.modeler,
254
                    "OPC UA Modeler",
255
                    "File already exit, do you really want to save to this file?",
256
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
257
                )
258
                if reply != QMessageBox.Yes:
259
                    return
260
            if self._last_model_dir != os.path.dirname(path):
261
                self._last_model_dir = os.path.dirname(path)
262
                self.settings.setValue("last_model_dir", self._last_model_dir)
263
            self._model_mgr.save_model(path)
264
265
    @trycatchslot
266
    def save(self):
267
        if not self._model_mgr.current_path:
268
            self.save_as()
269
        else:
270
            self._model_mgr.save_model()
271
272
    @trycatchslot
273
    def add_method(self):
274
        args, ok = NewUaMethodDialog.getArgs(self.modeler, "Add Method", self._model_mgr.server_mgr)
275
        if ok:
276
            self._model_mgr.add_method(*args)
277
278
    @trycatchslot
279
    def add_object_type(self):
280
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self._model_mgr.server_mgr)
281
        if ok:
282
            self._model_mgr.add_object_type(*args)
283
284
    @trycatchslot
285
    def add_folder(self):
286
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self._model_mgr.server_mgr)
287
        if ok:
288
            self._model_mgr.add_folder(*args)
289
290
    @trycatchslot
291
    def add_object(self):
292
        args, ok = NewUaObjectDialog.getArgs(self.modeler, "Add Object", self._model_mgr.server_mgr, base_node_type=self._model_mgr.server_mgr.nodes.base_object_type)
293
        if ok:
294
            self._model_mgr.add_object(*args)
295
296
    @trycatchslot
297
    def add_data_type(self):
298
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self._model_mgr.server_mgr)
299
        if ok:
300
            self._model_mgr.add_data_type(*args)
301
    
302
    @trycatchslot
303
    def add_variable(self):
304
        dtype = self.settings.value("last_datatype", None)
305
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
306
        if ok:
307
            self._model_mgr.add_variable(*args)
308
            self.settings.setValue("last_datatype", args[4])
309
310
    @trycatchslot
311
    def add_property(self):
312
        dtype = self.settings.value("last_datatype", None)
313
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
314
        if ok:
315
            self._model_mgr.add_property(*args)
316
317
    @trycatchslot
318
    def add_variable_type(self):
319
        args, ok = NewUaObjectDialog.getArgs(self.modeler, "Add Variable Type", self._model_mgr.server_mgr, base_node_type=self._model_mgr.server_mgr.get_node(ua.ObjectIds.BaseVariableType))
320
        if ok:
321
            self._model_mgr.add_variable_type(*args)
322
323
324
class UaModeler(QMainWindow):
325
    """
326
    Main class of modeler. Should be as simple as possible, try to push things to other classes
327
    or even better python-opcua
328
    """
329
330
    def __init__(self):
331
        QMainWindow.__init__(self)
332
        self.ui = Ui_UaModeler()
333
        self.ui.setupUi(self)
334
        self.setWindowIcon(QIcon(":/network.svg"))
335
336
        # we only show statusbar in case of errors
337
        self.ui.statusBar.hide()
338
339
        # setup QSettings for application and get a settings object
340
        QCoreApplication.setOrganizationName("FreeOpcUa")
341
        QCoreApplication.setApplicationName("OpcUaModeler")
342
        self.settings = QSettings()
343
344
        self._restore_ui_geometri()
345
        
346
        self.tree_ui = TreeWidget(self.ui.treeView)
347
        self.tree_ui.error.connect(self.show_error)
348
349
        self.refs_ui = RefsWidget(self.ui.refView)
350
        self.refs_ui.error.connect(self.show_error)
351
        self.refs_ui.reference_changed.connect(self.tree_ui.reload_current)  # FIXME: shoudl reload a specific node
352
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
353
        self.attrs_ui.error.connect(self.show_error)
354
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
355
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
356
        self.nodesets_ui.error.connect(self.show_error)
357
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
358
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
359
360
        self.ui.treeView.activated.connect(self.show_refs)
361
        self.ui.treeView.clicked.connect(self.show_refs)
362
        self.ui.treeView.activated.connect(self.show_attrs)
363
        self.ui.treeView.clicked.connect(self.show_attrs)
364
365
        self.model_mgr = ModelManagerUI(self)
366
        self.model_mgr.error.connect(self.show_error)
367
        self.actions = ActionsManager(self.ui, self.model_mgr)
368
369
        self.setup_context_menu_tree()
370
371
        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.get_new_nodes())
372
        self.ui.treeView.setItemDelegate(delegate)
373
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
374
375
    def get_current_node(self, idx=None):
376
        return self.tree_ui.get_current_node(idx)
377
378
    def get_current_server(self):
379
        """
380
        Used by tests
381
        """
382
        return self.model_mgr.get_current_server()
383
384
    def clear_all_widgets(self):
385
        self.tree_ui.clear()
386
        self.refs_ui.clear()
387
        self.attrs_ui.clear()
388
        self.idx_ui.clear()
389
        self.nodesets_ui.clear()
390
391
    @trycatchslot
392
    def _update_actions_state(self, current, previous):
393
        node = self.get_current_node(current)
394
        self.actions.update_actions_states(node)
395
396
    def setup_context_menu_tree(self):
397
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
398
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
399
        self._contextMenu = QMenu()
400
401
        # tree view menu
402
        self._contextMenu.addAction(self.ui.actionCopy)
403
        self._contextMenu.addAction(self.ui.actionPaste)
404
        self._contextMenu.addAction(self.ui.actionDelete)
405
        self._contextMenu.addSeparator()
406
        self._contextMenu.addAction(self.tree_ui.actionReload)
407
        self._contextMenu.addSeparator()
408
        self._contextMenu.addAction(self.ui.actionAddFolder)
409
        self._contextMenu.addAction(self.ui.actionAddObject)
410
        self._contextMenu.addAction(self.ui.actionAddVariable)
411
        self._contextMenu.addAction(self.ui.actionAddProperty)
412
        self._contextMenu.addAction(self.ui.actionAddMethod)
413
        self._contextMenu.addAction(self.ui.actionAddObjectType)
414
        self._contextMenu.addAction(self.ui.actionAddVariableType)
415
        self._contextMenu.addAction(self.ui.actionAddDataType)
416
417
    def _show_context_menu_tree(self, position):
418
        print("SHOW REQUEST")
419
        node = self.tree_ui.get_current_node()
420
        if node:
421
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
422
423
    def _restore_ui_geometri(self):
424
        self.resize(int(self.settings.value("main_window_width", 800)),
425
                    int(self.settings.value("main_window_height", 600)))
426
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
427
        self.restoreState(self.settings.value("main_window_state", bytearray()))
428
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
429
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
430
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
431
    def update_title(self, path):
432
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
433
434
    def show_error(self, msg):
435
        self.ui.statusBar.show()
436
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
437
        self.ui.statusBar.showMessage(str(msg))
438
        QTimer.singleShot(2500, self.ui.statusBar.hide)
439
440
    def show_msg(self, msg):
441
        self.ui.statusBar.show()
442
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
443
        self.ui.statusBar.showMessage(str(msg))
444
        QTimer.singleShot(1500, self.ui.statusBar.hide)
445
446
    @trycatchslot
447
    def show_refs(self, idx=None):
448
        node = self.get_current_node(idx)
449
        self.refs_ui.show_refs(node)
450
451
    @trycatchslot
452
    def show_attrs(self, idx=None):
453
        if not isinstance(idx, QModelIndex):
454
            idx = None
455
        node = self.get_current_node(idx)
456
        self.attrs_ui.show_attrs(node)
457
458
    def nodesets_change(self, data):
459
        self.idx_ui.reload()
460
        self.tree_ui.reload()
461
        self.refs_ui.clear()
462
        self.attrs_ui.clear()
463
464
    def closeEvent(self, event):
465
        if not self.model_mgr.try_close_model():
466
            event.ignore()
467
            return
468
        self.attrs_ui.save_state()
469
        self.refs_ui.save_state()
470
        self.tree_ui.save_state()
471
        self.settings.setValue("main_window_width", self.size().width())
472
        self.settings.setValue("main_window_height", self.size().height())
473
        self.settings.setValue("main_window_state", self.saveState())
474
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
475
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
476
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
477
        event.accept()
478
479
480
def main():
481
    app = QApplication(sys.argv)
482
    modeler = UaModeler()
483
    handler = QtHandler(modeler.ui.logTextEdit)
484
    logging.getLogger().addHandler(handler)
485
    logging.getLogger("uamodeler").setLevel(logging.INFO)
486
    logging.getLogger("uawidgets").setLevel(logging.INFO)
487
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
488
    modeler.show()
489
    sys.exit(app.exec_())
490
491
492
if __name__ == "__main__":
493
    main()
494