Completed
Pull Request — master (#5)
by Olivier
55s
created

ListData   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 6
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 10
wmc 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 5 1
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, name, value, uatype):
60
        self.name = name
61
        self.value = value
62
        self.uatype = uatype
63
64
65
class ListData(_Data):
66
    def __init__(self, mylist, idx, val, uatype):
67
        self.mylist = mylist
68
        self.idx = idx
69
        self.val = val
70
        self.uatype = uatype
71
72
73
class AttrsWidget(QObject):
74
75
    error = pyqtSignal(Exception)
76
    modified = pyqtSignal()
77
78
    def __init__(self, view, show_timestamps=True):
79
        QObject.__init__(self, view)
80
        self.view = view
81
        self._timestamps = show_timestamps
82
        delegate = MyDelegate(self.view, self)
83
        delegate.error.connect(self.error.emit)
84
        self.settings = QSettings()
85
        self.view.setItemDelegate(delegate)
86
        self.model = QStandardItemModel()
87
        self.model.setHorizontalHeaderLabels(['Attribute', 'Value', 'DataType'])
88
        state = self.settings.value("attrs_widget", None)
89
        if state is not None:
90
            self.view.header().restoreState(state)
91
        self.view.setModel(self.model)
92
        self.current_node = None
93
        self.model.itemChanged.connect(self._item_changed)
94
        self.view.header().setSectionResizeMode(0)
95
        self.view.header().setStretchLastSection(True)
96
        self.view.expanded.connect(self._item_expanded)
97
        self.view.collapsed.connect(self._item_collapsed)
98
99
        # Context menu
100
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
101
        self.view.customContextMenuRequested.connect(self.showContextMenu)
102
        copyaction = QAction("&Copy Value", self.model)
103
        copyaction.triggered.connect(self._copy_value)
104
        self._contextMenu = QMenu()
105
        self._contextMenu.addAction(copyaction)
106
107
    def save_state(self):
108
        self.settings.setValue("attrs_widget", self.view.header().saveState())
109
110
    def _item_expanded(self, idx):
111
        #item = self.model.itemFromIndex(idx)
112
        #print(item.
113
        #if item.data(Qt.UserRole) == "Value Line":
114
        if not idx.parent().isValid():
115
            # only for value attributes which a re childs
116
            # maybe add more tests
117
            return
118
        it = self.model.itemFromIndex(idx.sibling(0, 1))
119
        it.setText("")
120
121
    def _item_collapsed(self, idx):
122
        #item = self.model.itemFromIndex(idx)
123
        #if item.data(Qt.UserRole) == "Value Line":
124
        it = self.model.itemFromIndex(idx.sibling(0, 1))
125
        data = it.data(Qt.UserRole)
126
        it.setText(val_to_string(data.value))
127
128
    def _item_changed(self, item):
129
        print("ITEM CHANGED DISABLED")
130
        return
131
        data = item.data(Qt.UserRole)
132
        if data is None:
133
            print("Error item has no data", item.text())
134
            return
135
        if isinstance(data, AttributeData):
136
            if data.attr == ua.AttributeIds.Value:
137
                print("Should update timestamps!!!")
138
            self._write_attr(data)
139
        elif isinstance(data, MemberData):
140
            #it = self.model.item(item.index().row(), 0)
141
            #self._show_timestamps(it, dv)
142
            print("ITEM", item.text(), data)
143
            print("ITEM PARENT", item.parent().text(), item.data(Qt.UserRole))
144
            print("DATA is ", data)
145
        elif isinstance(data, ListData):
146
            parent = item
147
            while parent.text() != "Value":
148
                parent = item.parent()
149
            it = self.model.itemFromIndex(parent.index().sibling(0, 1))
150
            data = it.data(Qt.UserRole)
151
            self._write_attr(data)
152
        self.modified.emit()
153
154
    def showContextMenu(self, position):
155
        item = self.get_current_item()
156
        if item:
157
            self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
