Completed
Push — master ( 032a8d...911c72 )
by Olivier
01:04
created

AttrsWidget.showContextMenu()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
c 3
b 0
f 0
dl 0
loc 4
rs 10
1
import traceback
2
3
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QSettings
4
from PyQt5.QtGui import QStandardItemModel, QStandardItem
5
from PyQt5.QtWidgets import QApplication, QMenu, QAction, QStyledItemDelegate, QComboBox, QVBoxLayout, QCheckBox, QDialog
6
7
from opcua import ua
8
from opcua import Node
9
from opcua.common.ua_utils import string_to_val, val_to_string
10
11
from uawidgets.get_node_dialog import GetNodeButton
12
13
14
class BitEditor(QDialog):
15
    """
16
    Edit bits in data
17
    FIXME: this should be a dialog but a Widget appearing directly in treewidget
18
    Patch welcome
19
    """
20
21
    def __init__(self, parent, attr, val):
22
        QDialog.__init__(self, parent)
23
        layout = QVBoxLayout(self)
24
        self.setLayout(layout)
25
        self.boxes = []
26
        self.enum = attr_to_enum(attr)
27
        for el in self.enum:
28
            box = QCheckBox(el.name, parent)
29
            layout.addWidget(box)
30
            self.boxes.append(box)
31
            if ua.ua_binary.test_bit(val, el.value):
32
                box.setChecked(True)
33
            else:
34
                box.setChecked(False)
35
36
    def get_byte(self):
37
        data = 0
38
        for box in self.boxes:
39
            if box.isChecked():
40
                data = ua.ua_binary.set_bit(data, self.enum[box.text()].value)
41
        return data
42
43
44
class _Data(object):
45
    def is_editable(self):
46
        if self.uatype != ua.VariantType.ExtensionObject:
47
            return True
48
        return False
49
50
51
class AttributeData(_Data):
52
    def __init__(self, attr, value, uatype):
53
        self.attr = attr
54
        self.value = value
55
        self.uatype = uatype
56
57
58
class MemberData(_Data):
59
    def __init__(self, obj, name, value, uatype):
60
        self.obj = obj
61
        self.name = name
62
        self.value = value
63
        self.uatype = uatype
64
65
66
class ListData(_Data):
67
    def __init__(self, mylist, idx, val, uatype):
68
        self.mylist = mylist
69
        self.idx = idx
70
        self.value = val
71
        self.uatype = uatype
72
73
74
class AttrsWidget(QObject):
75
76
    error = pyqtSignal(Exception)
77
    attr_written = pyqtSignal(ua.AttributeIds, ua.DataValue)
78
79
    def __init__(self, view, show_timestamps=True):
80
        QObject.__init__(self, view)
81
        self.view = view
82
        self._timestamps = show_timestamps
83
        delegate = MyDelegate(self.view, self)
84
        delegate.error.connect(self.error.emit)
85
        delegate.attr_written.connect(self.attr_written.emit)
86
        self.settings = QSettings()
87
        self.view.setItemDelegate(delegate)
88
        self.model = QStandardItemModel()
89
        self.model.setHorizontalHeaderLabels(['Attribute', 'Value', 'DataType'])
90
        state = self.settings.value("attrs_widget", None)
91
        if state is not None:
92
            self.view.header().restoreState(state)
93
        self.view.setModel(self.model)
94
        self.current_node = None
95
        self.view.header().setSectionResizeMode(0)
96
        self.view.header().setStretchLastSection(True)
97
        self.view.expanded.connect(self._item_expanded)
98
        self.view.collapsed.connect(self._item_collapsed)
99
100
        # Context menu
101
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
102
        self.view.customContextMenuRequested.connect(self.showContextMenu)
103
        copyaction = QAction("&Copy Value", self.model)
104
        copyaction.triggered.connect(self._copy_value)
105
        self._contextMenu = QMenu()
106
        self._contextMenu.addAction(copyaction)
107
108
    def save_state(self):
109
        self.settings.setValue("attrs_widget", self.view.header().saveState())
