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

AttrsWidget   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 164
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 1
Metric Value
c 10
b 0
f 1
dl 0
loc 164
rs 8.6
wmc 37

16 Methods

Rating   Name   Duplication   Size   Complexity  
B __init__() 0 26 2
A save_state() 0 2 1
A _copy_value() 0 4 2
A _show_attrs() 0 7 3
A showContextMenu() 0 4 2
A _show_attr() 0 15 3
A reload() 0 2 1
B _item_changed() 0 19 5
A get_current_item() 0 3 1
A show_attrs() 0 6 2
A clear() 0 3 1
A _show_timestamps() 0 7 1
A _show_value_attr() 0 9 1
A _show_val() 0 14 4
B get_all_attrs() 0 13 6
A _show_ext_obj() 0 9 2
1
from datetime import datetime
2
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QSettings
3
from PyQt5.QtGui import QStandardItemModel, QStandardItem
4
from PyQt5.QtWidgets import QApplication, QMenu, QAction, QStyledItemDelegate, QComboBox, QWidget, QVBoxLayout, QCheckBox, QDialog
5
6
from opcua import ua
7
from opcua import Node
8
from opcua.common.ua_utils import string_to_variant, variant_to_string, val_to_string
9
10
from uawidgets.get_node_dialog import GetNodeButton
11
12
13
class BitEditor(QDialog):
14
    """
15
    Edit bits in data
16
    FIXME: this should be a dialog but a Widget appearing directly in treewidget
17
    Patch welcome
18
    """
19
20
    def __init__(self, parent, attr, val):
21
        QDialog.__init__(self, parent)
22
        layout = QVBoxLayout(self)
23
        self.setLayout(layout)
24
        self.boxes = []
25
        self.enum = attr_to_enum(attr)
26
        for el in self.enum:
27
            box = QCheckBox(el.name, parent)
28
            layout.addWidget(box)
29
            self.boxes.append(box)
30
            if ua.ua_binary.test_bit(val, el.value):
31
                box.setChecked(True)
32
            else:
33
                box.setChecked(False)
34
35
    def get_byte(self):
36
        data = 0
37
        for box in self.boxes:
38
            if box.isChecked():
39
                data = ua.ua_binary.set_bit(data, self.enum[box.text()].value)
40
        return data
41
42
43
class AttrsWidget(QObject):
44
45
    error = pyqtSignal(Exception)
46
    modified = pyqtSignal()
47
48
    def __init__(self, view, show_timestamps=True):
49
        QObject.__init__(self, view)
50
        self.view = view
51
        self._timestamps = show_timestamps
52
        delegate = MyDelegate(self.view, self)
53
        delegate.error.connect(self.error.emit)
54
        self.settings = QSettings()
55
        self.view.setItemDelegate(delegate)
56
        self.model = QStandardItemModel()
57
        self.model.setHorizontalHeaderLabels(['Attribute', 'Value', 'DataType'])
58
        state = self.settings.value("attrs_widget", None)
59
        if state is not None:
60
            self.view.header().restoreState(state)
61
        self.view.setModel(self.model)
62
        self.current_node = None
63
        self.model.itemChanged.connect(self._item_changed)
64
        self.view.header().setSectionResizeMode(0)
65
        self.view.header().setStretchLastSection(True)
66
67
        # Context menu
68
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
69
        self.view.customContextMenuRequested.connect(self.showContextMenu)
70
        copyaction = QAction("&Copy Value", self.model)
71
        copyaction.triggered.connect(self._copy_value)
72
        self._contextMenu = QMenu()
73
        self._contextMenu.addAction(copyaction)
74
75
    def save_state(self):
76
        self.settings.setValue("attrs_widget", self.view.header().saveState())
77
78
    def _item_changed(self, item):
79
        print("DATACHANGED DISABLED")
80
        return
81
        data = item.data(Qt.UserRole)
82
        if data is None:
83
            print("Error item has no data", item.text())
84
            return
85
        attr, dv = item.data(Qt.UserRole)
86
        if attr == ua.AttributeIds.Value:
87
            dv.SourceTimestamp = datetime.now()
88
        try:
89
            self.current_node.set_attribute(attr, dv)
90
        except Exception as ex:
91
            self.error.emit(ex)
92
            raise
93
        if attr == ua.AttributeIds.Value:
94
            it = self.model.item(item.index().row(), 0)
