Completed
Push — master ( 587311...83145d )
by Olivier
01:05
created

ActionsManager.update_actions_states()   D

Complexity

Conditions 8

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 38
rs 4
cc 8
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, modeler):
51
        self.modeler = modeler
52
        self.ui = modeler.ui
53
        self.model_slots = modeler.model_slots
54
55
        self._fix_icons()
56
        # actions
57
        self.ui.actionNew.triggered.connect(self.model_slots.new)
58
        self.ui.actionOpen.triggered.connect(self.model_slots.open)
59
        self.ui.actionCopy.triggered.connect(self.model_slots.copy)
60
        self.ui.actionPaste.triggered.connect(self.model_slots.paste)
61
        self.ui.actionDelete.triggered.connect(self.model_slots.delete)
62
        self.ui.actionImport.triggered.connect(self.model_slots.import_xml)
63
        self.ui.actionSave.triggered.connect(self.model_slots.save)
64
        self.ui.actionSaveAs.triggered.connect(self.model_slots.save_as)
65
        self.ui.actionCloseModel.triggered.connect(self.model_slots.close_model)
66
        self.ui.actionAddObjectType.triggered.connect(self.model_slots.add_object_type)
67
        self.ui.actionAddObject.triggered.connect(self.model_slots.add_object)
68
        self.ui.actionAddFolder.triggered.connect(self.model_slots.add_folder)
69
        self.ui.actionAddMethod.triggered.connect(self.model_slots.add_method)
70
        self.ui.actionAddDataType.triggered.connect(self.model_slots.add_data_type)
71
        self.ui.actionAddVariable.triggered.connect(self.model_slots.add_variable)
72
        self.ui.actionAddVariableType.triggered.connect(self.model_slots.add_variable_type)
73
        self.ui.actionAddProperty.triggered.connect(self.model_slots.add_property)
74
75
        self.disable_all_actions()
76
77
    def _fix_icons(self):
78
        # fix icon stuff
79
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
80
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
81
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
82
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
83
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
84
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
85
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
86
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
87
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
88
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
89
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
90
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
91
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
92
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
93
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
94
95
    def update_actions_states(self, node):
96
        self.disable_add_actions()
97
        if not node or node in (self.modeler.get_current_server().nodes.root, 
98
                                self.modeler.get_current_server().nodes.types, 
99
                                self.modeler.get_current_server().nodes.event_types, 
100
                                self.modeler.get_current_server().nodes.object_types, 
101
                                self.modeler.get_current_server().nodes.reference_types, 
102
                                self.modeler.get_current_server().nodes.variable_types, 
103
                                self.modeler.get_current_server().nodes.data_types):
104
            return
105
        path = node.get_path()
106
        nodeclass = node.get_node_class()
107
108
        self.ui.actionCopy.setEnabled(True)
109
        self.ui.actionDelete.setEnabled(True)
110
111
        if nodeclass == ua.NodeClass.Variable:
112
            return
113
114
        self.ui.actionPaste.setEnabled(True)
115
116
        if self.modeler.get_current_server().nodes.base_object_type in path:
117
            self.ui.actionAddObjectType.setEnabled(True)
118
119
        if self.modeler.get_current_server().nodes.base_variable_type in path:
120
            self.ui.actionAddVariableType.setEnabled(True)
121
122
        if self.modeler.get_current_server().nodes.base_data_type in path:
123
            self.ui.actionAddDataType.setEnabled(True)
124
            if self.modeler.get_current_server().nodes.enum_data_type in path:
125
                self.ui.actionAddProperty.setEnabled(True)
126
            return  # not other nodes should be added here
127
128
        self.ui.actionAddFolder.setEnabled(True)
129
        self.ui.actionAddObject.setEnabled(True)
130
        self.ui.actionAddVariable.setEnabled(True)
131
        self.ui.actionAddProperty.setEnabled(True)
132
        self.ui.actionAddMethod.setEnabled(True)
133
134
    def disable_model_actions(self):
135
        self.ui.actionImport.setEnabled(False)
136
        self.ui.actionSave.setEnabled(False)
