manage signals between screens through sfascreen
[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 class NodeNameDelegate(QStyledItemDelegate):
52     def __init__(self, parent):
53         QStyledItemDelegate.__init__(self, parent)
54
55     def paint(self, painter, option, index):
56         model = index.model()
57         data = "%s" % index.data().toString()
58         status_index = model.index(index.row(), 2, index.parent())
59         status_data = status_index.data().toString()
60
61         if status_data not in (node_status['in'], node_status['remove'], node_status['add']):
62             # default view
63             QStyledItemDelegate.paint(self, painter, option, index)
64             return
65
66         fm = QFontMetrics(option.font)
67         rect = option.rect
68         rect.setWidth(fm.width(QString(data)) + 8)
69         rect.setHeight(rect.height() - 2)
70         rect.setX(rect.x() + 4)
71         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width() 
72
73         path = QPainterPath()
74         path.addRoundedRect(x, y, w, h, 4, 4)
75
76         painter.save()
77         painter.setRenderHint(QPainter.Antialiasing)
78         painter.drawRoundedRect(rect, 4, 4)
79
80         if status_data == node_status['in']: # already in the slice
81             painter.fillPath(path, QColor.fromRgb(0, 250, 0))
82             painter.setPen(QColor.fromRgb(0, 0, 0))
83             painter.drawText(option.rect, 0, QString(data))
84
85         elif status_data == node_status['add']: # newly added to the slice
86             painter.fillPath(path, QColor.fromRgb(0, 250, 0))
87             painter.setPen(QColor.fromRgb(0, 0, 0))
88             painter.drawText(option.rect, 0, QString(data))
89             painter.drawRect(x + w + 10, y + 3, 10, 10)
90             painter.fillRect(x + w + 10, y + 3, 10, 10, QColor.fromRgb(0, 250, 0))
91
92         elif status_data == node_status['remove']: # removed from the slice
93             painter.fillPath(path, QColor.fromRgb(250, 0, 0))
94             painter.setPen(QColor.fromRgb(0, 0, 0))
95             painter.drawText(option.rect, 0, QString(data))
96             painter.drawRect(x + w + 10, y + 3, 10, 10)
97             painter.fillRect(x + w + 10, y + 3, 10, 10, QColor.fromRgb(250, 0, 0))
98
99         painter.restore()
100
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 class SliceWidget(QWidget):
271     def __init__(self, parent):
272         QWidget.__init__(self, parent)
273
274         self.network_names = []
275         self.process = SfiProcess(self)
276
277         self.slicename = QLabel("", self)
278         self.updateSliceName()
279         self.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(self.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         self.parent().signalAll("rspecUpdated")
333
334     def readSliceRSpec(self):
335         rspec_file = config.getSliceRSpecFile()
336         if os.path.exists(rspec_file):
337             xml = open(rspec_file).read()
338             return xml
339         return None
340
341     def setStatus(self, msg, timeout=None):
342         self.parent().setStatus(msg, timeout)
343
344     def checkRunningProcess(self):
345         if self.process.isRunning():
346             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
347             return True
348         return False
349
350     def filter(self, filter_string):
351         # for hierarchical models QSortFilterProxyModel applies the
352         # sort recursively. if the parent doesn't match the criteria
353         # we won't be able to match the children. so we need to match
354         # parent (by matching the network_names)
355         networks = ["^%s$" % n for n in self.network_names]
356         filters = networks + [str(filter_string)]
357         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
358
359     def submit(self):
360         if self.checkRunningProcess():
361             return
362
363         rspec = RSpec(self.readSliceRSpec())
364         
365         no_change = True
366         all_child = self.nodeModel.rootItem.allChildItems()
367         for c in all_child:
368             testbed, hostname, status = c.itemData
369             if isinstance(status, QVariant):
370                 status = status.toString()
371
372             if status == node_status['add']:
373                 rspec.add_sliver(hostname)
374                 no_change = False
375             elif str(status) == node_status['remove']:
376                 rspec.remove_sliver(hostname)
377                 no_change = False
378
379         if no_change:
380             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
381             return
382
383         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
384         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
385
386         self.process.applyRSpec(rspec)
387         self.setStatus("Sending slice data (RSpec). This may take some time...")
388         
389
390     def refresh(self):
391         if not config.getSlice():
392             self.setStatus("<font color='red'>Slice not set yet!</font>")
393             return
394
395         if self.process.isRunning():
396             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
397             return
398
399         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
400         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
401
402         self.process.getRSpecFromSM()
403         self.setStatus("Updating slice data. This may take some time...")
404
405     def updateView(self):
406         global already_in_nodes
407         already_in_nodes = []
408         self.network_names = []
409         self.nodeModel.clear()
410         
411         rspec_string = self.readSliceRSpec()
412         if not rspec_string:
413             return None
414
415         networks = rspec_get_networks(rspec_string)
416         for network in networks:
417             self.network_names.append(network)
418             networkItem = TreeItem([QString(network), QString(""), QString("")], self.nodeModel.rootItem)
419
420             all_nodes = rspec_get_nodes_from_network(rspec_string, network)
421             sliver_nodes = rspec_get_sliver_nodes_from_network(rspec_string, network)
422             available_nodes = filter(lambda x:x not in sliver_nodes, all_nodes)
423
424             already_in_nodes += sliver_nodes
425
426             for node in sliver_nodes:
427                 nodeItem = TreeItem([QString(""), QString("%s" % node), QString(node_status['in'])], networkItem)
428                 networkItem.appendChild(nodeItem)
429
430             for node in available_nodes:
431                 nodeItem = TreeItem([QString(""), QString(node), QString(node_status['out'])], networkItem)
432                 networkItem.appendChild(nodeItem)
433
434             self.nodeModel.rootItem.appendChild(networkItem)
435
436         self.filterModel.setSourceModel(self.nodeModel)
437         self.filterModel.setFilterKeyColumn(-1)
438         self.filterModel.setDynamicSortFilter(True)
439
440         self.nodeView.setItemDelegateForColumn(1, self.nodeNameDelegate)
441         self.nodeView.setModel(self.filterModel)
442         self.nodeView.expandAll()
443         self.nodeView.resizeColumnToContents(1)
444
445     def updateSliceName(self):
446         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
447
448
449 class MainScreen(SfaScreen):
450     def __init__(self, parent):
451         SfaScreen.__init__(self, parent)
452
453         slice = SliceWidget(self)
454         self.init(slice, "Main Window", "OneLab Federation GUI")
455
456     def configurationChanged(self):
457         self.widget.updateSliceName()
458         self.widget.updateView()
459