Completed
Push — master ( 29b69f...70700b )
by Olivier
34s
created

TreeViewModel   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 118
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 5
Metric Value
wmc 38
c 11
b 0
f 5
dl 0
loc 118
rs 8.3999

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 3 1
A _get_node_desc() 0 9 1
A set_root_node() 0 3 1
A clear() 0 4 1
F add_item() 0 38 15
A hasChildren() 0 8 3
A mimeData() 0 11 4
B _fetchMore() 0 12 5
A fetchMore() 0 4 2
A canFetchMore() 0 9 3
A reset_cache() 0 3 2
1
from PyQt5.QtCore import pyqtSignal, QMimeData, QObject, Qt, QSettings
2
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QIcon
3
from PyQt5.QtWidgets import QApplication, QAbstractItemView, QAction
4
5
from opcua import ua
6
from opcua import Node
7
from opcua.ua import UaError
8
9
10
class TreeWidget(QObject):
11
12
    error = pyqtSignal(Exception)
13
14
    def __init__(self, view):
15
        QObject.__init__(self, view)
16
        self.view = view
17
        self.model = TreeViewModel()
18
        self.model.clear()  # FIXME: do we need this?
19
        self.model.error.connect(self.error)
20
        self.view.setModel(self.model)
21
22
        #self.view.setUniformRowHeights(True)
23
        self.model.setHorizontalHeaderLabels(['DisplayName', "BrowseName", 'NodeId'])
24
        self.view.header().setSectionResizeMode(0)
25
        self.view.header().setStretchLastSection(True)
26
        self.view.setSelectionBehavior(QAbstractItemView.SelectRows)
27
        self.settings = QSettings()
28
        state = self.settings.value("tree_widget_state", None)
29
        if state is not None:
30
            self.view.header().restoreState(state)
31
32
        self.actionReload = QAction("Reload", self)
33
        self.actionReload.triggered.connect(self.reload_current)
34
35
    def save_state(self):
36
        self.settings.setValue("tree_widget_state", self.view.header().saveState())
37
38
    def clear(self):
39
        self.model.clear()
40
41
    def set_root_node(self, node):
42
        self.model.clear()
43
        self.model.set_root_node(node)
44
        self.view.expandToDepth(0)
45
46
    def copy_path(self):
47
        path = self.get_current_path()
48
        path_str = ",".join(path)
49
        QApplication.clipboard().setText(path_str)
50
51
    def expand_current_node(self, expand=True):
52
        idx = self.view.currentIndex()
53
        self.view.setExpanded(idx, expand)
54
55
    def expand_to_node(self, node):
56
        """
57
        Expand tree until given node and select it
58
        """
59
        if isinstance(node, str):
60
            idxlist = self.model.match(self.model.index(0, 0), Qt.DisplayRole, node, 1, Qt.MatchExactly|Qt.MatchRecursive)
61
            node = self.model.data(idxlist[0], Qt.UserRole)
62
        path = node.get_path()
63
        for node in path:
64
            # FIXME: this would be the correct way if it would work
65
            #idxlist = self.model.match(self.model.index(0, 0), Qt.UserRole, QVariantnode, 2, Qt.MatchExactly|Qt.MatchRecursive)
66
            try:
67
                text = node.get_display_name().Text
68
            except UaError as ex:
69
                return
70
            idxlist = self.model.match(self.model.index(0, 0), Qt.DisplayRole, text, 1, Qt.MatchExactly|Qt.MatchRecursive)
71
            if idxlist:
72
                idx = idxlist[0]
73
                self.view.setExpanded(idx, True)
74
                self.view.setCurrentIndex(idx)
75
                self.view.activated.emit(idx)
76
77
    def copy_nodeid(self):
78
        node = self.get_current_node()
79
        text = node.nodeid.to_string()
80
        QApplication.clipboard().setText(text)
81
82
    def get_current_path(self):
83
        idx = self.view.currentIndex()
84
        idx = idx.sibling(idx.row(), 0)
85
        it = self.model.itemFromIndex(idx)
86
        path = []
87
        while it and it.data(Qt.UserRole):
88
            node = it.data(Qt.UserRole)
89
            name = node.get_browse_name().to_string()
90
            path.insert(0, name)
91
            it = it.parent()
92
        return path
93
94
    def update_browse_name_current_item(self, bname):
95
        idx = self.view.currentIndex()
96
        idx = idx.sibling(idx.row(), 1)
97
        it = self.model.itemFromIndex(idx)
98
        it.setText(bname.to_string())
99
100
    def update_display_name_current_item(self, dname):
101
        idx = self.view.currentIndex()
102
        idx = idx.sibling(idx.row(), 0)
103
        it = self.model.itemFromIndex(idx)
104
        it.setText(dname.Text)
105
106
    def reload_current(self):
107
        idx = self.view.currentIndex()
108
        idx = idx.sibling(idx.row(), 0)
109
        it = self.model.itemFromIndex(idx)
110
        if not it:
111
            return None
112
        self.reload(it)
113
114
    def reload(self, item=None):
115
        if item is None:
116
            item = self.model.item(0, 0)
117
        for _ in range(item.rowCount()):
118
            child_it = item.child(0, 0)
119
            node = child_it.data(Qt.UserRole)
120
            if node:
121
                self.model.reset_cache(node)
122
            item.takeRow(0)
123
        node = item.data(Qt.UserRole)
124
        if node:
125
            self.model.reset_cache(node)
126
            idx = self.model.indexFromItem(item)
127
            #if self.view.isExpanded(idx):
