Completed
Push — master ( 1aba3a...a153c0 )
by Olivier
31s
created

ActionsManager.update_actions_states()   F

Complexity

Conditions 9

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 44
rs 3
cc 9
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
            self._model_mgr.add_method(*args)
281
282
    @trycatchslot
283
    def add_object_type(self):
284
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self._model_mgr.server_mgr)
285
        if ok:
286
            self._model_mgr.add_object_type(*args)
287
288
    @trycatchslot
289
    def add_folder(self):
290
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self._model_mgr.server_mgr)
291
        if ok:
292
            self._model_mgr.add_folder(*args)
293
294
    @trycatchslot
295
    def add_object(self):
296
        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)
297
        if ok:
298
            self._model_mgr.add_object(*args)
299
300
    @trycatchslot
301
    def add_data_type(self):
302
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self._model_mgr.server_mgr)
303
        if ok:
304
            self._model_mgr.add_data_type(*args)
305
    
306
    @trycatchslot
307
    def add_variable(self):
308
        dtype = self.settings.value("last_datatype", None)
309
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
310
        if ok:
311
            self._model_mgr.add_variable(*args)
312
            self.settings.setValue("last_datatype", args[4])
313
314
    @trycatchslot
315
    def add_property(self):
316
        dtype = self.settings.value("last_datatype", None)
317
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self._model_mgr.server_mgr, default_value=9.99, dtype=dtype)
318
        if ok:
319
            self._model_mgr.add_property(*args)
320
321
    @trycatchslot
322
    def add_variable_type(self):
323
        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))
324
        if ok:
325
            self._model_mgr.add_variable_type(*args)
326
327
328
class UaModeler(QMainWindow):
329
    """
330
    Main class of modeler. Should be as simple as possible, try to push things to other classes
331
    or even better python-opcua
332
    """
333
334
    def __init__(self):
335
        QMainWindow.__init__(self)
336
        self.ui = Ui_UaModeler()
337
        self.ui.setupUi(self)
338
        self.setWindowIcon(QIcon(":/network.svg"))
339
340
        # we only show statusbar in case of errors
341
        self.ui.statusBar.hide()
342
343
        # setup QSettings for application and get a settings object
344
        QCoreApplication.setOrganizationName("FreeOpcUa")
345
        QCoreApplication.setApplicationName("OpcUaModeler")
346
        self.settings = QSettings()
347
348
        self._restore_ui_geometri()
349
        
350
        self.tree_ui = TreeWidget(self.ui.treeView)
351
        self.tree_ui.error.connect(self.show_error)
352
353
        self.refs_ui = RefsWidget(self.ui.refView)
354
        self.refs_ui.error.connect(self.show_error)
355
        self.refs_ui.reference_changed.connect(self.tree_ui.reload_current)  # FIXME: shoudl reload a specific node
356
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
357
        self.attrs_ui.error.connect(self.show_error)
358
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
359
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
360
        self.nodesets_ui.error.connect(self.show_error)
361
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
362
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
363
364
        self.ui.treeView.activated.connect(self.show_refs)
365
        self.ui.treeView.clicked.connect(self.show_refs)
366
        self.ui.treeView.activated.connect(self.show_attrs)
367
        self.ui.treeView.clicked.connect(self.show_attrs)
368
369
        self.model_mgr = ModelManagerUI(self)
370
        self.model_mgr.error.connect(self.show_error)
371
        self.model_mgr.titleChanged.connect(self.update_title)
372
        self.actions = ActionsManager(self.ui, self.model_mgr)
373
374
        self.setup_context_menu_tree()
375
376
        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.get_new_nodes())
377
        self.ui.treeView.setItemDelegate(delegate)
378
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
379
380
    def get_current_node(self, idx=None):
381
        return self.tree_ui.get_current_node(idx)
382
383
    def get_current_server(self):
384
        """
385
        Used by tests
386
        """
387
        return self.model_mgr.get_current_server()
388
389
    def clear_all_widgets(self):
390
        self.tree_ui.clear()
391
        self.refs_ui.clear()