137
        self.ui.actionSaveAs.setEnabled(False)
138
139
    def disable_all_actions(self):
140
        self.disable_add_actions()
141
        self.disable_model_actions()
142
143
    def disable_add_actions(self):
144
        self.ui.actionPaste.setEnabled(False)
145
        self.ui.actionCopy.setEnabled(False)
146
        self.ui.actionDelete.setEnabled(False)
147
        self.ui.actionAddObject.setEnabled(False)
148
        self.ui.actionAddFolder.setEnabled(False)
149
        self.ui.actionAddVariable.setEnabled(False)
150
        self.ui.actionAddProperty.setEnabled(False)
151
        self.ui.actionAddDataType.setEnabled(False)
152
        self.ui.actionAddVariableType.setEnabled(False)
153
        self.ui.actionAddObjectType.setEnabled(False)
154
        self.ui.actionAddMethod.setEnabled(False)
155
156
    def enable_model_actions(self):
157
        self.ui.actionImport.setEnabled(True)
158
        self.ui.actionSave.setEnabled(True)
159
        self.ui.actionSaveAs.setEnabled(True)
160
161
162
class ModelSlots(QObject):
163
    """
164
    All methods that are called to modify/create/close models.
165
    Logic is inside ModelManager, this class only handle the UI and dialogs
166
    """
167
168
    error = pyqtSignal(Exception)
169
170
    def __init__(self, modeler):
171
        QObject.__init__(self)
172
        self.modeler = modeler
173
        self.model_mgr = modeler.model_mgr
174
        self.settings = QSettings()
175
        self._last_dir = self.settings.value("last_dir", ".")
176
        self._copy_clipboard = None
177
178
    @trycatchslot
179
    def new(self):
180
        if not self.try_close_model():
181
            return
182
        self.model_mgr.new_model()
183
184
    @trycatchslot
185
    def delete(self):
186
        node = self.modeler.get_current_node()
187
        self.model_mgr.delete_node(node)
188
189
    @trycatchslot
190
    def copy(self):
191
        node = self.modeler.get_current_node()
192
        if node:
193
            self._copy_clipboard = node
194
195
    @trycatchslot
196
    def paste(self):
197
        if self._copy_clipboard:
198
            self.model_mgr.paste_node(self._copy_clipboard)
199
200
    @trycatchslot
201
    def close_model(self):
202
        self.try_close_model()
203
204
    def try_close_model(self):
205
        if self.model_mgr.modified:
206
            reply = QMessageBox.question(
207
                self.modeler,
208
                "OPC UA Modeler",
209
                "Model is modified, do you really want to close model?",
210
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
211
            )
212
            if reply != QMessageBox.Yes:
213
                return False
214
        self.model_mgr.close_model(force=True)
215
        return True
216
217
    def _get_xml(self):
218
        path, ok = QFileDialog.getOpenFileName(self.modeler, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
219
        if ok:
220
            self._last_dir = os.path.dirname(path)
221
            self.settings.setValue("last_dir", self._last_dir)
222
        return path, ok
223
224
    @trycatchslot
225
    def open(self):
226
        if not self.try_close_model():
227
            return
228
        path, ok = self._get_xml()
229
        if not ok:
230
            return
231
        self.model_mgr.open_model(path)
232
233
    @trycatchslot
234
    def import_xml(self):
235
        path, ok = self._get_xml()
236
        if not ok:
237
            return None
238
        self.model_mgr.import_xml(path)
239
240
    @trycatchslot
241
    def save_as(self):
242
        self._save_as()
243
244
    def _save_as(self):
245
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
246
        if ok:
247
            if os.path.isfile(path):
248
                reply = QMessageBox.question(
249
                    self.modeler,
250
                    "OPC UA Modeler",
251
                    "File already exit, do you really want to save to this file?",
252
                    QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
253
                )
254
                if reply != QMessageBox.Yes:
255
                    return
256
            self.model_mgr.save_model(path)
257
258
    @trycatchslot
259
    def save(self):
260
        if not self.model_mgr.current_path:
261
            self.save_as()
262
        else:
263
            self.model_mgr.save_model()
264
265
    @trycatchslot
266
    def add_method(self):
267
        args, ok = NewUaMethodDialog.getArgs(self.modeler, "Add Method", self.model_mgr.server_mgr)
268
        if ok:
269
            self.model_mgr.add_method(*args)
270
271
    @trycatchslot
272
    def add_object_type(self):
273
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Object Type", self.model_mgr.server_mgr)
274
        if ok:
275
            self.model_mgr.add_object_type(*args)
276
277
    @trycatchslot
278
    def add_folder(self):
279
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Folder", self.model_mgr.server_mgr)
280
        if ok:
281
            self.model_mgr.add_folder(*args)
282
283
    @trycatchslot
284
    def add_object(self):
285
        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)
