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

AttrsWidget   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 181
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 1
Metric Value
c 11
b 0
f 1
dl 0
loc 181
rs 8.2769
wmc 41

18 Methods

Rating   Name   Duplication   Size   Complexity  
A _copy_value() 0 4 2
A _show_timestamps() 0 7 1
A _show_attrs() 0 7 3
A _show_value_attr() 0 9 1
A showContextMenu() 0 4 2
A _item_expanded() 0 5 2
A _show_attr() 0 15 3
A _show_val() 0 16 4
B get_all_attrs() 0 13 6
A reload() 0 2 1
B _item_changed() 0 19 5
A _item_collapsed() 0 6 2
A get_current_item() 0 3 1
B __init__() 0 28 2
A show_attrs() 0 6 2
A _show_ext_obj() 0 9 2
A clear() 0 3 1
A save_state() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like AttrsWidget often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
        self.view.expanded.connect(self._item_expanded)
67
        self.view.collapsed.connect(self._item_collapsed)
68
69
        # Context menu
70
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
71
        self.view.customContextMenuRequested.connect(self.showContextMenu)
72
        copyaction = QAction("&Copy Value", self.model)
73
        copyaction.triggered.connect(self._copy_value)
74
        self._contextMenu = QMenu()
75
        self._contextMenu.addAction(copyaction)
76
77
    def save_state(self):
78
        self.settings.setValue("attrs_widget", self.view.header().saveState())
79
80
    def _item_expanded(self, idx):
81
        item = self.model.itemFromIndex(idx)
82
        if item.data(Qt.UserRole) == "Value Line":
83
            it = self.model.itemFromIndex(idx.sibling(0, 1))
84
            it.setText("")
85
86
    def _item_collapsed(self, idx):
87
        item = self.model.itemFromIndex(idx)
88
        if item.data(Qt.UserRole) == "Value Line":
89
            it = self.model.itemFromIndex(idx.sibling(0, 1))
90
            attr, dv = it.data(Qt.UserRole)
91
            it.setText(val_to_string(dv.Value.Value))
92
93
    def _item_changed(self, item):
94
        print("DATACHANGED DISABLED")
95
        return
96
        data = item.data(Qt.UserRole)
97
        if data is None:
98
            print("Error item has no data", item.text())
99
            return
100
        attr, dv = item.data(Qt.UserRole)
101
        if attr == ua.AttributeIds.Value:
102
            dv.SourceTimestamp = datetime.now()
103
        try:
104
            self.current_node.set_attribute(attr, dv)
105
        except Exception as ex:
106
            self.error.emit(ex)
107
            raise
108
        if attr == ua.AttributeIds.Value:
109
            it = self.model.item(item.index().row(), 0)
110
            self._show_timestamps(it, dv)
111
        self.modified.emit()
112
113
    def showContextMenu(self, position):
114
        item = self.get_current_item()
115
        if item:
116
            self._contextMenu.exec_(self.view.viewport().mapToGlobal(position))
117
118
    def get_current_item(self, col_idx=0):
119
        idx = self.view.currentIndex()
120
        return self.model.item(idx.row(), col_idx)
121
122
    def _copy_value(self, position):
123
        it = self.get_current_item(1)
124
        if it:
125
            QApplication.clipboard().setText(it.text())
126
127
    def clear(self):
128
        # remove all rows but not header!!
129
        self.model.removeRows(0, self.model.rowCount())
130
131
    def reload(self):
132
        self.show_attrs(self.current_node)
133
134
    def show_attrs(self, node):
135
        self.current_node = node
136
        self.clear()
137
        if self.current_node:
138
            self._show_attrs()
139
        self.view.expandToDepth(0)
140
141
    def _show_attrs(self):
142
        attrs = self.get_all_attrs()
143
        for attr, dv in attrs:
144
            if attr == ua.AttributeIds.Value:
145
                self._show_value_attr(attr, dv)
146
            else:
147
                self._show_attr(attr, dv)
148
149
    def _show_attr(self, attr, dv):
150
        if attr == ua.AttributeIds.DataType:
151
            string = data_type_to_string(dv)
152
        elif attr in (ua.AttributeIds.AccessLevel,
153
                      ua.AttributeIds.UserAccessLevel,
154
                      ua.AttributeIds.WriteMask,
155
                      ua.AttributeIds.UserWriteMask,
156
                      ua.AttributeIds.EventNotifier):
157
            string = enum_to_string(attr, dv)
158
        else:
159
            string = val_to_string(dv.Value)
160
        name_item = QStandardItem(attr.name)
161
        vitem = QStandardItem(string)
162
        vitem.setData((attr, dv), Qt.UserRole)
163
        self.model.appendRow([name_item, vitem, QStandardItem(dv.Value.VariantType.name)])
164
165
    def _show_value_attr(self, attr, dv):
166
        name_item = QStandardItem("DataValue")
167
        vitem = QStandardItem()
168
        items = self._show_val(name_item, "Value", dv.Value.Value, dv.Value.VariantType)
169
        items[0].setData("Value Line", Qt.UserRole)
170
        items[1].setData((attr, dv), Qt.UserRole)
171
        row = [name_item, vitem, QStandardItem(dv.Value.VariantType.name)]
172
        self.model.appendRow(row)
173
        self._show_timestamps(name_item, dv)
174
175
    def _show_val(self, parent, name, val, vtype, root=False):
176
        name_item = QStandardItem(name)
177
        vitem = QStandardItem()
178
        vitem.setText(val_to_string(val))
179
        row = [name_item, vitem, QStandardItem(vtype.name)]
180
        if isinstance(val, (list, tuple)):
181
            row[2].setText("List of " + vtype.name)
182
            for idx, element in enumerate(val):
183
                self._show_val(name_item, str(idx), element, vtype)
184
        elif vtype == ua.VariantType.ExtensionObject:
185
            self._show_ext_obj(name_item, val)
186
        else:
187
            pass
188
            #vitem.setText(val_to_string(val))
189
        parent.appendRow(row)
190
        return row
191
192
    def _show_ext_obj(self, item, val):
193
        item.setText(item.text() + ": " + val.__class__.__name__)
194
        #ext_it = QStandardItem("ExtensionObject: " + val.__class__.__name__)
195
        #item.appendRow([ext_it, QStandardItem(), QStandardItem()])
196
        for att_name, att_type in val.ua_types.items():
197
            member_val = getattr(val, att_name)
198
            attr = getattr(ua.VariantType, att_type)
199
            #self._show_val(ext_it, att_name, member_val, attr)
200
            self._show_val(item, att_name, member_val, attr)
201
202
    def _show_timestamps(self, item, dv):
203
        #while item.hasChildren():
204
            #self.model.removeRow(0, item.index())
205
        string = val_to_string(dv.ServerTimestamp)
