1
|
|
|
from PyQt5.QtCore import pyqtSignal, QMimeData, QObject |
2
|
|
|
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QIcon |
3
|
|
|
from PyQt5.QtWidgets import QApplication, QAbstractItemView |
4
|
|
|
|
5
|
|
|
from opcua import ua |
6
|
|
|
|
7
|
|
|
|
8
|
|
|
class TreeWidget(QObject): |
9
|
|
|
|
10
|
|
|
error = pyqtSignal(str) |
11
|
|
|
|
12
|
|
|
def __init__(self, view): |
13
|
|
|
QObject.__init__(self, view) |
14
|
|
|
self.view = view |
15
|
|
|
self.model = TreeViewModel() |
16
|
|
|
self.model.clear() # FIXME: do we need this? |
17
|
|
|
#self.model.error.connect(lambda x: self.error.emit(x)) |
18
|
|
|
self.model.error.connect(self.error) |
19
|
|
|
self.view.setModel(self.model) |
20
|
|
|
#self.view.setUniformRowHeights(True) |
21
|
|
|
self.view.setSelectionBehavior(QAbstractItemView.SelectRows) |
22
|
|
|
self.view.header().setSectionResizeMode(1) |
23
|
|
|
|
24
|
|
|
def clear(self): |
25
|
|
|
self.model.clear() |
26
|
|
|
|
27
|
|
|
def start(self, uaclient): |
28
|
|
|
self.model.clear() |
29
|
|
|
self.model.set_uaclient(uaclient) |
30
|
|
|
self.model.add_item(self._get_root_desc(uaclient)) |
31
|
|
|
|
32
|
|
|
def _get_root_desc(self, uaclient): |
33
|
|
|
node = uaclient.get_root_node() |
34
|
|
|
attrs = node.get_attributes([ua.AttributeIds.DisplayName, ua.AttributeIds.BrowseName, ua.AttributeIds.NodeId, ua.AttributeIds.NodeClass]) |
35
|
|
|
desc = ua.ReferenceDescription() |
36
|
|
|
desc.DisplayName = attrs[0].Value.Value |
37
|
|
|
desc.BrowseName = attrs[1].Value.Value |
38
|
|
|
desc.NodeId = attrs[2].Value.Value |
39
|
|
|
desc.NodeClass = attrs[3].Value.Value |
40
|
|
|
desc.TypeDefinition = ua.TwoByteNodeId(ua.ObjectIds.FolderType) |
41
|
|
|
return desc |
42
|
|
|
|
43
|
|
|
def copy_path(self): |
44
|
|
|
path = self.get_current_path() |
45
|
|
|
path_str = ",".join(path) |
46
|
|
|
QApplication.clipboard().setText(path_str) |
47
|
|
|
|
48
|
|
|
def copy_nodeid(self): |
49
|
|
|
node = self.get_current_node() |
50
|
|
|
if node: |
51
|
|
|
text = node.nodeid.to_string() |
52
|
|
|
else: |
53
|
|
|
text = "" |
54
|
|
|
QApplication.clipboard().setText(text) |
55
|
|
|
|
56
|
|
|
def get_current_path(self): |
57
|
|
|
idx = self.view.currentIndex() |
58
|
|
|
idx = idx.sibling(idx.row(), 0) |
59
|
|
|
it = self.model.itemFromIndex(idx) |
60
|
|
|
path = [] |
61
|
|
|
while it and it.data(): |
62
|
|
|
node = it.data() |
63
|
|
|
name = node.get_browse_name().to_string() |
64
|
|
|
path.insert(0, name) |
65
|
|
|
it = it.parent() |
66
|
|
|
return path |
67
|
|
|
|
68
|
|
|
def get_current_node(self, idx=None): |
69
|
|
|
if idx is None: |
70
|
|
|
idx = self.view.currentIndex() |
71
|
|
|
idx = idx.sibling(idx.row(), 0) |
72
|
|
|
it = self.model.itemFromIndex(idx) |
73
|
|
|
if not it: |
74
|
|
|
return None |
75
|
|
|
node = it.data() |
76
|
|
|
if not node: |
77
|
|
|
print("No node for item:", it, it.text()) |
78
|
|
|
return None |
79
|
|
|
return node |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
class TreeViewModel(QStandardItemModel): |
83
|
|
|
|
84
|
|
|
error = pyqtSignal(str) |
85
|
|
|
|
86
|
|
|
def __init__(self): |
87
|
|
|
super(TreeViewModel, self).__init__() |
88
|
|
|
self.uaclient = None |
89
|
|
|
self._fetched = [] |
90
|
|
|
|
91
|
|
|
def set_uaclient(self, uaclient): |
92
|
|
|
self.uaclient = uaclient |
93
|
|
|
|
94
|
|
|
def clear(self): |
95
|
|
|
QStandardItemModel.clear(self) |
96
|
|
|
self._fetched = [] |
97
|
|
|
self.setHorizontalHeaderLabels(['DisplayName', "BrowseName", 'NodeId']) |
98
|
|
|
|
99
|
|
|
def add_item(self, desc, parent=None): |
100
|
|
|
item = [QStandardItem(desc.DisplayName.to_string()), QStandardItem(desc.BrowseName.to_string()), QStandardItem(desc.NodeId.to_string())] |
101
|
|
|
if desc.NodeClass == ua.NodeClass.Object: |
102
|
|
|
if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.FolderType): |
103
|
|
|
item[0].setIcon(QIcon(":/folder.svg")) |
104
|
|
|
else: |
105
|
|
|
item[0].setIcon(QIcon(":/object.svg")) |
106
|
|
|
elif desc.NodeClass == ua.NodeClass.Variable: |
107
|
|
|
if desc.TypeDefinition == ua.TwoByteNodeId(ua.ObjectIds.PropertyType): |
108
|
|
|
item[0].setIcon(QIcon(":/property.svg")) |
109
|
|
|
else: |
110
|
|
|
item[0].setIcon(QIcon(":/variable.svg")) |
111
|
|
|
elif desc.NodeClass == ua.NodeClass.Method: |
112
|
|
|
item[0].setIcon(QIcon(":/method.svg")) |
113
|
|
|
|
114
|
|
|
item[0].setData(self.uaclient.get_node(desc.NodeId)) |
115
|
|
|
if parent: |
116
|
|
|
return parent.appendRow(item) |
117
|
|
|
else: |
118
|
|
|
return self.appendRow(item) |
119
|
|
|
|
120
|
|
|
def canFetchMore(self, idx): |
121
|
|
|
item = self.itemFromIndex(idx) |
122
|
|
|
if not item: |
123
|
|
|
return True |
124
|
|
|
node = item.data() |
125
|
|
|
if node not in self._fetched: |
126
|
|
|
self._fetched.append(node) |
127
|
|
|
return True |
128
|
|
|
return False |
129
|
|
|
|
130
|
|
|
def hasChildren(self, idx): |
131
|
|
|
item = self.itemFromIndex(idx) |
132
|
|
|
if not item: |
133
|
|
|
return True |
134
|
|
|
node = item.data() |
135
|
|
|
if node in self._fetched: |
136
|
|
|
return QStandardItemModel.hasChildren(self, idx) |
137
|
|
|
return True |
138
|
|
|
|
139
|
|
|
def fetchMore(self, idx): |
140
|
|
|
parent = self.itemFromIndex(idx) |
141
|
|
|
if parent: |
142
|
|
|
self._fetchMore(parent) |
143
|
|
|
|
144
|
|
|
def _fetchMore(self, parent): |
145
|
|
|
try: |
146
|
|
|
descs = parent.data().get_children_descriptions() |
147
|
|
|
for desc in descs: |
148
|
|
|
self.add_item(desc, parent) |
149
|
|
|
except Exception as ex: |
150
|
|
|
self.error.emit(ex) |
151
|
|
|
raise |
152
|
|
|
|
153
|
|
|
#def flags(self, idx): |
154
|
|
|
#item = self.itemFromIndex(idx) |
155
|
|
|
#flags = QStandardItemModel.flags(self, idx) |
156
|
|
|
#if not item: |
157
|
|
|
#return flags |
158
|
|
|
#node = item.data() |
159
|
|
|
#if node and node.get_node_class() == ua.NodeClass.Variable: |
160
|
|
|
## FIXME not efficient to query, should be stored in data() |
161
|
|
|
##print(1, flags) |
162
|
|
|
#return flags | Qt.ItemIsDropEnabled |
163
|
|
|
#else: |
164
|
|
|
#print(2, flags) |
165
|
|
|
#return flags |
166
|
|
|
|
167
|
|
|
#def mimeTypes(self): |
168
|
|
|
#return ["application/vnd.text.list"] |
169
|
|
|
|
170
|
|
|
def mimeData(self, idxs): |
171
|
|
|
mdata = QMimeData() |
172
|
|
|
nodes = [] |
173
|
|
|
for idx in idxs: |
174
|
|
|
item = self.itemFromIndex(idx) |
175
|
|
|
if item: |
176
|
|
|
node = item.data() |
177
|
|
|
if node: |
178
|
|
|
nodes.append(node.nodeid.to_string()) |
179
|
|
|
mdata.setText(", ".join(nodes)) |
180
|
|
|
return mdata |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
|