Completed
Push — master ( 562337...d7061b )
by Olivier
01:06
created

ModelManagerUI._save_as()   B

Complexity

Conditions 5

Size

Total Lines 16

Duplication

Lines 16
Ratio 100 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 16
loc 16
rs 8.5454
cc 5
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.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
352
        self.attrs_ui.error.connect(self.show_error)
353
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
354
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
355
        self.nodesets_ui.error.connect(self.show_error)
356
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
357
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
358
359
        self.ui.treeView.activated.connect(self.show_refs)
360
        self.ui.treeView.clicked.connect(self.show_refs)
361
        self.ui.treeView.activated.connect(self.show_attrs)
362
        self.ui.treeView.clicked.connect(self.show_attrs)
363
364
        self.model_mgr = ModelManagerUI(self)
365
        self.model_mgr.error.connect(self.show_error)
366
        self.actions = ActionsManager(self.ui, self.model_mgr)
367
368
        self.setup_context_menu_tree()
369
370
        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.get_new_nodes())
371
        self.ui.treeView.setItemDelegate(delegate)
372
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
373
374
    def get_current_node(self, idx=None):
375
        return self.tree_ui.get_current_node(idx)
376
377
    def clear_all_widgets(self):
378
        self.tree_ui.clear()
379
        self.refs_ui.clear()
380
        self.attrs_ui.clear()
381
        self.idx_ui.clear()
382
        self.nodesets_ui.clear()
383
384
    @trycatchslot
385
    def _update_actions_state(self, current, previous):
386
        node = self.get_current_node(current)
387
        self.actions.update_actions_states(node)
388
389
    def setup_context_menu_tree(self):
390
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
391
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
392
        self._contextMenu = QMenu()
393
394
        # tree view menu
395
        self._contextMenu.addAction(self.ui.actionCopy)
396
        self._contextMenu.addAction(self.ui.actionPaste)
397
        self._contextMenu.addAction(self.ui.actionDelete)
398
        self._contextMenu.addSeparator()
399
        self._contextMenu.addAction(self.tree_ui.actionReload)
400
        self._contextMenu.addSeparator()
401
        self._contextMenu.addAction(self.ui.actionAddFolder)
402
        self._contextMenu.addAction(self.ui.actionAddObject)
403
        self._contextMenu.addAction(self.ui.actionAddVariable)
404
        self._contextMenu.addAction(self.ui.actionAddProperty)
405
        self._contextMenu.addAction(self.ui.actionAddMethod)
406
        self._contextMenu.addAction(self.ui.actionAddObjectType)
407
        self._contextMenu.addAction(self.ui.actionAddVariableType)
408
        self._contextMenu.addAction(self.ui.actionAddDataType)
409
410
    def _show_context_menu_tree(self, position):
411
        print("SHOW REQUEST")
412
        node = self.tree_ui.get_current_node()
413
        if node:
414
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
415
416
    def _restore_ui_geometri(self):
417
        self.resize(int(self.settings.value("main_window_width", 800)),
418
                    int(self.settings.value("main_window_height", 600)))
419
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
420
        self.restoreState(self.settings.value("main_window_state", bytearray()))
421
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
422
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
423
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
424
    def update_title(self, path):
425
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
426
427
    def show_error(self, msg):
428
        self.ui.statusBar.show()
429
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
430
        self.ui.statusBar.showMessage(str(msg))
431
        QTimer.singleShot(2500, self.ui.statusBar.hide)
432
433
    def show_msg(self, msg):
434
        self.ui.statusBar.show()
435
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
436
        self.ui.statusBar.showMessage(str(msg))
437
        QTimer.singleShot(1500, self.ui.statusBar.hide)
438
439
    @trycatchslot
440
    def show_refs(self, idx=None):
441
        node = self.get_current_node(idx)
442
        self.refs_ui.show_refs(node)
443
444
    @trycatchslot
445
    def show_attrs(self, idx=None):
446
        if not isinstance(idx, QModelIndex):
447
            idx = None
448
        node = self.get_current_node(idx)
449
        self.attrs_ui.show_attrs(node)
450
451
    def nodesets_change(self, data):
452
        self.idx_ui.reload()
453
        self.tree_ui.reload()
454
        self.refs_ui.clear()
455
        self.attrs_ui.clear()
456
457
    def closeEvent(self, event):
458
        if not self.model_mgr.try_close_model():
459
            event.ignore()
460
            return
461
        self.attrs_ui.save_state()
462
        self.refs_ui.save_state()
463
        self.tree_ui.save_state()
464
        self.settings.setValue("main_window_width", self.size().width())
465
        self.settings.setValue("main_window_height", self.size().height())
466
        self.settings.setValue("main_window_state", self.saveState())
467
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
468
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
469
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
470
        event.accept()
471
472
473
def main():
474
    app = QApplication(sys.argv)
475
    modeler = UaModeler()
476
    handler = QtHandler(modeler.ui.logTextEdit)
477
    logging.getLogger().addHandler(handler)
478
    logging.getLogger("uamodeler").setLevel(logging.INFO)
479
    logging.getLogger("uawidgets").setLevel(logging.INFO)
480
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
481
    modeler.show()
482
    sys.exit(app.exec_())
483
484
485
if __name__ == "__main__":
486
    main()
487