Completed
Push — master ( 83145d...562337 )
by Olivier
01:02
created

ModelManagerUI.delete()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
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, 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_dir = self.settings.value("last_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
    def _get_xml(self):
224
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
225
        if ok:
226
            self._last_dir = os.path.dirname(path)
227
            self.settings.setValue("last_dir", self._last_dir)
228
        return path, ok
229
230
    @trycatchslot
231
    def open(self):
232
        if not self.try_close_model():
233
            return
234
        path, ok = self._get_xml()
235
        if not ok:
236
            return
237
        self._model_mgr.open_model(path)
238
239
    @trycatchslot
240
    def import_xml(self):
241
        path, ok = self._get_xml()
242
        if not ok:
243
            return None
244
        self._model_mgr.import_xml(path)
245
246
    @trycatchslot
247
    def save_as(self):
248
        self._save_as()
249
250
    def _save_as(self):
251
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
252
        if ok:
253
            if os.path.isfile(path):
254
                reply = QMessageBox.question(
255
                    self.modeler,
256
                    "OPC UA Modeler",
257
                    "File already exit, do you really want to save to this file?",
258
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
259
                )
260
                if reply != QMessageBox.Yes:
261
                    return
262
            self._model_mgr.save_model(path)
263
264
    @trycatchslot
265
    def save(self):
266
        if not self._model_mgr.current_path:
267
            self.save_as()
268
        else:
269
            self._model_mgr.save_model()
270
271
    @trycatchslot
272
    def add_method(self):
273
        args, ok = NewUaMethodDialog.getArgs(self.modeler, "Add Method", self._model_mgr.server_mgr)
274
        if ok:
275
            self._model_mgr.add_method(*args)
276
277
    @trycatchslot
278
    def add_object_type(self):
279
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self._model_mgr.server_mgr)
280
        if ok:
281
            self._model_mgr.add_object_type(*args)
282
283
    @trycatchslot
284
    def add_folder(self):
285
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self._model_mgr.server_mgr)
286
        if ok:
287
            self._model_mgr.add_folder(*args)
288
289
    @trycatchslot
290
    def add_object(self):
291
        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)
292
        if ok:
293
            self._model_mgr.add_object(*args)
294
295
    @trycatchslot
296
    def add_data_type(self):
297
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self._model_mgr.server_mgr)
298
        if ok:
299
            self._model_mgr.add_data_type(*args)
300
    
301
    @trycatchslot
302
    def add_variable(self):
303
        dtype = self.settings.value("last_datatype", None)
304
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
305
        if ok:
306
            self._model_mgr.add_variable(*args)
307
            self.settings.setValue("last_datatype", args[4])
308
309
    @trycatchslot
310
    def add_property(self):
311
        dtype = self.settings.value("last_datatype", None)
312
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
313
        if ok:
314
            self._model_mgr.add_property(*args)
315
316
    @trycatchslot
317
    def add_variable_type(self):
318
        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))
319
        if ok:
320
            self._model_mgr.add_variable_type(*args)
321
322
323
class UaModeler(QMainWindow):
324
    """
325
    Main class of modeler. Should be as simple as possible, try to push things to other classes
326
    or even better python-opcua
327
    """
328
329
    def __init__(self):
330
        QMainWindow.__init__(self)
331
        self.ui = Ui_UaModeler()
332
        self.ui.setupUi(self)
333
        self.setWindowIcon(QIcon(":/network.svg"))
334
335
        # we only show statusbar in case of errors
336
        self.ui.statusBar.hide()
337
338
        # setup QSettings for application and get a settings object
339
        QCoreApplication.setOrganizationName("FreeOpcUa")
340
        QCoreApplication.setApplicationName("OpcUaModeler")
341
        self.settings = QSettings()
342
343
        self._restore_ui_geometri()
344
        
345
        self.tree_ui = TreeWidget(self.ui.treeView)
346
        self.tree_ui.error.connect(self.show_error)
