Completed
Push — master ( 6f923a...cff1b8 )
by Olivier
01:33
created

ModelManagerUI.setModified()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 2
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
    titleChanged = pyqtSignal(str)
169
170
    def __init__(self, modeler):
171
        QObject.__init__(self)
172
        self.modeler = modeler
173
        self._model_mgr = ModelManager(modeler)
174
        self._model_mgr.error.connect(self.error)
175
        self._model_mgr.titleChanged.connect(self.titleChanged)
176
        self.settings = QSettings()
177
        self._last_model_dir = self.settings.value("last_model_dir", ".")
178
        self._copy_clipboard = None
179
180
    def get_current_server(self):
181
        return self._model_mgr.server_mgr
182
183
    def get_new_nodes(self):
184
        return self._model_mgr.new_nodes
185
186
    def setModified(self, val=True):
187
        self._model_mgr.modified = val
188
189
    @trycatchslot
190
    def new(self):
191
        if not self.try_close_model():
192
            return
193
        self._model_mgr.new_model()
194
195
    @trycatchslot
196
    def delete(self):
197
        node = self.modeler.get_current_node()
198
        self._model_mgr.delete_node(node)
199
200
    @trycatchslot
201
    def copy(self):
202
        node = self.modeler.get_current_node()
203
        if node:
204
            self._copy_clipboard = node
205
206
    @trycatchslot
207
    def paste(self):
208
        if self._copy_clipboard:
209
            self._model_mgr.paste_node(self._copy_clipboard)
210
211
    @trycatchslot
212
    def close_model(self):
213
        self.try_close_model()
214
215
    def try_close_model(self):
216
        if self._model_mgr.modified:
217
            reply = QMessageBox.question(
218
                self.modeler,
219
                "OPC UA Modeler",
220
                "Model is modified, do you really want to close model?",
221
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
222
            )
223
            if reply != QMessageBox.Yes:
224
                return False
225
        self._model_mgr.close_model(force=True)
226
        return True
227
228
    @trycatchslot
229
    def open(self):
230
        if not self.try_close_model():
231
            return
232
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML *.uamodel)", directory=self._last_model_dir)
233
        if not ok:
234
            return
235
        if self._last_model_dir != os.path.dirname(path):
236
            self._last_model_dir = os.path.dirname(path)
237
            self.settings.setValue("last_model_dir", self._last_model_dir)
238
        self._model_mgr.open(path)
239
240
    @trycatchslot
241
    def import_xml(self):
242
        last_import_dir = self.settings.value("last_import_dir", ".")
243
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Import reference OPC UA XML", filter="XML Files (*.xml *.XML)", directory=last_import_dir)
244
        if not ok:
245
            return None
246
        self.settings.setValue("last_import_dir", last_import_dir)
247
        self._model_mgr.import_xml(path)
248
249
    @trycatchslot
250
    def save_as(self):
251
        self._save_as()
252
253
    def _save_as(self):
254
        path, ok = QFileDialog.getSaveFileName(self.modeler, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
255
        if ok:
256
            if self._last_model_dir != os.path.dirname(path):
257
                self._last_model_dir = os.path.dirname(path)
258
                self.settings.setValue("last_model_dir", self._last_model_dir)
259
            self._model_mgr.save_xml(path)
260
            self._model_mgr.save_ua_model(path)
261
262
    @trycatchslot
263
    def save(self):
264
        if not self._model_mgr.current_path:
265
            self.save_as()
266
        else:
267
            self._model_mgr.save_xml()
268
            self._model_mgr.save_ua_model()
269
270
    @trycatchslot
271
    def add_method(self):
272
        args, ok = NewUaMethodDialog.getArgs(self.modeler, "Add Method", self._model_mgr.server_mgr)
273
        if ok:
274
            self._model_mgr.add_method(*args)
275
276
    @trycatchslot
277
    def add_object_type(self):
278
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self._model_mgr.server_mgr)
279
        if ok:
280
            self._model_mgr.add_object_type(*args)
281
282
    @trycatchslot
283
    def add_folder(self):
284
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self._model_mgr.server_mgr)
285
        if ok:
286
            self._model_mgr.add_folder(*args)
287
288
    @trycatchslot
289
    def add_object(self):
290
        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)
291
        if ok:
292
            self._model_mgr.add_object(*args)
293
294
    @trycatchslot
295
    def add_data_type(self):
296
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self._model_mgr.server_mgr)
297
        if ok:
298
            self._model_mgr.add_data_type(*args)
299
    
300
    @trycatchslot
301
    def add_variable(self):
302
        dtype = self.settings.value("last_datatype", None)
303
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
304
        if ok:
305
            self._model_mgr.add_variable(*args)
306
            self.settings.setValue("last_datatype", args[4])
