Completed
Push — master ( 0d6791...fdb625 )
by Olivier
01:03
created

TreeWidget.expand_to_node()   A

Complexity

Conditions 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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