ModelManagerUI.save_as()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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