implement xmlwindow as a screen (rspecscreen)
[sface.git] / sface / screens / mainscreen.py
1
2 import os
3 from PyQt4.QtCore import *
4 from PyQt4.QtGui import *
5
6 from sfa.util.rspecHelper import RSpec
7 from sface.sfahelper import *
8 from sface.config import config
9 from sface.sfiprocess import SfiProcess
10 from sface.screens.sfascreen import SfaScreen
11
12 already_in_nodes = []
13
14 node_status = { "in": "Already Selected",
15                 "out": "Not Selected",
16                 "add": "To be Added",
17                 "remove": "To be Removed"}
18
19 class NodeView(QTreeView):
20     def __init__(self, parent):
21         QTreeView.__init__(self, parent)
22
23         self.setAnimated(True)
24         self.setItemsExpandable(True)
25         self.setRootIsDecorated(True)
26         self.setAlternatingRowColors(True)
27 #        self.setSelectionMode(self.MultiSelection)
28         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
29         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
30         self.setToolTip("Double click on a row to change its status")
31
32     def mouseDoubleClickEvent(self, event):
33         index = self.currentIndex()
34         model = index.model()
35         status_index = model.index(index.row(), 2, index.parent())
36         status_data = status_index.data().toString()
37         hostname_index = model.index(index.row(), 1, index.parent())
38         hostname_data = hostname_index.data().toString()
39
40         if status_data == node_status['in']:
41             model.setData(status_index, QString(node_status['remove']))
42         elif status_data == node_status['out']:
43             model.setData(status_index, QString(node_status['add']))
44         elif status_data in (node_status['add'], node_status['remove']):
45             if hostname_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
46             else: model.setData(status_index, QString(node_status['out']))
47
48         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), hostname_index, hostname_index)
49                 
50         
51
52 class NodeNameDelegate(QStyledItemDelegate):
53     def __init__(self, parent):
54         QStyledItemDelegate.__init__(self, parent)
55
56     def paint(self, painter, option, index):
57         model = index.model()
58         data = "%s" % index.data().toString()
59         status_index = model.index(index.row(), 2, index.parent())
60         status_data = status_index.data().toString()
61
62         if status_data not in (node_status['in'], node_status['remove'], node_status['add']):
63             # default view
64             QStyledItemDelegate.paint(self, painter, option, index)
65             return
66
67         fm = QFontMetrics(option.font)
68         rect = option.rect
69         rect.setWidth(fm.width(QString(data)) + 8)
70         rect.setHeight(rect.height() - 2)
71         rect.setX(rect.x() + 4)
72         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width() 
73
74         path = QPainterPath()
75         path.addRoundedRect(x, y, w, h, 4, 4)
76
77         painter.save()
78         painter.setRenderHint(QPainter.Antialiasing)
79         painter.drawRoundedRect(rect, 4, 4)
80
81         if status_data == node_status['in']: # already in the slice
82             painter.fillPath(path, QColor.fromRgb(0, 250, 0))
83             painter.setPen(QColor.fromRgb(0, 0, 0))
84             painter.drawText(option.rect, 0, QString(data))
85
86         elif status_data == node_status['add']: # newly added to the slice
87             painter.fillPath(path, QColor.fromRgb(0, 250, 0))
88             painter.setPen(QColor.fromRgb(0, 0, 0))
89             painter.drawText(option.rect, 0, QString(data))
90             painter.drawRect(x + w + 10, y + 3, 10, 10)
91             painter.fillRect(x + w + 10, y + 3, 10, 10, QColor.fromRgb(0, 250, 0))
92
93         elif status_data == node_status['remove']: # removed from the slice
94             painter.fillPath(path, QColor.fromRgb(250, 0, 0))
95             painter.setPen(QColor.fromRgb(0, 0, 0))
96             painter.drawText(option.rect, 0, QString(data))
97             painter.drawRect(x + w + 10, y + 3, 10, 10)
98             painter.fillRect(x + w + 10, y + 3, 10, 10, QColor.fromRgb(250, 0, 0))
99
100         painter.restore()
101
102 class TreeItem:
103     def __init__(self, data, parent=None):
104         self.parentItem = parent
105         self.itemData = data
106         self.childItems = []
107
108     def clear(self):
109         for child in self.childItems:
110             child.clear()
111             del child
112         del self.childItems
113         self.childItems = []
114
115     def allChildItems(self):
116         all = []
117         for c in self.childItems:
118             all.append(c)
119             if c.childItems:
120                 for cc in c.childItems:
121                     all.append(cc)
122         return all
123
124     def appendChild(self, child):
125         self.childItems.append(child)
126
127     def child(self, row):
128         return self.childItems[row]
129     
130     def childCount(self):
131         return len(self.childItems)
132
133     def childNumber(self):
134         if self.parentItem:
135             return self.parentItem.childItems.index(self)
136         return 0
137
138     def columnCount(self):
139         return len(self.itemData)
140
141     def data(self, column):
142         return self.itemData[column]
143
144     def insertChildren(self, position, count, columns):
145         if position < 0 or position > len(self.childItems):
146             return False
147         
148         for row in range(count):
149             data = self.data(columns)
150             item = TreeItem(data, self)
151             self.childItems.insert(position, item)
152
153         return True
154
155     def insertColumns(self, position, columns):
156         if position < 0 or position > len(self.itemData):
157             return False
158
159         for column in range(columns):
160             self.itemData.insert(position, QVariant())
161         
162         for child in self.childItems:
163             child.insertColumns(position, columns)
164         
165         return True
166
167     def setData(self, column, value):
168         if column < 0 or column >= len(self.itemData):
169             return False
170
171         self.itemData[column] = value
172         return True
173     
174     def parent(self):
175         return self.parentItem
176
177
178 class NodeModel(QAbstractItemModel):
179     def __init__(self, parent):
180         QAbstractItemModel.__init__(self, parent)
181         self.__initRoot()
182
183     def clear(self):
184         self.rootItem.clear()
185         self.__initRoot()
186
187     def __initRoot(self):
188         self.rootItem = TreeItem([QString("Testbed"), QString("Hostname"), QString("Status")])
189
190     def getItem(self, index):
191         if index.isValid():
192             item = index.internalPointer()
193             if isinstance(item, TreeItem):
194                 return item
195         return self.rootItem
196
197     def headerData(self, section, orientation, role):
198         if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
199             return self.rootItem.data(section)
200         return QVariant()
201
202     def index(self, row, column, parent):
203         if not self.hasIndex(row, column, parent):
204             return QModelIndex()
205
206         parentItem = self.getItem(parent)
207         childItem = parentItem.child(row)
208         if childItem:
209             return self.createIndex(row, column, childItem)
210         else:
211             return QModelIndex()
212
213     def insertColumns(self, position, columns, parent):
214         self.beginInsertColumns(parent, position, position + columns -1)
215         ret = self.rootItem.insertColumns(position, columns)
216         self.endInsertColumns()
217         return ret
218
219     def insertRows(self, position, rows, parent):
220         parentItem = self.getItem(parent)
221         self.beginInsertRows(parent, position, position + rows -1)
222         ret = parentItem.insertChildren(position, rows, self.rootItem.columnCount())
223         self.endInsertRows()
224         return ret
225
226     def parent(self, index):
227         if not index.isValid():
228             return QModelIndex()
229
230         childItem = self.getItem(index)
231         parentItem = childItem.parent()
232         if parentItem is self.rootItem:
233             return QModelIndex()
234
235         return self.createIndex(parentItem.childNumber(), 0, parentItem)
236
237     def rowCount(self, parent=QModelIndex()):
238         parentItem = self.getItem(parent)
239         return parentItem.childCount()
240
241     def columnCount(self, parent=None):
242         return self.rootItem.columnCount()
243
244     def data(self, index, role):
245         if not index.isValid():
246             return QVariant()
247
248         if role != Qt.DisplayRole and role != Qt.EditRole:
249             return QVariant()
250
251         item = self.getItem(index)
252         return item.data(index.column())
253
254     def flags(self, index):
255         if not index.isValid():
256             return 0
257         return Qt.ItemIsEnabled | Qt.ItemIsSelectable# | Qt.ItemIsEditable
258
259     def setData(self, index, value, role):
260         if role != Qt.EditRole:
261             return False
262
263         item = self.getItem(index)
264         ret = item.setData(index.column(), value)
265         if ret:
266             self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
267         return ret
268
269
270
271 class SliceWidget(QWidget):
272     def __init__(self, parent):
273         QWidget.__init__(self, parent)
274
275         self.network_names = []
276         self.process = SfiProcess(self)
277
278         slicename = QLabel ("Slice : %s"%(config.getSlice() or "None"),self)
279         slicename.setScaledContents(False)
280         searchlabel = QLabel ("Search: ", self)
281         searchlabel.setScaledContents(False)
282         searchbox = QLineEdit(self)
283         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
284
285         toplayout = QHBoxLayout()
286         toplayout.addWidget(slicename, 0, Qt.AlignLeft)
287         toplayout.addStretch()
288         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
289         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
290
291         self.nodeView = NodeView(self)
292         self.nodeModel = NodeModel(self)
293         self.filterModel = QSortFilterProxyModel(self) # enable filtering
294
295         self.nodeNameDelegate = NodeNameDelegate(self)
296
297         refresh = QPushButton("Update Slice Data", self)
298         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
299         submit = QPushButton("Submit", self)
300         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
301
302         bottomlayout = QHBoxLayout()
303         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
304         bottomlayout.addStretch()
305         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
306
307         layout = QVBoxLayout()
308         layout.addLayout(toplayout)
309         layout.addWidget(self.nodeView)
310         layout.addLayout(bottomlayout)
311         self.setLayout(layout)
312         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
313
314         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
315         self.connect(submit, SIGNAL('clicked()'), self.submit)
316         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
317         self.connect(self.process, SIGNAL('readyReadStandardOutput()'), self.processOutputReady)
318         self.connect(self.process, SIGNAL('readyReadStandardError()'), self.processOutputReady)
319
320         self.updateView()
321
322     def processOutputReady(self):
323         self.parent().logWindow.setText(self.process.readOutput())
324
325     def submitFinished(self):
326         self.setStatus("<font color='green'>Slice data submitted.</font>")
327         QTimer.singleShot(1000, self.refresh)
328
329     def refreshFinished(self):
330         self.setStatus("<font color='green'>Slice data updated.</font>", timeout=5000)
331         self.updateView()
332
333     def readSliceRSpec(self):
334         rspec_file = config.getSliceRSpecFile()
335         if os.path.exists(rspec_file):
336             xml = open(rspec_file).read()
337             return xml
338         return None
339
340     def setStatus(self, msg, timeout=None):
341         self.parent().setStatus(msg, timeout)
342
343     def checkRunningProcess(self):
344         if self.process.isRunning():
345             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
346             return True
347         return False
348
349     def filter(self, filter_string):
350         # for hierarchical models QSortFilterProxyModel applies the
351         # sort recursively. if the parent doesn't match the criteria
352         # we won't be able to match the children. so we need to match
353         # parent (by matching the network_names)
354         networks = ["^%s$" % n for n in self.network_names]
355         filters = networks + [str(filter_string)]
356         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
357
358     def submit(self):
359         if self.checkRunningProcess():
360             return
361
362         rspec = RSpec(self.readSliceRSpec())
363         
364         no_change = True
365         all_child = self.nodeModel.rootItem.allChildItems()
366         for c in all_child:
367             testbed, hostname, status = c.itemData
368             if isinstance(status, QVariant):
369                 status = status.toString()
370
371             if status == node_status['add']:
372                 rspec.add_sliver(hostname)
373                 no_change = False
374             elif str(status) == node_status['remove']:
375                 rspec.remove_sliver(hostname)
376                 no_change = False
377
378         if no_change:
379             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
380             return
381
382         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
383         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
384
385         self.process.applyRSpec(rspec)
386         self.setStatus("Sending slice data (RSpec). This may take some time...")
387         
388
389     def refresh(self):
390         if not config.getSlice():
391             self.setStatus("<font color='red'>Slice not set yet!</font>")
392             return
393
394         if self.process.isRunning():
395             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
396             return
397
398         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
399         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
400
401         self.process.getRSpecFromSM()
402         self.setStatus("Updating slice data. This may take some time...")
403
404     def updateView(self):
405         global already_in_nodes
406         already_in_nodes = []
407         self.network_names = []
408         self.nodeModel.clear()
409         
410         rspec_string = self.readSliceRSpec()
411         if not rspec_string:
412             return None
413
414         networks = rspec_get_networks(rspec_string)
415         for network in networks:
416             self.network_names.append(network)
417             networkItem = TreeItem([QString(network), QString(""), QString("")], self.nodeModel.rootItem)
418
419             all_nodes = rspec_get_nodes_from_network(rspec_string, network)
420             sliver_nodes = rspec_get_sliver_nodes_from_network(rspec_string, network)
421             available_nodes = filter(lambda x:x not in sliver_nodes, all_nodes)
422
423             already_in_nodes += sliver_nodes
424
425             for node in sliver_nodes:
426                 nodeItem = TreeItem([QString(""), QString("%s" % node), QString(node_status['in'])], networkItem)
427                 networkItem.appendChild(nodeItem)
428
429             for node in available_nodes:
430                 nodeItem = TreeItem([QString(""), QString(node), QString(node_status['out'])], networkItem)
431                 networkItem.appendChild(nodeItem)
432
433             self.nodeModel.rootItem.appendChild(networkItem)
434
435         self.filterModel.setSourceModel(self.nodeModel)
436         self.filterModel.setFilterKeyColumn(-1)
437         self.filterModel.setDynamicSortFilter(True)
438
439         self.nodeView.setItemDelegateForColumn(1, self.nodeNameDelegate)
440         self.nodeView.setModel(self.filterModel)
441         self.nodeView.expandAll()
442         self.nodeView.resizeColumnToContents(1)
443
444
445 class MainScreen(SfaScreen):
446     def __init__(self, parent):
447         SfaScreen.__init__(self, parent)
448
449         slice = SliceWidget(self)
450         self.init(slice, "Main Window", "OneLab Federation GUI")