Completed
Pull Request — master (#51)
by Olivier
30s
created

ModelManagerUI._save_as()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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