347
348
        self.refs_ui = RefsWidget(self.ui.refView)
349
        self.refs_ui.error.connect(self.show_error)
350
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
351
        self.attrs_ui.error.connect(self.show_error)
352
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
353
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
354
        self.nodesets_ui.error.connect(self.show_error)
355
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
356
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
357
358
        self.ui.treeView.activated.connect(self.show_refs)
359
        self.ui.treeView.clicked.connect(self.show_refs)
360
        self.ui.treeView.activated.connect(self.show_attrs)
361
        self.ui.treeView.clicked.connect(self.show_attrs)
362
363
        self.model_mgr = ModelManagerUI(self)
364
        self.model_mgr.error.connect(self.show_error)
365
        self.actions = ActionsManager(self.ui, self.model_mgr)
366
        self.setup_context_menu_tree()
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.ui.actionAddFolder)
400
        self._contextMenu.addAction(self.ui.actionAddObject)
401
        self._contextMenu.addAction(self.ui.actionAddVariable)
402
        self._contextMenu.addAction(self.ui.actionAddProperty)
403
        self._contextMenu.addAction(self.ui.actionAddMethod)
404
        self._contextMenu.addAction(self.ui.actionAddObjectType)
405
        self._contextMenu.addAction(self.ui.actionAddVariableType)
406
        self._contextMenu.addAction(self.ui.actionAddDataType)
407
408
    def _show_context_menu_tree(self, position):
409
        node = self.tree_ui.get_current_node()
410
        if node:
411
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
412
413
    def _restore_ui_geometri(self):
414
        self.resize(int(self.settings.value("main_window_width", 800)),
415
                    int(self.settings.value("main_window_height", 600)))
416
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
417
        self.restoreState(self.settings.value("main_window_state", bytearray()))
418
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
419
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
420
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
421
    def update_title(self, path):
422
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
423
424
    def show_error(self, msg):
425
        self.ui.statusBar.show()
426
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
427
        self.ui.statusBar.showMessage(str(msg))
428
        QTimer.singleShot(2500, self.ui.statusBar.hide)
429
430
    def show_msg(self, msg):
431
        self.ui.statusBar.show()
432
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
433
        self.ui.statusBar.showMessage(str(msg))
434
        QTimer.singleShot(1500, self.ui.statusBar.hide)
435
436
    @trycatchslot
437
    def show_refs(self, idx=None):
438
        node = self.get_current_node(idx)
439
        self.refs_ui.show_refs(node)
440
441
    @trycatchslot
442
    def show_attrs(self, idx=None):
443
        if not isinstance(idx, QModelIndex):
444
            idx = None
445
        node = self.get_current_node(idx)
446
        self.attrs_ui.show_attrs(node)
447
448
    def nodesets_change(self, data):
449
        self.idx_ui.reload()
450
        self.tree_ui.reload()
451
        self.refs_ui.clear()
452
        self.attrs_ui.clear()
453
454
    def closeEvent(self, event):
455
        if not self.model_mgr.try_close_model():
456
            event.ignore()
457
            return
458
        self.attrs_ui.save_state()
459
        self.refs_ui.save_state()
460
        self.settings.setValue("main_window_width", self.size().width())
461
        self.settings.setValue("main_window_height", self.size().height())
462
        self.settings.setValue("main_window_state", self.saveState())
463
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
464
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
465
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
466
        event.accept()
467
468
469
def main():
470
    app = QApplication(sys.argv)
471
    modeler = UaModeler()
472
    handler = QtHandler(modeler.ui.logTextEdit)
473
    logging.getLogger().addHandler(handler)
474
    logging.getLogger("uamodeler").setLevel(logging.INFO)
475
    logging.getLogger("uawidgets").setLevel(logging.INFO)
476
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
477
    modeler.show()
478
    sys.exit(app.exec_())
479
480
481
if __name__ == "__main__":
482
    main()
483