Completed
Pull Request — master (#5)
by Olivier
01:00
created

AttrsWidget   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 1
Metric Value
c 8
b 0
f 1
dl 0
loc 157
rs 8.3999
wmc 38

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_timestamps() 0 7 2
A _show_attrs() 0 7 3
A _show_value_attr() 0 3 1
A showContextMenu() 0 4 2
A _show_attr() 0 15 3
A _show_val() 0 13 4
B get_all_attrs() 0 13 6
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 _show_ext_obj() 0 7 2
A clear() 0 3 1
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
        items = self._show_val(self.model, "Value", dv.Value.Value, dv.Value.VariantType)
152
        items[1].setData((attr, dv), Qt.UserRole)
153
        #if self._timestamps:
154
            #self._show_timestamps(items[0], dv)
155
156
    def _show_val(self, parent, name, val, vtype):
157
        name_item = QStandardItem(name)
158
        vitem = QStandardItem()
159
        row = [name_item, vitem, QStandardItem(vtype.name)]
160
        parent.appendRow(row)
161
        if isinstance(val, (list, tuple)):
162
            for idx, element in enumerate(val):
163
                self._show_val(name_item, str(idx), element, vtype)
164
        elif vtype == ua.VariantType.ExtensionObject:
165
            self._show_ext_obj(name_item, val)
166
        else:
167
            vitem.setText(val_to_string(val))
168
        return row
169
170
    def _show_ext_obj(self, item, val):
171
        ext_it = QStandardItem("ExtensionObject: " + val.__class__.__name__)
172
        item.appendRow([ext_it, QStandardItem(), QStandardItem()])
173
        for att_name, att_type in val.ua_types.items():
174
            member_val = getattr(val, att_name)
175
            attr = getattr(ua.VariantType, att_type)
176
            self._show_val(ext_it, att_name, member_val, attr)
177
178
    def _show_timestamps(self, item, dv):
179
        while item.hasChildren():
180
            self.model.removeRow(0, item.index())
181
        string = val_to_string(dv.ServerTimestamp)
182
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
183
        string = val_to_string(dv.SourceTimestamp)
184
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
185
186
187
    def get_all_attrs(self):
188
        attrs = [attr for attr in ua.AttributeIds]
189
        try:
190
            dvs = self.current_node.get_attributes(attrs)
191
        except Exception as ex:
192
            self.error.emit(ex)
193
            raise
194
        res = []
195
        for idx, dv in enumerate(dvs):
196
            if dv.StatusCode.is_good():
197
                res.append((attrs[idx], dv))
198
        res.sort(key=lambda x: x[0].name)
199
        return res
200
201
202
class MyDelegate(QStyledItemDelegate):
203
204
    error = pyqtSignal(Exception)
205
206
    def __init__(self, parent, attrs_widget):
207
        QStyledItemDelegate.__init__(self, parent)
208
        self.attrs_widget = attrs_widget
209
210
    def createEditor(self, parent, option, idx):
211
        if idx.column() != 1:
212
            return None
213
        item = self.attrs_widget.model.itemFromIndex(idx)
214
        attr, dv = item.data(Qt.UserRole)
215
        text = item.text()
216
        if attr == ua.AttributeIds.NodeId:
217
            return None
218
        if dv.Value.VariantType == ua.VariantType.Boolean:
219
            combo = QComboBox(parent)
220
            combo.addItem("True")
221
            combo.addItem("False")
222
            combo.setCurrentText(text)
223
            return combo
224
        elif attr == ua.AttributeIds.NodeClass:
225
            combo = QComboBox(parent)
226
            for nclass in ua.NodeClass:
227
                combo.addItem(nclass.name)
228
            combo.setCurrentText(text)
229
            return combo
230
        elif attr == ua.AttributeIds.DataType:
231
            nodeid = getattr(ua.ObjectIds, text)
232
            node = Node(self.attrs_widget.current_node.server, nodeid)
233
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
234
            button = GetNodeButton(parent, node, startnode)
235
            return button
236
        elif attr in (ua.AttributeIds.AccessLevel,
237
                      ua.AttributeIds.UserAccessLevel,
238
                      ua.AttributeIds.WriteMask,
239
                      ua.AttributeIds.UserWriteMask,
240
                      ua.AttributeIds.EventNotifier):
241
            return BitEditor(parent, attr, dv.Value.Value)
242
        else:
243
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
244
245
    #def setEditorData(self, editor, index):
246
        #pass
247
248
    def setModelData(self, editor, model, idx):
249
        #item = self.attrs_widget.model.itemFromIndex(idx)
250
        attr, dv = model.data(idx, Qt.UserRole)
251
        if attr == ua.AttributeIds.NodeClass:
252
            dv.Value.Value = ua.NodeClass[editor.currentText()]
253
            text = editor.currentText()
254
        elif attr == ua.AttributeIds.DataType:
255
            dv.Value.Value = editor.get_node().nodeid
256
            text = data_type_to_string(dv)
257
        elif attr in (ua.AttributeIds.AccessLevel,
258
                      ua.AttributeIds.UserAccessLevel,
259
                      ua.AttributeIds.WriteMask,
260
                      ua.AttributeIds.UserWriteMask,
261
                      ua.AttributeIds.EventNotifier):
262
            dv.Value.Value = editor.get_byte()
263
            text = enum_to_string(attr, dv)
264
        else:
265
            if isinstance(editor, QComboBox):
266
                text = editor.currentText()
267
            else:
268
                text = editor.text()
269
270
            try:
271
                # if user is setting a value on a null variant, try using the nodes datatype instead
272
                if dv.Value.VariantType is ua.VariantType.Null:
273
                    dtype = self.attrs_widget.current_node.get_data_type_as_variant_type()
274
                    dv.Value = string_to_variant(text, dtype)
275
                else:
276
                    dv.Value = string_to_variant(text, dv.Value.VariantType)
277
            except Exception as ex:
278
                self.error.emit(ex)
279
                raise
280
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: (attr, dv)})
281
282
283
def data_type_to_string(dv):
284
    # a bit too complex, we could just display browse name of node but it requires a query
285
    if isinstance(dv.Value.Value.Identifier, int) and dv.Value.Value.Identifier < 63:
286
        string = ua.datatype_to_varianttype(dv.Value.Value).name
287
    elif dv.Value.Value.Identifier in ua.ObjectIdNames:
288
        string = ua.ObjectIdNames[dv.Value.Value.Identifier]
289
    else:
290
        string = dv.Value.Value.to_string()
291
    return string
292
293
294
def attr_to_enum(attr):
295
    attr_name = attr.name
296
    if attr_name.startswith("User"):
297
        attr_name = attr_name[4:]
298
    return getattr(ua, attr_name)
299
300
301
def enum_to_string(attr, dv):
302
    attr_enum = attr_to_enum(attr)
303
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(dv.Value.Value)])
304
    return string
305