158
159
    def get_current_item(self, col_idx=0):
160
        idx = self.view.currentIndex()
161
        return self.model.item(idx.row(), col_idx)
162
163
    def _copy_value(self, position):
164
        it = self.get_current_item(1)
165
        if it:
166
            QApplication.clipboard().setText(it.text())
167
168
    def clear(self):
169
        # remove all rows but not header!!
170
        self.model.removeRows(0, self.model.rowCount())
171
172
    def reload(self):
173
        self.show_attrs(self.current_node)
174
175
    def show_attrs(self, node):
176
        self.current_node = node
177
        self.clear()
178
        if self.current_node:
179
            self._show_attrs()
180
        self.view.expandToDepth(0)
181
182
    def _show_attrs(self):
183
        attrs = self.get_all_attrs()
184
        for attr, dv in attrs:
185
            try:
186
                # crashing here put down the application, we do not want that
187
                # so we use try/except
188
                if attr == ua.AttributeIds.Value:
189
                    self._show_value_attr(attr, dv)
190
                else:
191
                    self._show_attr(attr, dv)
192
            except Exception as ex:
193
                traceback.print_exc()
194
                self.error.emit(ex)
195
196
    def _show_attr(self, attr, dv):
197
        if attr == ua.AttributeIds.DataType:
198
            string = data_type_to_string(dv.Value.Value)
199
        elif attr in (ua.AttributeIds.AccessLevel,
200
                      ua.AttributeIds.UserAccessLevel,
201
                      ua.AttributeIds.WriteMask,
202
                      ua.AttributeIds.UserWriteMask,
203
                      ua.AttributeIds.EventNotifier):
204
            string = enum_to_string(attr, dv.Value.Value)
205
        else:
206
            string = val_to_string(dv.Value.Value)
207
        print("DISPLAYING", attr.name, string)
208
        name_item = QStandardItem(attr.name)
209
        vitem = QStandardItem(string)
210
        vitem.setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
211
        self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
212
213
    def _show_value_attr(self, attr, dv):
214
        name_item = QStandardItem("Value")
215
        vitem = QStandardItem()
216
        items = self._show_val(name_item, "Value", dv.Value.Value, dv.Value.VariantType)
217
        items[1].setData(AttributeData(attr, dv.Value.Value, dv.Value.VariantType), Qt.UserRole)
218
        row = [name_item, vitem, QStandardItem(dv.Value.VariantType.name)]
219
        self.model.appendRow(row)
220
        self._show_timestamps(name_item, dv)
221
222
    def _show_val(self, parent, name, val, vtype):
223
        name_item = QStandardItem(name)
224
        vitem = QStandardItem()
225
        vitem.setText(val_to_string(val))
226
        vitem.setData(MemberData(name, val, vtype), Qt.UserRole)
227
        row = [name_item, vitem, QStandardItem(vtype.name)]
228
        # if we have a list or extension object we display children
229
        if isinstance(val, list):
230
            row[2].setText("List of " + vtype.name)
231
            self._show_list(name_item, val, vtype)
232
        elif vtype == ua.VariantType.ExtensionObject:
233
            self._show_ext_obj(name_item, val)
234
        parent.appendRow(row)
235
        return row
236
237
    def _show_list(self, parent, mylist, vtype):
238
        for idx, val in enumerate(mylist):
239
            name_item = QStandardItem(str(idx))
240
            vitem = QStandardItem()
241
            vitem.setText(val_to_string(val))
242
            vitem.setData(ListData(mylist, idx, val, vtype), Qt.UserRole)
243
            row = [name_item, vitem, QStandardItem(vtype.name)]
244
            parent.appendRow(row)
245
            if vtype == ua.VariantType.ExtensionObject:
246
                self._show_ext_obj(name_item, val)
247
        return row
248
    
249
    def refresh_list(self, parent, mylist, vtype):
250
        print("REFREHS")
251
        while parent.hasChildren():