110
111
    def _item_expanded(self, idx):
112
        if not idx.parent().isValid():
113
            # only for value attributes which a re childs
114
            # maybe add more tests
115
            return
116
        it = self.model.itemFromIndex(idx.sibling(0, 1))
117
        it.setText("")
118
119
    def _item_collapsed(self, idx):
120
        it = self.model.itemFromIndex(idx.sibling(0, 1))
121
        data = it.data(Qt.UserRole)
122
        it.setText(val_to_string(data.value))
123
124
    def showContextMenu(self, position):
125
        item = self.get_current_item()
126
        if item:
127
            self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
128
129
    def get_current_item(self, col_idx=0):
130
        idx = self.view.currentIndex()
131
        return self.model.item(idx.row(), col_idx)
132
133
    def _copy_value(self, position):
134
        it = self.get_current_item(1)
135
        if it:
136
            QApplication.clipboard().setText(it.text())
137
138
    def clear(self):
139
        # remove all rows but not header!!
140
        self.model.removeRows(0, self.model.rowCount())
141
142
    def reload(self):
143
        self.show_attrs(self.current_node)
144
145
    def show_attrs(self, node):
146
        self.current_node = node
147
        self.clear()
148
        if self.current_node:
149
            self._show_attrs()
150
        self.view.expandToDepth(0)
151
152
    def _show_attrs(self):
153
        attrs = self.get_all_attrs()
154
        for attr, dv in attrs:
155
            try:
156
                # crashing here put down the application, we do not want that
157
                # so we use try/except
158
                if attr == ua.AttributeIds.Value:
159
                    self._show_value_attr(attr, dv)
160
                else:
161
                    self._show_attr(attr, dv)
162
            except Exception as ex:
163
                traceback.print_exc()
164
                self.error.emit(ex)
165
166
    def _show_attr(self, attr, dv):
167
        if attr == ua.AttributeIds.DataType:
168
            string = data_type_to_string(dv.Value.Value)
169
        elif attr in (ua.AttributeIds.AccessLevel,
170
                      ua.AttributeIds.UserAccessLevel,
171
                      ua.AttributeIds.WriteMask,
172
                      ua.AttributeIds.UserWriteMask,
173
                      ua.AttributeIds.EventNotifier):
174
            string = enum_to_string(attr, dv.Value.Value)
175
        else:
176
            string = val_to_string(dv.Value.Value)
177
        name_item = QStandardItem(attr.name)
178
        vitem = QStandardItem(string)
179
        vitem.setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
180
        self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
181
182
    def _show_value_attr(self, attr, dv):
183
        name_item = QStandardItem("Value")
184
        vitem = QStandardItem()
185
        items = self._show_val(name_item, None, "Value", dv.Value.Value, dv.Value.VariantType)
186
        items[1].setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
187
        row = [name_item, vitem, QStandardItem(dv.Value.VariantType.name)]
188
        self.model.appendRow(row)
189
        self._show_timestamps(name_item, dv)
190
191
    def _show_val(self, parent, obj, name, val, vtype):
192
        name_item = QStandardItem(name)
193
        vitem = QStandardItem()
194
        vitem.setText(val_to_string(val))
195
        vitem.setData(MemberData(obj, name, val, vtype), Qt.UserRole)
196
        row = [name_item, vitem, QStandardItem(vtype.name)]
197
        # if we have a list or extension object we display children
198
        if isinstance(val, list):
199
            row[2].setText("List of " + vtype.name)
200
            self._show_list(name_item, val, vtype)
201
        elif vtype == ua.VariantType.ExtensionObject:
202
            self._show_ext_obj(name_item, val)
203
        parent.appendRow(row)
204
        return row
205
206
    def _show_list(self, parent, mylist, vtype):
207
        for idx, val in enumerate(mylist):
208
            name_item = QStandardItem(str(idx))
209
            vitem = QStandardItem()
210
            vitem.setText(val_to_string(val))
211
            vitem.setData(ListData(mylist, idx, val, vtype), Qt.UserRole)
212
            row = [name_item, vitem, QStandardItem(vtype.name)]