286
        if ok:
287
            self.model_mgr.add_object(*args)
288
289
    @trycatchslot
290
    def add_data_type(self):
291
        args, ok = NewNodeBaseDialog.getArgs(self.modeler, "Add Data Type", self.model_mgr.server_mgr)
292
        if ok:
293
            self.model_mgr.add_data_type(*args)
294
    
295
    @trycatchslot
296
    def add_variable(self):
297
        dtype = self.settings.value("last_datatype", None)
298
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Variable", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
299
        if ok:
300
            self.model_mgr.add_variable(*args)
301
            self.settings.setValue("last_datatype", args[4])
302
303
    @trycatchslot
304
    def add_property(self):
305
        dtype = self.settings.value("last_datatype", None)
306
        args, ok = NewUaVariableDialog.getArgs(self.modeler, "Add Property", self.model_mgr.server_mgr, default_value=9.99, dtype=dtype)
307
        if ok:
308
            self.model_mgr.add_property(*args)
309
310
    @trycatchslot
311
    def add_variable_type(self):
312
        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))
313
        if ok:
314
            self.model_mgr.add_variable_type(*args)
315
316
317
class UaModeler(QMainWindow):
318
    """
319
    Main class of modeler. Should be as simple as possible, try to push things to other classes
320
    or even better python-opcua
321
    """
322
323
    def __init__(self):
324
        QMainWindow.__init__(self)
325
        self.ui = Ui_UaModeler()
326
        self.ui.setupUi(self)
327
        self.setWindowIcon(QIcon(":/network.svg"))
328
329
        # we only show statusbar in case of errors
330
        self.ui.statusBar.hide()
331
332
        # setup QSettings for application and get a settings object
333
        QCoreApplication.setOrganizationName("FreeOpcUa")
334
        QCoreApplication.setApplicationName("OpcUaModeler")
335
        self.settings = QSettings()
336
337
        self._restore_state()
338
        
339
        self.tree_ui = TreeWidget(self.ui.treeView)
340
        self.tree_ui.error.connect(self.show_error)
341
342
        self.refs_ui = RefsWidget(self.ui.refView)
343
        self.refs_ui.error.connect(self.show_error)
344
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
345
        self.attrs_ui.error.connect(self.show_error)
346
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
347
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
348
        self.nodesets_ui.error.connect(self.show_error)
349
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
350
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
351
352
        self.ui.treeView.activated.connect(self.show_refs)
353
        self.ui.treeView.clicked.connect(self.show_refs)
354
        self.ui.treeView.activated.connect(self.show_attrs)
355
        self.ui.treeView.clicked.connect(self.show_attrs)
356
357
        self.model_mgr = ModelManager(self)
358
        self.model_slots = ModelSlots(self)
359
        self.actions = ActionsManager(self)
360
        self.model_slots.error.connect(self.show_error)
361
        self.setup_context_menu_tree()
362
363
        self.setup_context_menu_tree()
364
365
        delegate = BoldDelegate(self, self.tree_ui.model, self.model_mgr.new_nodes)
366
        self.ui.treeView.setItemDelegate(delegate)
367
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
368
369
    def get_current_server(self):
370
        return self.model_mgr.server_mgr
371
372
    def get_current_node(self, idx=None):
373
        return self.tree_ui.get_current_node(idx)