252
            self.model.removeRow(0, parent.index())
253
        self._show_list(parent, mylist, vtype)
254
255
    def _show_ext_obj(self, item, val):
256
        item.setText(item.text() + ": " + val.__class__.__name__)
257
        for att_name, att_type in val.ua_types.items():
258
            member_val = getattr(val, att_name)
259
            attr = getattr(ua.VariantType, att_type)
260
            self._show_val(item, att_name, member_val, attr)
261
262
    def _show_timestamps(self, item, dv):
263
        #while item.hasChildren():
264
            #self.model.removeRow(0, item.index())
265
        string = val_to_string(dv.ServerTimestamp)
266
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
267
        string = val_to_string(dv.SourceTimestamp)
268
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
269
270
271
    def get_all_attrs(self):
272
        attrs = [attr for attr in ua.AttributeIds]
273
        try:
274
            dvs = self.current_node.get_attributes(attrs)
275
        except Exception as ex:
276
            self.error.emit(ex)
277
            raise
278
        res = []
279
        for idx, dv in enumerate(dvs):
280
            if dv.StatusCode.is_good():
281
                res.append((attrs[idx], dv))
282
        res.sort(key=lambda x: x[0].name)
283
        return res
284
285
286
class MyDelegate(QStyledItemDelegate):
287
288
    error = pyqtSignal(Exception)
289
290
    def __init__(self, parent, attrs_widget):
291
        QStyledItemDelegate.__init__(self, parent)
292
        self.attrs_widget = attrs_widget
293
294
    def createEditor(self, parent, option, idx):
295
        if idx.column() != 1:
296
            return None
297
        item = self.attrs_widget.model.itemFromIndex(idx)
298
        data = item.data(Qt.UserRole)
299
        if not data.is_editable():
300
            return None
301
        text = item.text()
302
        if isinstance(data, (ListData, MemberData)):
303
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
304
        elif data.attr == ua.AttributeIds.NodeId:
305
            return None
306
        elif data.uatype == ua.VariantType.Boolean:
307
            combo = QComboBox(parent)
308
            combo.addItem("True")
309
            combo.addItem("False")
310
            combo.setCurrentText(text)
311
            return combo
312
        elif data.attr == ua.AttributeIds.NodeClass:
313
            combo = QComboBox(parent)
314
            for nclass in ua.NodeClass:
315
                combo.addItem(nclass.name)
316
            combo.setCurrentText(text)
317
            return combo
318
        elif data.attr == ua.AttributeIds.DataType:
319
            nodeid = getattr(ua.ObjectIds, text)
320
            node = Node(self.attrs_widget.current_node.server, nodeid)
321
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
322
            button = GetNodeButton(parent, node, startnode)
323
            return button
324
        elif data.attr in (ua.AttributeIds.AccessLevel,
325
                           ua.AttributeIds.UserAccessLevel,
326
                           ua.AttributeIds.WriteMask,
327
                           ua.AttributeIds.UserWriteMask,
328
                           ua.AttributeIds.EventNotifier):
329
            return BitEditor(parent, data.attr, data.value)
330
        else:
331
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
332
333
    #def setEditorData(self, editor, index):
334
        #pass
335
336
    def setModelData(self, editor, model, idx):
337
        # if user is setting a value on a null variant, try using the nodes datatype instead
338
        data = model.data(idx, Qt.UserRole)
339
340
        if isinstance(data, AttributeData):
341
            self._set_attribute_data(data, editor, model, idx)
342
        elif isinstance(data, MemberData):
343
            self._set_member_data(data, editor, model, idx)
344
        elif isinstance(data, ListData):
345
            self._set_list_data(data, editor, model, idx)
346
        else:
347
            print("Error while setting model data, data is ", data)
348
349
    def _set_list_data(self, data, editor, model, idx):
350
        text = editor.text()
351
        data.mylist[data.idx] = string_to_val(text, data.uatype)
352
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
353
        attr_data = self._get_attr_data(idx, model)