206
        item.appendRow([QStandardItem("Server Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
207
        string = val_to_string(dv.SourceTimestamp)
208
        item.appendRow([QStandardItem("Source Timestamp"), QStandardItem(string), QStandardItem(ua.VariantType.DateTime.name)])
209
210
211
    def get_all_attrs(self):
212
        attrs = [attr for attr in ua.AttributeIds]
213
        try:
214
            dvs = self.current_node.get_attributes(attrs)
215
        except Exception as ex:
216
            self.error.emit(ex)
217
            raise
218
        res = []
219
        for idx, dv in enumerate(dvs):
220
            if dv.StatusCode.is_good():
221
                res.append((attrs[idx], dv))
222
        res.sort(key=lambda x: x[0].name)
223
        return res
224
225
226
class MyDelegate(QStyledItemDelegate):
227
228
    error = pyqtSignal(Exception)
229
230
    def __init__(self, parent, attrs_widget):
231
        QStyledItemDelegate.__init__(self, parent)
232
        self.attrs_widget = attrs_widget
233
234
    def createEditor(self, parent, option, idx):
235
        if idx.column() != 1:
236
            return None
237
        item = self.attrs_widget.model.itemFromIndex(idx)
238
        attr, dv = item.data(Qt.UserRole)
239
        text = item.text()
240
        if attr == ua.AttributeIds.NodeId:
241
            return None
242
        if dv.Value.VariantType == ua.VariantType.Boolean:
243
            combo = QComboBox(parent)
244
            combo.addItem("True")
245
            combo.addItem("False")
246
            combo.setCurrentText(text)
247
            return combo
248
        elif attr == ua.AttributeIds.NodeClass:
249
            combo = QComboBox(parent)
250
            for nclass in ua.NodeClass:
251
                combo.addItem(nclass.name)
252
            combo.setCurrentText(text)
253
            return combo
254
        elif attr == ua.AttributeIds.DataType:
255
            nodeid = getattr(ua.ObjectIds, text)
256
            node = Node(self.attrs_widget.current_node.server, nodeid)
257
            startnode = Node(self.attrs_widget.current_node.server, ua.ObjectIds.BaseDataType)
258
            button = GetNodeButton(parent, node, startnode)
259
            return button
260
        elif attr in (ua.AttributeIds.AccessLevel,
261
                      ua.AttributeIds.UserAccessLevel,
262
                      ua.AttributeIds.WriteMask,
263
                      ua.AttributeIds.UserWriteMask,
264
                      ua.AttributeIds.EventNotifier):
265
            return BitEditor(parent, attr, dv.Value.Value)
266
        else:
267
            return QStyledItemDelegate.createEditor(self, parent, option, idx)
268
269
    #def setEditorData(self, editor, index):
270
        #pass
271
272
    def setModelData(self, editor, model, idx):
273
        #item = self.attrs_widget.model.itemFromIndex(idx)
274
        attr, dv = model.data(idx, Qt.UserRole)
275
        if attr == ua.AttributeIds.NodeClass:
276
            dv.Value.Value = ua.NodeClass[editor.currentText()]
277
            text = editor.currentText()
278
        elif attr == ua.AttributeIds.DataType:
279
            dv.Value.Value = editor.get_node().nodeid
280
            text = data_type_to_string(dv)
281
        elif attr in (ua.AttributeIds.AccessLevel,
282
                      ua.AttributeIds.UserAccessLevel,
283
                      ua.AttributeIds.WriteMask,
284
                      ua.AttributeIds.UserWriteMask,
285
                      ua.AttributeIds.EventNotifier):
286
            dv.Value.Value = editor.get_byte()
287
            text = enum_to_string(attr, dv)
288
        else:
289
            if isinstance(editor, QComboBox):
290
                text = editor.currentText()
291
            else:
292
                text = editor.text()
293
294
            try:
295
                # if user is setting a value on a null variant, try using the nodes datatype instead
296
                if dv.Value.VariantType is ua.VariantType.Null:
297
                    dtype = self.attrs_widget.current_node.get_data_type_as_variant_type()
298
                    dv.Value = string_to_variant(text, dtype)
299
                else:
300
                    dv.Value = string_to_variant(text, dv.Value.VariantType)
301
            except Exception as ex:
302
                self.error.emit(ex)
303
                raise
304
        model.setItemData(idx, {Qt.DisplayRole: text, Qt.UserRole: (attr, dv)})
305
306
307
def data_type_to_string(dv):
308
    # a bit too complex, we could just display browse name of node but it requires a query
309
    if isinstance(dv.Value.Value.Identifier, int) and dv.Value.Value.Identifier < 63:
310
        string = ua.datatype_to_varianttype(dv.Value.Value).name
311
    elif dv.Value.Value.Identifier in ua.ObjectIdNames:
312
        string = ua.ObjectIdNames[dv.Value.Value.Identifier]
313
    else:
314
        string = dv.Value.Value.to_string()
315
    return string
316
317
318
def attr_to_enum(attr):
319
    attr_name = attr.name
320
    if attr_name.startswith("User"):
321
        attr_name = attr_name[4:]
322
    return getattr(ua, attr_name)
323
324
325
def enum_to_string(attr, dv):
326
    attr_enum = attr_to_enum(attr)
327
    string = ", ".join([e.name for e in attr_enum.parse_bitfield(dv.Value.Value)])
328
    return string
329