307
308
    @trycatchslot
309
    def add_property(self):
310
        dtype = self.settings.value("last_datatype", None)
311
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
312
        if ok:
313
            self._model_mgr.add_property(*args)
314
315
    @trycatchslot
316
    def add_variable_type(self):
317
        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))
318
        if ok:
319
            self._model_mgr.add_variable_type(*args)
320
321
322
class UaModeler(QMainWindow):
323
    """
324
    Main class of modeler. Should be as simple as possible, try to push things to other classes
325
    or even better python-opcua
326
    """
327
328
    def __init__(self):
329
        QMainWindow.__init__(self)
330
        self.ui = Ui_UaModeler()
331
        self.ui.setupUi(self)
332
        self.setWindowIcon(QIcon(":/network.svg"))
333
334
        # we only show statusbar in case of errors
335
        self.ui.statusBar.hide()
336
337
        # setup QSettings for application and get a settings object
338
        QCoreApplication.setOrganizationName("FreeOpcUa")
339
        QCoreApplication.setApplicationName("OpcUaModeler")
340
        self.settings = QSettings()
341
342
        self._restore_ui_geometri()
343
        
344
        self.tree_ui = TreeWidget(self.ui.treeView)
345
        self.tree_ui.error.connect(self.show_error)
346
347
        self.refs_ui = RefsWidget(self.ui.refView)
348
        self.refs_ui.error.connect(self.show_error)
349
        self.refs_ui.reference_changed.connect(self.tree_ui.reload_current)  # FIXME: shoudl reload a specific node
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.model_mgr.titleChanged.connect(self.update_title)
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 get_current_server(self):
378
        """
379
        Used by tests
380
        """
381
        return self.model_mgr.get_current_server()
382
383
    def clear_all_widgets(self):
384
        self.tree_ui.clear()
385
        self.refs_ui.clear()
386
        self.attrs_ui.clear()
387
        self.idx_ui.clear()
388
        self.nodesets_ui.clear()
389
390
    @trycatchslot
391
    def _update_actions_state(self, current, previous):
392
        node = self.get_current_node(current)
393
        self.actions.update_actions_states(node)
394
395
    def setup_context_menu_tree(self):
396
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
397
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
398
        self._contextMenu = QMenu()
399
400
        # tree view menu
401
        self._contextMenu.addAction(self.ui.actionCopy)
402
        self._contextMenu.addAction(self.ui.actionPaste)
403
        self._contextMenu.addAction(self.ui.actionDelete)
404
        self._contextMenu.addSeparator()
405
        self._contextMenu.addAction(self.tree_ui.actionReload)
406
        self._contextMenu.addSeparator()
407
        self._contextMenu.addAction(self.ui.actionAddFolder)
408
        self._contextMenu.addAction(self.ui.actionAddObject)
409
        self._contextMenu.addAction(self.ui.actionAddVariable)
410
        self._contextMenu.addAction(self.ui.actionAddProperty)
411
        self._contextMenu.addAction(self.ui.actionAddMethod)
412
        self._contextMenu.addAction(self.ui.actionAddObjectType)
413
        self._contextMenu.addAction(self.ui.actionAddVariableType)
414
        self._contextMenu.addAction(self.ui.actionAddDataType)
415
416
    def _show_context_menu_tree(self, position):
417
        print("SHOW REQUEST")
418
        node = self.tree_ui.get_current_node()
419
        if node:
420
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
421
422
    def _restore_ui_geometri(self):
423
        self.resize(int(self.settings.value("main_window_width", 800)),
424
                    int(self.settings.value("main_window_height", 600)))
425
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
426
        self.restoreState(self.settings.value("main_window_state", bytearray()))
427
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
428
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
429
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
430
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
        self.model_mgr.setModified(True)
464
465
    def closeEvent(self, event):
466
        if not self.model_mgr.try_close_model():
467
            event.ignore()
468
            return
469
        self.attrs_ui.save_state()
470
        self.refs_ui.save_state()
471
        self.tree_ui.save_state()
472
        self.settings.setValue("main_window_width", self.size().width())
473
        self.settings.setValue("main_window_height", self.size().height())
474
        self.settings.setValue("main_window_state", self.saveState())
475
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
476
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
477
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
478
        event.accept()
479
480
481
def main():
482
    app = QApplication(sys.argv)
483
    modeler = UaModeler()
484
    handler = QtHandler(modeler.ui.logTextEdit)
485
    logging.getLogger().addHandler(handler)
486
    logging.getLogger("uamodeler").setLevel(logging.INFO)
487
    logging.getLogger("uawidgets").setLevel(logging.INFO)
488
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
489
    modeler.show()
490
    sys.exit(app.exec_())
491
492
493
if __name__ == "__main__":
494
    main()
495