213
            parent.appendRow(row)
214
            if vtype == ua.VariantType.ExtensionObject:
215
                self._show_ext_obj(name_item, val)
216
        return row
217
    
218
    def refresh_list(self, parent, mylist, vtype):
219
        while parent.hasChildren():
220
            self.model.removeRow(0, parent.index())
221
        self._show_list(parent, mylist, vtype)
222
223
    def _show_ext_obj(self, item, val):
224
        item.setText(item.text() + ": " + val.__class__.__name__)
225
        for att_name, att_type in val.ua_types.items():
226
            member_val = getattr(val, att_name)
227
            attr = getattr(ua.VariantType, att_type)
228
            self._show_val(item, val, att_name, member_val, attr)
229
230
    def _show_timestamps(self, item, dv):
231
        #while item.hasChildren():
232
            #self.model.removeRow(0, item.index())
233
        string = val_to_string(dv.ServerTimestamp)
234
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
235
        string = val_to_string(dv.SourceTimestamp)
236
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
237
238
239
    def get_all_attrs(self):
240
        attrs = [attr for attr in ua.AttributeIds]
241
        try:
242
            dvs = self.current_node.get_attributes(attrs)
243
        except Exception as ex:
244
            self.error.emit(ex)
245
            raise
246
        res = []
247
        for idx, dv in enumerate(dvs):
248
            if dv.StatusCode.is_good():
249
                res.append((attrs[idx], dv))
250
        res.sort(key=lambda x: x[0].name)
251
        return res
252
253
254
class MyDelegate(QStyledItemDelegate):
255
256
    error = pyqtSignal(Exception)
257
    attr_written = pyqtSignal(ua.AttributeIds, ua.DataValue)
258
259
    def __init__(self, parent, attrs_widget):
260
        QStyledItemDelegate.__init__(self, parent)
261
        self.attrs_widget = attrs_widget
262
263
    def createEditor(self, parent, option, idx):
264
        if idx.column() != 1:
265
            return None
266
        item = self.attrs_widget.model.itemFromIndex(idx)
267
        data = item.data(Qt.UserRole)
268
        if not data.is_editable():
269
            return None
270
        text = item.text()
271
        if isinstance(data, (ListData, MemberData)):
272
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
273
        elif data.attr == ua.AttributeIds.NodeId:
274
            return None
275
        elif data.uatype == ua.VariantType.Boolean:
276
            combo = QComboBox(parent)
277
            combo.addItem("True")
278
            combo.addItem("False")
279
            combo.setCurrentText(text)
280
            return combo
281
        elif data.attr == ua.AttributeIds.NodeClass:
282
            combo = QComboBox(parent)
283
            for nclass in ua.NodeClass:
284
                combo.addItem(nclass.name)
285
            combo.setCurrentText(text)
286
            return combo
287
        elif data.attr == ua.AttributeIds.DataType:
288
            nodeid = getattr(ua.ObjectIds, text)
289
            node = Node(self.attrs_widget.current_node.server, nodeid)
290
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
291
            button = GetNodeButton(parent, node, startnode)
292
            return button
293
        elif data.attr in (ua.AttributeIds.AccessLevel,
294
                           ua.AttributeIds.UserAccessLevel,
295
                           ua.AttributeIds.WriteMask,
296
                           ua.AttributeIds.UserWriteMask,
297
                           ua.AttributeIds.EventNotifier):
298
            return BitEditor(parent, data.attr, data.value)
299
        else:
300
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
301
302
    #def setEditorData(self, editor, index):
303
        #pass
304
305
    def setModelData(self, editor, model, idx):
306
        # if user is setting a value on a null variant, try using the nodes datatype instead
307
        data = model.data(idx, Qt.UserRole)
308
309
        if isinstance(data, AttributeData):
310
            self._set_attribute_data(data, editor, model, idx)
311
        elif isinstance(data, MemberData):
312
            self._set_member_data(data, editor, model, idx)
313
        elif isinstance(data, ListData):
314
            self._set_list_data(data, editor, model, idx)