95
            self._show_timestamps(it, dv)
96
        self.modified.emit()
97
98
    def showContextMenu(self, position):
99
        item = self.get_current_item()
100
        if item:
101
            self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
102
103
    def get_current_item(self, col_idx=0):
104
        idx = self.view.currentIndex()
105
        return self.model.item(idx.row(), col_idx)
106
107
    def _copy_value(self, position):
108
        it = self.get_current_item(1)
109
        if it:
110
            QApplication.clipboard().setText(it.text())
111
112
    def clear(self):
113
        # remove all rows but not header!!
114
        self.model.removeRows(0, self.model.rowCount())
115
116
    def reload(self):
117
        self.show_attrs(self.current_node)
118
119
    def show_attrs(self, node):
120
        self.current_node = node
121
        self.clear()
122
        if self.current_node:
123
            self._show_attrs()
124
        self.view.expandAll()
125
126
    def _show_attrs(self):
127
        attrs = self.get_all_attrs()
128
        for attr, dv in attrs:
129
            if attr == ua.AttributeIds.Value:
130
                self._show_value_attr(attr, dv)
131
            else:
132
                self._show_attr(attr, dv)
133
134
    def _show_attr(self, attr, dv):
135
        if attr == ua.AttributeIds.DataType:
136
            string = data_type_to_string(dv)
137
        elif attr in (ua.AttributeIds.AccessLevel,
138
                      ua.AttributeIds.UserAccessLevel,
139
                      ua.AttributeIds.WriteMask,
140
                      ua.AttributeIds.UserWriteMask,
141
                      ua.AttributeIds.EventNotifier):
142
            string = enum_to_string(attr, dv)
143
        else:
144
            string = val_to_string(dv.Value)
145
        name_item = QStandardItem(attr.name)
146
        vitem = QStandardItem(string)
147
        vitem.setData((attr, dv), Qt.UserRole)
148
        self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
149
150
    def _show_value_attr(self, attr, dv):
151
        name_item = QStandardItem("Value")
152
        vitem = QStandardItem()
153
        row = [name_item, vitem, QStandardItem(dv.Value.VariantType.name)]
154
        items = self._show_val(name_item, "Value", dv.Value.Value, dv.Value.VariantType)
155
        items[1].setData((attr, dv), Qt.UserRole)
156
        #if self._timestamps:
157
        self.model.appendRow(row)
158
        self._show_timestamps(name_item, dv)
159
160
    def _show_val(self, parent, name, val, vtype):
161
        name_item = QStandardItem(name)
162
        vitem = QStandardItem()
163
        row = [name_item, vitem, QStandardItem(vtype.name)]
164
        if isinstance(val, (list, tuple)):
165
            row[2].setText("List of " + vtype.name)
166
            for idx, element in enumerate(val):
167
                self._show_val(name_item, str(idx), element, vtype)
168
        elif vtype == ua.VariantType.ExtensionObject:
169
            self._show_ext_obj(name_item, val)
170
        else:
171
            vitem.setText(val_to_string(val))
172
        parent.appendRow(row)
173
        return row
174
175
    def _show_ext_obj(self, item, val):
176
        item.setText(item.text() + ": " + val.__class__.__name__)
177
        #ext_it = QStandardItem("ExtensionObject: " + val.__class__.__name__)
178
        #item.appendRow([ext_it, QStandardItem(), QStandardItem()])
179
        for att_name, att_type in val.ua_types.items():
180
            member_val = getattr(val, att_name)
181
            attr = getattr(ua.VariantType, att_type)
182
            #self._show_val(ext_it, att_name, member_val, attr)
183
            self._show_val(item, att_name, member_val, attr)
184
185
    def _show_timestamps(self, item, dv):
186
        #while item.hasChildren():
187
            #self.model.removeRow(0, item.index())
188
        string = val_to_string(dv.ServerTimestamp)
189
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
190
        string = val_to_string(dv.SourceTimestamp)
191
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
192
193
194
    def get_all_attrs(self):
195
        attrs = [attr for attr in ua.AttributeIds]
196
        try:
197
            dvs = self.current_node.get_attributes(attrs)
198
        except Exception as ex:
199
            self.error.emit(ex)
200
            raise
201
        res = []
202
        for idx, dv in enumerate(dvs):
203
            if dv.StatusCode.is_good():
204
                res.append((attrs[idx], dv))