354
        self._write_attr(attr_data)
355
356
    def _set_member_data(self, data, editor, model, idx):
357
        members = []
358
        parent_idx = idx
359
        while True:
360
            parent_idx, parent_data = self._get_parent_data(parent_idx, model)
361
            if isinstance(parent_data, MemberData):
362
                members.append(parent_data.name)
363
            else:
364
                break
365
        print("MEMBERS", members)
366
        val = parent_data.value
367
        for name in reversed(members):
368
            val = getattr(val, name)
369
        text = editor.text()
370
        setattr(val, data.name, string_to_val(text, data.uatype))
371
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
372
373
    def _get_attr_data(self, idx, model):
374
        while True:
375
            idx = idx.parent()
376
            it = model.itemFromIndex(idx.sibling(0, 1))
377
            data = it.data(Qt.UserRole)
378
            if isinstance(data, AttributeData):
379
                return data
380
381
    def _get_parent_data(self, idx, model):
382
        parent_idx = idx.parent()
383
        it = model.itemFromIndex(parent_idx.sibling(0, 1))
384
        return parent_idx, it.data(Qt.UserRole)
385
386
    def _set_attribute_data(self, data, editor, model, idx):
387
        if data.uatype is ua.VariantType.Null:
388
            try:
389
                data.uatype = self.attrs_widget.current_node.get_data_type_as_variant_type()
390
            except Exception as ex:
391
                self.error.emit(ex)
392
                raise
393
394
        if data.attr == ua.AttributeIds.NodeClass:
395
            data.value = ua.NodeClass[editor.currentText()]
396
            text = editor.currentText()
397
        elif data.attr == ua.AttributeIds.DataType:
398
            data.value = editor.get_node().nodeid
399
            text = data_type_to_string(data.value)
400
        elif data.attr in (ua.AttributeIds.AccessLevel,
401
                           ua.AttributeIds.UserAccessLevel,
402
                           ua.AttributeIds.WriteMask,
403
                           ua.AttributeIds.UserWriteMask,
404
                           ua.AttributeIds.EventNotifier):
405
            data.value = editor.get_byte()
406
            text = enum_to_string(data.attr, data.value)
407
        else:
408
            if isinstance(editor, QComboBox):
409
                text = editor.currentText()
410
            else:
411
                text = editor.text()
412
            data.value = string_to_val(text, data.uatype)
413
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: data})
414
        self._write_attr(data)
415
        print("LIST test", data.value)
416
        if isinstance(data.value, list):
417
            print("LIST", data.value)
418
            # we need to refresh children
419
            item = self.attrs_widget.model.itemFromIndex(idx.sibling(0, 0))
420
            self.attrs_widget.refresh_list(item, data.value, data.uatype)
421
422
    def _write_attr(self, data):
423
        dv = ua.DataValue(ua.Variant(data.value, varianttype=data.uatype))
424
        try:
425
            print("Writing ", dv, " to ", data.attr)
426
            self.attrs_widget.current_node.set_attribute(data.attr, dv)
427
        except Exception as ex:
428
            self.error.emit(ex)
429
            raise
430
431
432
433
def data_type_to_string(dtype):
434
    # a bit too complex, we could just display browse name of node but it requires a query
435
    if isinstance(dtype.Identifier, int) and dtype.Identifier < 63:
436
        string = ua.datatype_to_varianttype(dtype).name
437
    elif dtype.Identifier in ua.ObjectIdNames:
438
        string = ua.ObjectIdNames[dtype.Identifier]
439
    else:
440
        string = dtype.to_string()
441
    return string
442
443
444
def attr_to_enum(attr):
445
    attr_name = attr.name
446
    if attr_name.startswith("User"):
447
        attr_name = attr_name[4:]
448
    return getattr(ua, attr_name)
449
450
451
def enum_to_string(attr, val):
452
    attr_enum = attr_to_enum(attr)
453
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(val)])
454
    return string
455