315
        else:
316
            print("Error while setting model data, data is ", data)
317
318
    def _set_list_data(self, data, editor, model, idx):
319
        text = editor.text()
320
        data.mylist[data.idx] = string_to_val(text, data.uatype)
321
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
322
        attr_data = self._get_attr_data(idx, model)
323
        self._write_attr(attr_data)
324
325
    def _set_member_data(self, data, editor, model, idx):
326
        val = string_to_val(editor.text(), data.uatype)
327
        data.value = val
328
        model.setItemData(idx, {Qt.DisplayRole: editor.text(), Qt.UserRole: data})
329
        setattr(data.obj, data.name, val)
330
        attr_data = self._get_attr_data(idx, model)
331
        self._write_attr(attr_data)
332
333
    def _get_attr_data(self, idx, model):
334
        while True:
335
            idx = idx.parent()
336
            it = model.itemFromIndex(idx.sibling(0, 1))
337
            data = it.data(Qt.UserRole)
338
            if isinstance(data, AttributeData):
339
                return data
340
341
    def _get_parent_data(self, idx, model):
342
        parent_idx = idx.parent()
343
        it = model.itemFromIndex(parent_idx.sibling(0, 1))
344
        return parent_idx, it.data(Qt.UserRole)
345
346
    def _set_attribute_data(self, data, editor, model, idx):
347
        if data.uatype is ua.VariantType.Null:
348
            try:
349
                data.uatype = self.attrs_widget.current_node.get_data_type_as_variant_type()
350
            except Exception as ex:
351
                self.error.emit(ex)
352
                raise
353
354
        if data.attr == ua.AttributeIds.NodeClass:
355
            data.value = ua.NodeClass[editor.currentText()]
356
            text = editor.currentText()
357
        elif data.attr == ua.AttributeIds.DataType:
358
            data.value = editor.get_node().nodeid
359
            text = data_type_to_string(data.value)
360
        elif data.attr in (ua.AttributeIds.AccessLevel,
361
                           ua.AttributeIds.UserAccessLevel,
362
                           ua.AttributeIds.WriteMask,
363
                           ua.AttributeIds.UserWriteMask,
364
                           ua.AttributeIds.EventNotifier):
365
            data.value = editor.get_byte()
366
            text = enum_to_string(data.attr, data.value)
367
        else:
368
            if isinstance(editor, QComboBox):
369
                text = editor.currentText()
370
            else:
371
                text = editor.text()
372
            data.value = string_to_val(text, data.uatype)
373
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
374
        self._write_attr(data)
375
        if isinstance(data.value, list):
376
            # we need to refresh children
377
            item = self.attrs_widget.model.itemFromIndex(idx.sibling(0, 0))
378
            self.attrs_widget.refresh_list(item, data.value, data.uatype)
379
380
    def _write_attr(self, data):
381
        dv = ua.DataValue(ua.Variant(data.value, varianttype=data.uatype))
382
        try:
383
            print("Writing ", dv, " to ", data.attr)
384
            self.attrs_widget.current_node.set_attribute(data.attr, dv)
385
        except Exception as ex:
386
            self.error.emit(ex)
387
            raise
388
        else:
389
            self.attr_written.emit(data.attr, dv)
390
391
392
def data_type_to_string(dtype):
393
    # a bit too complex, we could just display browse name of node but it requires a query
394
    if isinstance(dtype.Identifier, int) and dtype.Identifier < 63:
395
        string = ua.datatype_to_varianttype(dtype).name
396
    elif dtype.Identifier in ua.ObjectIdNames:
397
        string = ua.ObjectIdNames[dtype.Identifier]
398
    else:
399
        string = dtype.to_string()
400
    return string
401
402
403
def attr_to_enum(attr):
404
    attr_name = attr.name
405
    if attr_name.startswith("User"):
406
        attr_name = attr_name[4:]
407
    return getattr(ua, attr_name)
408
409
410
def enum_to_string(attr, val):
411
    attr_enum = attr_to_enum(attr)
412
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(val)])
413
    return string
414