392
        self.attrs_ui.clear()
393
        self.idx_ui.clear()
394
        self.nodesets_ui.clear()
395
396
    @trycatchslot
397
    def _update_actions_state(self, current, previous):
398
        node = self.get_current_node(current)
399
        self.actions.update_actions_states(node)
400
401
    def setup_context_menu_tree(self):
402
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
403
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
404
        self._contextMenu = QMenu()
405
406
        # tree view menu
407
        self._contextMenu.addAction(self.ui.actionCopy)
408
        self._contextMenu.addAction(self.ui.actionPaste)
409
        self._contextMenu.addAction(self.ui.actionDelete)
410
        self._contextMenu.addSeparator()
411
        self._contextMenu.addAction(self.tree_ui.actionReload)
412
        self._contextMenu.addSeparator()
413
        self._contextMenu.addAction(self.ui.actionAddFolder)
414
        self._contextMenu.addAction(self.ui.actionAddObject)
415
        self._contextMenu.addAction(self.ui.actionAddVariable)
416
        self._contextMenu.addAction(self.ui.actionAddProperty)
417
        self._contextMenu.addAction(self.ui.actionAddMethod)
418
        self._contextMenu.addAction(self.ui.actionAddObjectType)
419
        self._contextMenu.addAction(self.ui.actionAddVariableType)
420
        self._contextMenu.addAction(self.ui.actionAddDataType)
421
422
    def _show_context_menu_tree(self, position):
423
        print("SHOW REQUEST")
424
        node = self.tree_ui.get_current_node()
425
        if node:
426
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
427
428
    def _restore_ui_geometri(self):
429
        self.resize(int(self.settings.value("main_window_width", 800)),
430
                    int(self.settings.value("main_window_height", 600)))
431
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
432
        self.restoreState(self.settings.value("main_window_state", bytearray()))
433
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
434
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
435
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
436
437
    def update_title(self, path):
438
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
439
440
    def show_error(self, msg):
441
        self.ui.statusBar.show()
442
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
443
        self.ui.statusBar.showMessage(str(msg))
444
        QTimer.singleShot(2500, self.ui.statusBar.hide)
445
446
    def show_msg(self, msg):
447
        self.ui.statusBar.show()
448
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
449
        self.ui.statusBar.showMessage(str(msg))
450
        QTimer.singleShot(1500, self.ui.statusBar.hide)
451
452
    @trycatchslot
453
    def show_refs(self, idx=None):
454
        node = self.get_current_node(idx)
455
        self.refs_ui.show_refs(node)
456
457
    @trycatchslot
458
    def show_attrs(self, idx=None):
459
        if not isinstance(idx, QModelIndex):
460
            idx = None
461
        node = self.get_current_node(idx)
462
        self.attrs_ui.show_attrs(node)
463
464
    def nodesets_change(self, data):
465
        self.idx_ui.reload()
466
        self.tree_ui.reload()
467
        self.refs_ui.clear()
468
        self.attrs_ui.clear()
469
        self.model_mgr.setModified(True)
470
471
    def closeEvent(self, event):
472
        if not self.model_mgr.try_close_model():
473
            event.ignore()
474
            return
475
        self.attrs_ui.save_state()
476
        self.refs_ui.save_state()
477
        self.tree_ui.save_state()
478
        self.settings.setValue("main_window_width", self.size().width())
479
        self.settings.setValue("main_window_height", self.size().height())
480
        self.settings.setValue("main_window_state", self.saveState())
481
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
482
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
483
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
484
        event.accept()
485
486
487
def main():
488
    app = QApplication(sys.argv)
489
    modeler = UaModeler()
490
    handler = QtHandler(modeler.ui.logTextEdit)
491
    logging.getLogger().addHandler(handler)
492
    logging.getLogger("uamodeler").setLevel(logging.INFO)
493
    logging.getLogger("uawidgets").setLevel(logging.INFO)
494
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
495
    modeler.show()
496
    sys.exit(app.exec_())
497
498
499
if __name__ == "__main__":
500
    main()
501