374
375
    def clear_all_widgets(self):
376
        self.tree_ui.clear()
377
        self.refs_ui.clear()
378
        self.attrs_ui.clear()
379
        self.idx_ui.clear()
380
        self.nodesets_ui.clear()
381
382
    def _update_actions_state(self, current, previous):
383
        node = self.get_current_node(current)
384
        self.actions.update_actions_states(node)
385
386
    def setup_context_menu_tree(self):
387
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
388
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
389
        self._contextMenu = QMenu()
390
391
        # tree view menu
392
        self._contextMenu.addAction(self.ui.actionCopy)
393
        self._contextMenu.addAction(self.ui.actionPaste)
394
        self._contextMenu.addAction(self.ui.actionDelete)
395
        self._contextMenu.addSeparator()
396
        self._contextMenu.addAction(self.ui.actionAddFolder)
397
        self._contextMenu.addAction(self.ui.actionAddObject)
398
        self._contextMenu.addAction(self.ui.actionAddVariable)
399
        self._contextMenu.addAction(self.ui.actionAddProperty)
400
        self._contextMenu.addAction(self.ui.actionAddMethod)
401
        self._contextMenu.addAction(self.ui.actionAddObjectType)
402
        self._contextMenu.addAction(self.ui.actionAddVariableType)
403
        self._contextMenu.addAction(self.ui.actionAddDataType)
404
405
    def _show_context_menu_tree(self, position):
406
        node = self.tree_ui.get_current_node()
407
        if node:
408
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
409
410
    def _restore_state(self):
411
        self.resize(int(self.settings.value("main_window_width", 800)),
412
                    int(self.settings.value("main_window_height", 600)))
413
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
414
        self.restoreState(self.settings.value("main_window_state", bytearray()))
415
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
416
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
417
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
418
    def update_title(self, path):
419
        self.setWindowTitle("FreeOpcUa Modeler " + str(path))
420
421
    def show_error(self, msg):
422
        self.ui.statusBar.show()
423
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
424
        self.ui.statusBar.showMessage(str(msg))
425
        QTimer.singleShot(2500, self.ui.statusBar.hide)
426
427
    def show_msg(self, msg):
428
        self.ui.statusBar.show()
429
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
430
        self.ui.statusBar.showMessage(str(msg))
431
        QTimer.singleShot(1500, self.ui.statusBar.hide)
432
433
    @trycatchslot
434
    def show_refs(self, idx=None):
435
        node = self.get_current_node(idx)
436
        self.refs_ui.show_refs(node)
437
438
    @trycatchslot
439
    def show_attrs(self, idx=None):
440
        if not isinstance(idx, QModelIndex):
441
            idx = None
442
        node = self.get_current_node(idx)
443
        self.attrs_ui.show_attrs(node)
444
445
    def nodesets_change(self, data):
446
        self.idx_ui.reload()
447
        self.tree_ui.reload()
448
        self.refs_ui.clear()
449
        self.attrs_ui.clear()
450
451
    def closeEvent(self, event):
452
        if not self.model_slots.try_close_model():
453
            event.ignore()
454
            return
455
        self.attrs_ui.save_state()
456
        self.refs_ui.save_state()
457
        self.settings.setValue("main_window_width", self.size().width())
458
        self.settings.setValue("main_window_height", self.size().height())
459
        self.settings.setValue("main_window_state", self.saveState())
460
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
461
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
462
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
463
        self.model_mgr.server_mgr.close()
464
        event.accept()
465
466
467
def main():
468
    app = QApplication(sys.argv)
469
    modeler = UaModeler()
470
    handler = QtHandler(modeler.ui.logTextEdit)
471
    logging.getLogger().addHandler(handler)
472
    logging.getLogger("uamodeler").setLevel(logging.INFO)
473
    logging.getLogger("uawidgets").setLevel(logging.INFO)
474
    #logging.getLogger("opcua").setLevel(logging.INFO)  # to enable logging of ua server
475
    modeler.show()
476
    sys.exit(app.exec_())
477
478
479
if __name__ == "__main__":
480
    main()
481