128
            #self.view.setExpanded(idx, True)
129
130
    def remove_current_item(self):
131
        idx = self.view.currentIndex()
132
        self.model.removeRow(idx.row(), idx.parent())
133
134
    def get_current_node(self, idx=None):
135
        if idx is None:
136
            idx = self.view.currentIndex()
137
        idx = idx.sibling(idx.row(), 0)
138
        it = self.model.itemFromIndex(idx)
139
        if not it:
140
            return None
141
        node = it.data(Qt.UserRole)
142
        if not node:
143
            ex = RuntimeError("Item does not contain node data, report!")
144
            self.error.emit(ex)
145
            raise ex
146
        return node
147
148
149
class TreeViewModel(QStandardItemModel):
150
151
    error = pyqtSignal(Exception)
152
153
    def __init__(self):
154
        super(TreeViewModel, self).__init__()
155
        self._fetched = []
156
157
    def clear(self):
158
        # remove all rows but not header!!
159
        self.removeRows(0, self.rowCount())
160
        self._fetched = []
161
162
    def set_root_node(self, node):
163
        desc = self._get_node_desc(node)
164
        self.add_item(desc, node=node)
165
166
    def _get_node_desc(self, node):
167
        attrs = node.get_attributes([ua.AttributeIds.DisplayName, ua.AttributeIds.BrowseName, ua.AttributeIds.NodeId, ua.AttributeIds.NodeClass])
168
        desc = ua.ReferenceDescription()
169
        desc.DisplayName = attrs[0].Value.Value
170
        desc.BrowseName = attrs[1].Value.Value
171
        desc.NodeId = attrs[2].Value.Value
172
        desc.NodeClass = attrs[3].Value.Value
173
        desc.TypeDefinition = ua.TwoByteNodeId(ua.ObjectIds.FolderType)
174
        return desc
175
176
    def add_item(self, desc, parent=None, node=None):
177
        dname = bname = nodeid = "No Value"
178
        if desc.DisplayName:
179
            dname = desc.DisplayName.to_string()
180
        if desc.BrowseName:
181
            bname = desc.BrowseName.to_string()
182
        if desc.NodeId:
183
            nodeid = desc.NodeId.to_string()
184
        item = [QStandardItem(dname), QStandardItem(bname), QStandardItem(nodeid)]
185
        if desc.NodeClass == ua.NodeClass.Object:
186
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.FolderType):
187
                item[0].setIcon(QIcon(":/folder.svg"))
188
            else:
189
                item[0].setIcon(QIcon(":/object.svg"))
190
        elif desc.NodeClass == ua.NodeClass.Variable:
191
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.PropertyType):
192
                item[0].setIcon(QIcon(":/property.svg"))
193
            else:
194
                item[0].setIcon(QIcon(":/variable.svg"))
195
        elif desc.NodeClass == ua.NodeClass.Method:
196
            item[0].setIcon(QIcon(":/method.svg"))
197
        elif desc.NodeClass == ua.NodeClass.ObjectType:
198
            item[0].setIcon(QIcon(":/object_type.svg"))
199
        elif desc.NodeClass == ua.NodeClass.VariableType:
200
            item[0].setIcon(QIcon(":/variable_type.svg"))
201
        elif desc.NodeClass == ua.NodeClass.DataType:
202
            item[0].setIcon(QIcon(":/data_type.svg"))
203
        elif desc.NodeClass == ua.NodeClass.ReferenceType:
204
            item[0].setIcon(QIcon(":/reference_type.svg"))
205
        if node:
206
            item[0].setData(node, Qt.UserRole)
207
        else:
208
            parent_node = parent.data(Qt.UserRole)
209
            item[0].setData(Node(parent_node.server, desc.NodeId), Qt.UserRole)
210
        if parent:
211
            return parent.appendRow(item)
212
        else:
213
            return self.appendRow(item)
214
215
    def reset_cache(self, node):
216
        if node in self._fetched:
217
            self._fetched.remove(node)
218
219
    def canFetchMore(self, idx):
220
        item = self.itemFromIndex(idx)
221
        if not item:
222
            return False
223
        node = item.data(Qt.UserRole)
224
        if node not in self._fetched:
225
            self._fetched.append(node)
226
            return True
227
        return False
228
229
    def hasChildren(self, idx):
230
        item = self.itemFromIndex(idx)
231
        if not item:
232
            return True
233
        node = item.data(Qt.UserRole)
234
        if node in self._fetched:
235
            return QStandardItemModel.hasChildren(self, idx)
236
        return True
237
238
    def fetchMore(self, idx):
239
        parent = self.itemFromIndex(idx)
240
        if parent:
241
            self._fetchMore(parent)
242
243
    def _fetchMore(self, parent):
244
        try:
245
            descs = parent.data(Qt.UserRole).get_children_descriptions()
246
            descs.sort(key=lambda x: x.BrowseName)
247
            added = []
248
            for desc in descs:
249
                if not desc.NodeId in added:
250
                    self.add_item(desc, parent)
251
                    added.append(desc.NodeId)
252
        except Exception as ex:
253
            self.error.emit(ex)
254
            raise
255
256
    def mimeData(self, idxs):
257
        mdata = QMimeData()
258
        nodes = []
259
        for idx in idxs:
260
            item = self.itemFromIndex(idx)
261
            if item:
262
                node = item.data(Qt.UserRole)
263
                if node:
264
                    nodes.append(node.nodeid.to_string())
265
        mdata.setText(", ".join(nodes))
266
        return mdata
267
268
269