Completed
Push — master ( e3e1f6...34077a )
by Olivier
01:07
created

TreeWidget.expand_to_node()   B

Complexity

Conditions 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 20
rs 8.5454
cc 5
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.decode()
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
76
    def copy_nodeid(self):
77
        node = self.get_current_node()
78
        text = node.nodeid.to_string()
79
        QApplication.clipboard().setText(text)
80
81
    def get_current_path(self):
82
        idx = self.view.currentIndex()
83
        idx = idx.sibling(idx.row(), 0)
84
        it = self.model.itemFromIndex(idx)
85
        path = []
86
        while it and it.data(Qt.UserRole):
87
            node = it.data(Qt.UserRole)
88
            name = node.get_browse_name().to_string()
89
            path.insert(0, name)
90
            it = it.parent()
91
        return path
92
93
    def update_browse_name_current_item(self, bname):
94
        idx = self.view.currentIndex()
95
        idx = idx.sibling(idx.row(), 1)
96
        it = self.model.itemFromIndex(idx)
97
        it.setText(bname.to_string())
98
99
    def update_display_name_current_item(self, dname):
100
        idx = self.view.currentIndex()
101
        idx = idx.sibling(idx.row(), 0)
102
        it = self.model.itemFromIndex(idx)
103
        it.setText(dname.Text.decode())
104
105
    def reload_current(self):
106
        idx = self.view.currentIndex()
107
        idx = idx.sibling(idx.row(), 0)
108
        it = self.model.itemFromIndex(idx)
109
        if not it:
110
            return None
111
        self.reload(it)
112
113
    def reload(self, item=None):
114
        if item is None:
115
            item = self.model.item(0, 0)
116
        for _ in range(item.rowCount()):
117
            child_it = item.child(0, 0)
118
            node = child_it.data(Qt.UserRole)
119
            if node:
120
                self.model.reset_cache(node)
121
            item.takeRow(0)
122
        node = item.data(Qt.UserRole)
123
        if node:
124
            self.model.reset_cache(node)
125
            idx = self.model.indexFromItem(item)
126
            #if self.view.isExpanded(idx):
127
            #self.view.setExpanded(idx, True)
128
129
    def remove_current_item(self):
130
        idx = self.view.currentIndex()
131
        self.model.removeRow(idx.row(), idx.parent())
132
133
    def get_current_node(self, idx=None):
134
        if idx is None:
135
            idx = self.view.currentIndex()
136
        idx = idx.sibling(idx.row(), 0)
137
        it = self.model.itemFromIndex(idx)
138
        if not it:
139
            return None
140
        node = it.data(Qt.UserRole)
141
        if not node:
142
            ex = RuntimeError("Item does not contain node data, report!")
143
            self.error.emit(ex)
144
            raise ex
145
        return node
146
147
148
class TreeViewModel(QStandardItemModel):
149
150
    error = pyqtSignal(Exception)
151
152
    def __init__(self):
153
        super(TreeViewModel, self).__init__()
154
        self._fetched = []
155
156
    def clear(self):
157
        # remove all rows but not header!!
158
        self.removeRows(0, self.rowCount())
159
        self._fetched = []
160
161
    def set_root_node(self, node):
162
        desc = self._get_node_desc(node)
163
        self.add_item(desc, node=node)
164
165
    def _get_node_desc(self, node):
166
        attrs = node.get_attributes([ua.AttributeIds.DisplayName, ua.AttributeIds.BrowseName, ua.AttributeIds.NodeId, ua.AttributeIds.NodeClass])
167
        desc = ua.ReferenceDescription()
168
        desc.DisplayName = attrs[0].Value.Value
169
        desc.BrowseName = attrs[1].Value.Value
170
        desc.NodeId = attrs[2].Value.Value
171
        desc.NodeClass = attrs[3].Value.Value
172
        desc.TypeDefinition = ua.TwoByteNodeId(ua.ObjectIds.FolderType)
173
        return desc
174
175
    def add_item(self, desc, parent=None, node=None):
176
        item = [QStandardItem(desc.DisplayName.to_string()), QStandardItem(desc.BrowseName.to_string()), QStandardItem(desc.NodeId.to_string())]
177
        if desc.NodeClass == ua.NodeClass.Object:
178
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.FolderType):
179
                item[0].setIcon(QIcon(":/folder.svg"))
180
            else:
181
                item[0].setIcon(QIcon(":/object.svg"))
182
        elif desc.NodeClass == ua.NodeClass.Variable:
183
            if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.PropertyType):
184
                item[0].setIcon(QIcon(":/property.svg"))
185
            else:
186
                item[0].setIcon(QIcon(":/variable.svg"))
187
        elif desc.NodeClass == ua.NodeClass.Method:
188
            item[0].setIcon(QIcon(":/method.svg"))
189
        elif desc.NodeClass == ua.NodeClass.ObjectType:
190
            item[0].setIcon(QIcon(":/object_type.svg"))
191
        elif desc.NodeClass == ua.NodeClass.VariableType:
192
            item[0].setIcon(QIcon(":/variable_type.svg"))
193
        elif desc.NodeClass == ua.NodeClass.DataType:
194
            item[0].setIcon(QIcon(":/data_type.svg"))
195
        elif desc.NodeClass == ua.NodeClass.ReferenceType:
196
            item[0].setIcon(QIcon(":/reference_type.svg"))
197
        if node:
198
            item[0].setData(node, Qt.UserRole)
199
        else:
200
            parent_node = parent.data(Qt.UserRole)
201
            item[0].setData(Node(parent_node.server, desc.NodeId), Qt.UserRole)
202
        if parent:
203
            return parent.appendRow(item)
204
        else:
205
            return self.appendRow(item)
206
207
    def reset_cache(self, node):
208
        if node in self._fetched:
209
            self._fetched.remove(node)
210
211
    def canFetchMore(self, idx):
212
        item = self.itemFromIndex(idx)
213
        if not item:
214
            return False
215
        node = item.data(Qt.UserRole)
216
        if node not in self._fetched:
217
            self._fetched.append(node)
218
            return True
219
        return False
220
221
    def hasChildren(self, idx):
222
        item = self.itemFromIndex(idx)
223
        if not item:
224
            return True
225
        node = item.data(Qt.UserRole)
226
        if node in self._fetched:
227
            return QStandardItemModel.hasChildren(self, idx)
228
        return True
229
230
    def fetchMore(self, idx):
231
        parent = self.itemFromIndex(idx)
232
        if parent:
233
            self._fetchMore(parent)
234
235
    def _fetchMore(self, parent):
236
        try:
237
            descs = parent.data(Qt.UserRole).get_children_descriptions()
238
            descs.sort(key=lambda x: x.BrowseName)
239
            added = []
240
            for desc in descs:
241
                if not desc.NodeId in added:
242
                    self.add_item(desc, parent)
243
                    added.append(desc.NodeId)
244
        except Exception as ex:
245
            self.error.emit(ex)
246
            raise
247
248
    def mimeData(self, idxs):
249
        mdata = QMimeData()
250
        nodes = []
251
        for idx in idxs:
252
            item = self.itemFromIndex(idx)
253
            if item:
254
                node = item.data(Qt.UserRole)
255
                if node:
256
                    nodes.append(node.nodeid.to_string())
257
        mdata.setText(", ".join(nodes))
258
        return mdata
259
260
261