205
        res.sort(key=lambda x: x[0].name)
206
        return res
207
208
209
class MyDelegate(QStyledItemDelegate):
210
211
    error = pyqtSignal(Exception)
212
213
    def __init__(self, parent, attrs_widget):
214
        QStyledItemDelegate.__init__(self, parent)
215
        self.attrs_widget = attrs_widget
216
217
    def createEditor(self, parent, option, idx):
218
        if idx.column() != 1:
219
            return None
220
        item = self.attrs_widget.model.itemFromIndex(idx)
221
        attr, dv = item.data(Qt.UserRole)
222
        text = item.text()
223
        if attr == ua.AttributeIds.NodeId:
224
            return None
225
        if dv.Value.VariantType == ua.VariantType.Boolean:
226
            combo = QComboBox(parent)
227
            combo.addItem("True")
228
            combo.addItem("False")
229
            combo.setCurrentText(text)
230
            return combo
231
        elif attr == ua.AttributeIds.NodeClass:
232
            combo = QComboBox(parent)
233
            for nclass in ua.NodeClass:
234
                combo.addItem(nclass.name)
235
            combo.setCurrentText(text)
236
            return combo
237
        elif attr == ua.AttributeIds.DataType:
238
            nodeid = getattr(ua.ObjectIds, text)
239
            node = Node(self.attrs_widget.current_node.server, nodeid)
240
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
241
            button = GetNodeButton(parent, node, startnode)
242
            return button
243
        elif attr in (ua.AttributeIds.AccessLevel,
244
                      ua.AttributeIds.UserAccessLevel,
245
                      ua.AttributeIds.WriteMask,
246
                      ua.AttributeIds.UserWriteMask,
247
                      ua.AttributeIds.EventNotifier):
248
            return BitEditor(parent, attr, dv.Value.Value)
249
        else:
250
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
251
252
    #def setEditorData(self, editor, index):
253
        #pass
254
255
    def setModelData(self, editor, model, idx):
256
        #item = self.attrs_widget.model.itemFromIndex(idx)
257
        attr, dv = model.data(idx, Qt.UserRole)
258
        if attr == ua.AttributeIds.NodeClass:
259
            dv.Value.Value = ua.NodeClass[editor.currentText()]
260
            text = editor.currentText()
261
        elif attr == ua.AttributeIds.DataType:
262
            dv.Value.Value = editor.get_node().nodeid
263
            text = data_type_to_string(dv)
264
        elif attr in (ua.AttributeIds.AccessLevel,
265
                      ua.AttributeIds.UserAccessLevel,
266
                      ua.AttributeIds.WriteMask,
267
                      ua.AttributeIds.UserWriteMask,
268
                      ua.AttributeIds.EventNotifier):
269
            dv.Value.Value = editor.get_byte()
270
            text = enum_to_string(attr, dv)
271
        else:
272
            if isinstance(editor, QComboBox):
273
                text = editor.currentText()
274
            else:
275
                text = editor.text()
276
277
            try:
278
                # if user is setting a value on a null variant, try using the nodes datatype instead
279
                if dv.Value.VariantType is ua.VariantType.Null:
280
                    dtype = self.attrs_widget.current_node.get_data_type_as_variant_type()
281
                    dv.Value = string_to_variant(text, dtype)
282
                else:
283
                    dv.Value = string_to_variant(text, dv.Value.VariantType)
284
            except Exception as ex:
285
                self.error.emit(ex)
286
                raise
287
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: (attr, dv)})
288
289
290
def data_type_to_string(dv):
291
    # a bit too complex, we could just display browse name of node but it requires a query
292
    if isinstance(dv.Value.Value.Identifier, int) and dv.Value.Value.Identifier < 63:
293
        string = ua.datatype_to_varianttype(dv.Value.Value).name
294
    elif dv.Value.Value.Identifier in ua.ObjectIdNames:
295
        string = ua.ObjectIdNames[dv.Value.Value.Identifier]
296
    else:
297
        string = dv.Value.Value.to_string()
298
    return string
299
300
301
def attr_to_enum(attr):
302
    attr_name = attr.name
303
    if attr_name.startswith("User"):
304
        attr_name = attr_name[4:]
305
    return getattr(ua, attr_name)
306
307
308
def enum_to_string(attr, dv):
309
    attr_enum = attr_to_enum(attr)
310
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(dv.Value.Value)])
311
    return string
312