ad6fee500fa3111deb5cfcb95f6fc5289f708970
[bertos.git] / wizard / BModulePage.py
1 #!/usr/bin/env python
2 # encoding: utf-8
3 #
4 # This file is part of BeRTOS.
5 #
6 # Bertos is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19 #
20 # As a special exception, you may use this file as part of a free software
21 # library without restriction.  Specifically, if other files instantiate
22 # templates or use macros or inline functions from this file, or you compile
23 # this file and link it with other files to produce an executable, this
24 # file does not by itself cause the resulting executable to be covered by
25 # the GNU General Public License.  This exception does not however
26 # invalidate any other reasons why the executable file might be covered by
27 # the GNU General Public License.
28 #
29 # Copyright 2008 Develer S.r.l. (http://www.develer.com/)
30 #
31 # $Id$
32 #
33 # Author: Lorenzo Berni <duplo@develer.com>
34 #
35
36 import os
37
38 from PyQt4.QtCore import *
39 from PyQt4.QtGui import *
40 from BWizardPage import *
41 import bertos_utils
42
43 from bertos_utils import SupportedException
44 from DefineException import *
45 from const import *
46
47 class BModulePage(BWizardPage):
48     """
49     Page of the wizard that permits to select and configurate the BeRTOS modules.
50     """
51     
52     def __init__(self):
53         BWizardPage.__init__(self, UI_LOCATION + "/module_select.ui")
54         self.setTitle(self.tr("Configure the BeRTOS modules"))
55         self._control_group = QControlGroup()
56         ## special connection needed for the QControlGroup
57         self.connect(self._control_group, SIGNAL("stateChanged"), self.saveValue)
58     
59     ## Overloaded BWizardPage methods ##
60
61     def setupUi(self):
62         """
63         Overload of BWizardPage setupUi method.
64         """
65         self.pageContent.moduleTree.clear()
66         self.pageContent.moduleTree.setHeaderHidden(True)
67         self.pageContent.propertyTable.horizontalHeader().setResizeMode(QHeaderView.Stretch)
68         self.pageContent.propertyTable.horizontalHeader().setVisible(False)
69         self.pageContent.propertyTable.verticalHeader().setResizeMode(QHeaderView.ResizeToContents)
70         self.pageContent.propertyTable.verticalHeader().setVisible(False)
71         self.pageContent.propertyTable.setColumnCount(2)
72         self.pageContent.propertyTable.setRowCount(0)
73         self.pageContent.moduleLabel.setVisible(False)
74         self.pageContent.warningLabel.setVisible(False)
75     
76     def connectSignals(self):
77         """
78         Overload of the BWizardPage connectSignals method.
79         """
80         self.connect(self.pageContent.moduleTree, SIGNAL("itemPressed(QTreeWidgetItem*, int)"), self.fillPropertyTable)
81         self.connect(self.pageContent.moduleTree, SIGNAL("itemPressed(QTreeWidgetItem*, int)"), self.moduleClicked)
82         self.connect(self.pageContent.moduleTree, SIGNAL("itemChanged(QTreeWidgetItem*, int)"), self.dependencyCheck)
83
84     def reloadData(self):
85         """
86         Overload of the BWizardPage reloadData method.
87         """
88         try:
89             QApplication.instance().setOverrideCursor(Qt.WaitCursor)
90             self.setupUi()
91             self.loadModuleData()
92             self.fillModuleTree()
93         finally:
94             QApplication.instance().restoreOverrideCursor()
95     
96     ####
97     
98     ## Slots ##
99
100     def moduleClicked(self, item, column):
101         self.setBold(item, False)
102
103     def fillPropertyTable(self):
104         """
105         Slot called when the user selects a module from the module tree.
106         Fills the property table using the configuration parameters defined in
107         the source tree.
108         """
109         module = self.currentModule()
110         if module:
111             try:
112                 supported = bertos_utils.isSupported(self.project, module=module)
113             except SupportedException, e:
114                 self.exceptionOccurred(self.tr("Error evaluating \"%1\" for module %2").arg(e.support_string).arg(module))
115                 supported = True
116             self._control_group.clear()
117             configuration = self.projectInfo("MODULES")[module]["configuration"]
118             module_description = self.projectInfo("MODULES")[module]["description"]
119             self.pageContent.moduleLabel.setText(module_description)
120             self.pageContent.moduleLabel.setVisible(True)
121             if not supported:
122                 self.pageContent.warningLabel.setVisible(True)
123                 selected_cpu = self.projectInfo("CPU_NAME")
124                 self.pageContent.warningLabel.setText(self.tr("<font color='#FF0000'>Warning: the selected module, \
125                     is not completely supported by the %1.</font>").arg(selected_cpu))
126             else:
127                 self.pageContent.warningLabel.setVisible(False)
128             self.pageContent.propertyTable.clear()
129             self.pageContent.propertyTable.setRowCount(0)
130             if configuration != "":
131                 configurations = self.projectInfo("CONFIGURATIONS")[configuration]
132                 param_list = sorted(configurations["paramlist"])
133                 index = 0
134                 for i, property in param_list:
135                     if "type" in configurations[property]["informations"] and configurations[property]["informations"]["type"] == "autoenabled":
136                         # Doesn't show the hidden fields
137                         continue
138                     try:
139                         param_supported = bertos_utils.isSupported(self.project, property_id=(configuration, property))
140                     except SupportedException, e:
141                         self.exceptionOccurred(self.tr("Error evaluating \"%1\" for parameter %2").arg(e.support_string).arg(property))
142                         param_supported = True
143                     if not param_supported:
144                         # Doesn't show the unsupported parameters
145                         continue
146                     # Set the row count to the current index + 1
147                     self.pageContent.propertyTable.setRowCount(index + 1)
148                     item = QTableWidgetItem(configurations[property]["brief"])
149                     item.setFlags(item.flags() & ~Qt.ItemIsSelectable)
150                     item.setToolTip(property)
151                     item.setData(Qt.UserRole, qvariant_converter.convertString(property))
152                     self.pageContent.propertyTable.setItem(index, 0, item)
153                     if "type" in configurations[property]["informations"] and configurations[property]["informations"]["type"] == "boolean":
154                         self.insertCheckBox(index, configurations[property]["value"])
155                     elif "type" in configurations[property]["informations"] and configurations[property]["informations"]["type"] == "enum":
156                         self.insertComboBox(index, configurations[property]["value"], configurations[property]["informations"]["value_list"])
157                     elif "type" in configurations[property]["informations"] and configurations[property]["informations"]["type"] == "int":
158                         self.insertSpinBox(index, configurations[property]["value"], configurations[property]["informations"])
159                     else:
160                         # Not defined type, rendered as a text field
161                         self.pageContent.propertyTable.setItem(index, 1, QTableWidgetItem(configurations[property]["value"]))
162                     index += 1
163             if self.pageContent.propertyTable.rowCount() == 0:
164                 module_label = self.pageContent.moduleLabel.text()
165                 module_label += "\n\nNo configuration needed."
166                 self.pageContent.moduleLabel.setText(module_label) 
167         else:
168             self.pageContent.moduleLabel.setText("")
169             self.pageContent.moduleLabel.setVisible(False)
170             self.pageContent.propertyTable.clear()
171             self.pageContent.propertyTable.setRowCount(0)
172
173     def dependencyCheck(self, item):
174         """
175         Checks the dependencies of the module associated with the given item.
176         """
177         checked = False
178         module = unicode(item.text(0))
179         if item.checkState(0) == Qt.Checked:
180             self.moduleSelected(module)
181         else:
182             self.moduleUnselected(module)
183             self.removeFileDependencies(module)
184
185     def showPropertyDescription(self):
186         """
187         Slot called when the property selection changes. Shows the description
188         of the selected property.
189         """
190         self.resetPropertyDescription()
191         configurations = self.currentModuleConfigurations()
192         if self.currentProperty() in configurations:
193             description = configurations[self.currentProperty()]["brief"]
194             name = self.currentProperty()
195             self.currentPropertyItem().setText(description + "\n" + name)
196
197     def saveValue(self, index):
198         """
199         Slot called when the user modifies one of the configuration parameters.
200         It stores the new value."""
201         property = qvariant_converter.getString(self.pageContent.propertyTable.item(index, 0).data(Qt.UserRole))
202         configuration = self.projectInfo("MODULES")[self.currentModule()]["configuration"]
203         configurations = self.projectInfo("CONFIGURATIONS")
204         if "type" not in configurations[configuration][property]["informations"] or configurations[configuration][property]["informations"]["type"] == "int":
205             configurations[configuration][property]["value"] = unicode(int(self.pageContent.propertyTable.cellWidget(index, 1).value()))
206         elif configurations[configuration][property]["informations"]["type"] == "enum":
207             configurations[configuration][property]["value"] = unicode(self.pageContent.propertyTable.cellWidget(index, 1).currentText())
208         elif configurations[configuration][property]["informations"]["type"] == "boolean":
209             if self.pageContent.propertyTable.cellWidget(index, 1).isChecked():
210                 configurations[configuration][property]["value"] = "1"
211             else:
212                 configurations[configuration][property]["value"] = "0"
213         self.setProjectInfo("CONFIGURATIONS", configurations)
214         if self.moduleItem(self.currentModule()).checkState(0) == Qt.Checked:
215             self.dependencyCheck(self.moduleItem(self.currentModule()))
216
217     ####
218     
219     def loadModuleData(self):
220         """
221         Loads the module data.
222         """
223         # Do not load the module data again when the Wizard is in editing mode
224         # or when it's working on a preset.
225         if not self.project.edit and not self.project.from_preset:
226             # Load the module data every time so that if the user changed the cpu
227             # the right configurations are picked up.
228             try:
229                 self.project.loadModuleData()
230             except ModuleDefineException, e:
231                 self.exceptionOccurred(self.tr("Error parsing line '%2' in file %1").arg(e.path).arg(e.line))
232             except EnumDefineException, e:
233                 self.exceptionOccurred(self.tr("Error parsing line '%2' in file %1").arg(e.path).arg(e.line))
234             except ConfigurationDefineException, e:
235                 self.exceptionOccurred(self.tr("Error parsing line '%2' in file %1").arg(e.path).arg(e.line))
236     
237     def fillModuleTree(self):
238         """
239         Fills the module tree with the module entries separated in categories.
240         """
241         self.pageContent.moduleTree.clear()
242         modules = self.projectInfo("MODULES")
243         if not modules:
244             return
245         categories = {}
246         for module, information in modules.items():
247             if information["category"] not in categories:
248                 categories[information["category"]] = []
249             categories[information["category"]].append(module)
250         for category, module_list in categories.items():
251             item = QTreeWidgetItem(QStringList([category]))
252             for module in module_list:
253                 enabled = modules[module]["enabled"]
254                 module_item = QTreeWidgetItem(item, QStringList([module]))
255                 try:
256                     supported = bertos_utils.isSupported(self.project, module=module)
257                 except SupportedException, e:
258                     self.exceptionOccurred(self.tr("Error evaluating \"%1\" for module %2").arg(e.support_string).arg(module))
259                     supported = True
260                 if not supported:
261                     module_item.setForeground(0, QBrush(QColor(Qt.red)))
262                 if enabled:
263                     module_item.setCheckState(0, Qt.Checked)
264                 else:
265                     module_item.setCheckState(0, Qt.Unchecked)
266             self.pageContent.moduleTree.addTopLevelItem(item)
267         self.pageContent.moduleTree.sortItems(0, Qt.AscendingOrder)
268         self.fillPropertyTable()
269             
270     def insertCheckBox(self, index, value):
271         """
272         Inserts in the table at index a checkbox for a boolean property setted
273         to value.
274         """
275         check_box = QCheckBox()
276         self.pageContent.propertyTable.setCellWidget(index, 1, check_box)
277         if value == "1":
278             check_box.setChecked(True)
279         else:
280             check_box.setChecked(False)
281         self._control_group.addControl(index, check_box)
282     
283     def insertComboBox(self, index, value, value_list):
284         """
285         Inserts in the table at index a combobox for an enum property setted
286         to value.
287         """
288         try:
289             enum = self.projectInfo("LISTS")[value_list]
290             combo_box = QComboBox()
291             self.pageContent.propertyTable.setCellWidget(index, 1, combo_box)
292             for i, element in enumerate(enum):
293                 combo_box.addItem(element)
294                 if element == value:
295                     combo_box.setCurrentIndex(i)
296             self._control_group.addControl(index, combo_box)
297         except KeyError:
298             self.exceptionOccurred(self.tr("Define list \"%1\" not found. Check definition files.").arg(value_list))
299             self.pageContent.propertyTable.setItem(index, 1, QTableWidgetItem(value))
300     
301     def insertSpinBox(self, index, value, informations):
302         """
303         Inserts in the table at index a spinbox for an int, a long or an unsigned
304         long property setted to value.
305         """
306         # int, long or undefined type property
307         spin_box = None
308         if bertos_utils.isLong(informations) or bertos_utils.isUnsignedLong(informations):
309             spin_box = QDoubleSpinBox()
310             spin_box.setDecimals(0)
311         else:
312             spin_box = QSpinBox()
313         self.pageContent.propertyTable.setCellWidget(index, 1, spin_box)
314         minimum = -32768
315         maximum = 32767
316         suff = ""
317         if bertos_utils.isLong(informations):
318             minimum = -2147483648
319             maximum = 2147483647
320             suff = "L"
321         elif bertos_utils.isUnsigned(informations):
322             minimum = 0
323             maximum = 65535
324             suff = "U"
325         elif bertos_utils.isUnsignedLong(informations):
326             minimum = 0
327             maximum = 4294967295
328             suff = "UL"
329         if "min" in informations:
330             minimum = int(informations["min"])
331         if "max" in informations:
332             maximum = int(informations["max"])
333         spin_box.setRange(minimum, maximum)
334         spin_box.setSuffix(suff)
335         spin_box.setValue(int(value.replace("L", "").replace("U", "")))
336         self._control_group.addControl(index, spin_box)
337         
338     
339     def currentModule(self):
340         """
341         Retuns the current module name.
342         """
343         current_module = self.pageContent.moduleTree.currentItem()
344         # return only the child items
345         if current_module and current_module.parent():
346             return unicode(current_module.text(0))
347         else:
348             return None
349
350     def moduleItem(self, module):
351         for top_level_index in range(self.pageContent.moduleTree.topLevelItemCount()):
352             top_level_item = self.pageContent.moduleTree.topLevelItem(top_level_index)
353             for child_index in range(top_level_item.childCount()):
354                 child_item = top_level_item.child(child_index)
355                 if unicode(child_item.text(0)) == module:
356                     return child_item
357         return None
358     
359     def currentModuleConfigurations(self):
360         """
361         Returns the current module configuration.
362         """
363         return self.configurations(self.currentModule())
364     
365     def currentProperty(self):
366         """
367         Rerturns the current property from the property table.
368         """
369         return qvariant_converter.getString(self.pageContent.propertyTable.item(self.pageContent.propertyTable.currentRow(), 0).data(Qt.UserRole))
370     
371     def currentPropertyItem(self):
372         """
373         Returns the QTableWidgetItem of the current property.
374         """
375         return self.pageContent.propertyTable.item(self.pageContent.propertyTable.currentRow(), 0)
376     
377     def configurations(self, module):
378         """
379         Returns the configuration for the selected module.
380         """
381         configuration = []
382         if module:
383             # On linux platform it seems that the behaviour of the focus
384             # changing is a bit different from the mac one. So if module is
385             # None then no configurations should be returned.
386             configuration = self.projectInfo("MODULES")[module]["configuration"]
387         if len(configuration) > 0:
388             return self.projectInfo("CONFIGURATIONS")[configuration]
389         else:
390             return {}
391     
392     def resetPropertyDescription(self):
393         """
394         Resets the label for each property table entry.
395         """
396         for index in range(self.pageContent.propertyTable.rowCount()):
397             property_name = qvariant_converter.getString(self.pageContent.propertyTable.item(index, 0).data(Qt.UserRole))
398             # Awful solution! Needed because if the user change the module, the selection changed...
399             if property_name not in self.currentModuleConfigurations():
400                 break
401             self.pageContent.propertyTable.item(index, 0).setText(self.currentModuleConfigurations()[property_name]['brief'])
402     
403     def setBold(self, item, bold):
404         self.pageContent.moduleTree.blockSignals(True)
405         font = item.font(0)
406         font.setBold(bold)
407         item.setFont(0, font)
408         self.pageContent.moduleTree.blockSignals(False)
409
410     def moduleSelected(self, selectedModule):
411         """
412         Resolves the selection dependencies.
413         """
414         try:
415             qApp.setOverrideCursor(Qt.WaitCursor)
416             modules = self.projectInfo("MODULES")
417             modules[selectedModule]["enabled"] = True
418             self.setProjectInfo("MODULES", modules)
419             depends = self.projectInfo("MODULES")[selectedModule]["depends"]
420             unsatisfied = []
421             if self.pageContent.automaticFix.isChecked():
422                 unsatisfied = self.selectDependencyCheck(selectedModule)
423             if len(unsatisfied) > 0:
424                 for module in unsatisfied:
425                     modules = self.projectInfo("MODULES")
426                     modules[module]["enabled"] = True
427                 for category in range(self.pageContent.moduleTree.topLevelItemCount()):
428                     item = self.pageContent.moduleTree.topLevelItem(category)
429                     for child in range(item.childCount()):
430                         if unicode(item.child(child).text(0)) in unsatisfied:
431                             self.setBold(item.child(child), True)
432                             self.setBold(item, True)
433                             item.child(child).setCheckState(0, Qt.Checked)
434         finally:
435             qApp.restoreOverrideCursor()
436     
437     def moduleUnselected(self, unselectedModule):
438         """
439         Resolves the unselection dependencies.
440         """
441         try:
442             qApp.setOverrideCursor(Qt.WaitCursor)
443             modules = self.projectInfo("MODULES")
444             modules[unselectedModule]["enabled"] = False
445             self.setProjectInfo("MODULES", modules)
446             unsatisfied = []
447             unsatisfied_params = []
448             if self.pageContent.automaticFix.isChecked():
449                 unsatisfied, unsatisfied_params = self.unselectDependencyCheck(unselectedModule)
450             if len(unsatisfied) > 0 or len(unsatisfied_params) > 0:
451                 message = []
452                 heading = self.tr("The module %1 is needed by").arg(unselectedModule)
453                 message.append(heading)
454                 module_list = ", ".join(unsatisfied)
455                 param_list = ", ".join(["%s (%s)" %(param_name, module) for module, param_name in unsatisfied_params])
456                 if module_list:
457                     message.append(QString(module_list))
458                 if module_list and param_list:
459                     message.append(self.tr("and by"))
460                 if param_list:
461                     message.append(QString(param_list))
462                 message_str = QStringList(message).join(" ")
463                 message_str.append(self.tr("\n\nDo you want to automatically fix these conflicts?"))
464                 qApp.restoreOverrideCursor()
465                 choice = QMessageBox.warning(self, self.tr("Dependency error"), message_str, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
466                 qApp.setOverrideCursor(Qt.WaitCursor)
467                 if choice == QMessageBox.Yes:
468                     for module in unsatisfied:
469                         modules = self.projectInfo("MODULES")
470                         modules[module]["enabled"] = False
471                     for category in range(self.pageContent.moduleTree.topLevelItemCount()):
472                         item = self.pageContent.moduleTree.topLevelItem(category)
473                         for child in range(item.childCount()):
474                             if unicode(item.child(child).text(0)) in unsatisfied:
475                                 item.child(child).setCheckState(0, Qt.Unchecked)
476                     for module, param in unsatisfied_params:
477                         configuration_file = self.projectInfo("MODULES")[module]["configuration"]
478                         configurations = self.projectInfo("CONFIGURATIONS")
479                         configurations[configuration_file][param]["value"] = "0"
480                         self.setProjectInfo("CONFIGURATIONS", configurations)
481         finally:
482             qApp.restoreOverrideCursor()
483     
484     def selectDependencyCheck(self, module):
485         """
486         Returns the list of unsatisfied dependencies after a selection.
487         """
488         unsatisfied = set()
489         modules = self.projectInfo("MODULES")
490         files = self.projectInfo("FILES")
491         configurations = self.projectInfo("CONFIGURATIONS").get(modules[module]["configuration"], {"paramlist": ()})
492         conditional_deps = ()
493         for i, param_name in configurations["paramlist"]:
494             information = configurations[param_name]
495             if information["informations"]["type"] == "boolean" and \
496                 information["value"] != "0" and \
497                 "conditional_deps" in information["informations"]:
498
499                 conditional_deps += information["informations"]["conditional_deps"]
500
501         for dependency in modules[module]["depends"] + conditional_deps:
502             if dependency in modules and not modules[dependency]["enabled"]:
503                 unsatisfied |= set([dependency])
504                 if dependency not in unsatisfied:
505                     unsatisfied |= self.selectDependencyCheck(dependency)
506             if dependency not in modules:
507                 if dependency in files:
508                     files[dependency] += 1
509                 else:
510                     files[dependency] = 1
511         self.setProjectInfo("FILES", files)
512         return unsatisfied
513     
514     def unselectDependencyCheck(self, dependency):
515         """
516         Returns the list of unsatisfied dependencies after an unselection.
517         """
518         unsatisfied = set()
519         unsatisfied_params = set()
520         modules = self.projectInfo("MODULES")
521         for module, informations in modules.items():
522             configurations = self.projectInfo("CONFIGURATIONS").get(informations["configuration"], {"paramlist": ()})
523             conditional_deps = {}
524             for i, param_name in configurations["paramlist"]:
525                 information = configurations[param_name]
526                 if information["informations"]["type"] == "boolean" and information["value"] != "0" and "conditional_deps" in information["informations"]:
527                     for dep in information["informations"]["conditional_deps"]:
528                         if not dep in conditional_deps:
529                             conditional_deps[dep] = []
530                         conditional_deps[dep].append((module, param_name))
531             if dependency in informations["depends"] and informations["enabled"]:
532                 unsatisfied |= set([module])
533                 if dependency not in unsatisfied:
534                     tmp = self.unselectDependencyCheck(module)
535                     unsatisfied |= tmp[0]
536                     unsatisfied_params |= tmp[1]
537             if dependency in conditional_deps:
538                 unsatisfied_params |= set(conditional_deps[dependency])
539         return unsatisfied, unsatisfied_params
540     
541     def removeFileDependencies(self, module):
542         """
543         Removes the files dependencies of the given module.
544         """
545         modules = self.projectInfo("MODULES")
546         files = self.projectInfo("FILES")
547         dependencies = modules[module]["depends"]
548         for dependency in dependencies:
549             if dependency in files:
550                 files[dependency] -= 1
551                 if files[dependency] == 0:
552                     del files[dependency]
553         self.setProjectInfo("FILES", files)
554
555 class QControlGroup(QObject):
556     """
557     Simple class that permit to connect different signals of different widgets
558     with a slot that emit a signal. Permits to group widget and to understand which of
559     them has sent the signal.
560     """
561     
562     def __init__(self):
563         QObject.__init__(self)
564         self._controls = {}
565     
566     def addControl(self, id, control):
567         """
568         Add a control.
569         """
570         self._controls[id] = control
571         if type(control) == QCheckBox:
572             self.connect(control, SIGNAL("stateChanged(int)"), lambda: self.stateChanged(id))
573         elif type(control) == QSpinBox:
574             self.connect(control, SIGNAL("valueChanged(int)"), lambda: self.stateChanged(id))
575         elif type(control) == QComboBox:
576             self.connect(control, SIGNAL("currentIndexChanged(int)"), lambda: self.stateChanged(id))
577         elif type(control) == QDoubleSpinBox:
578             self.connect(control, SIGNAL("valueChanged(double)"), lambda: self.stateChanged(id))
579     
580     def clear(self):
581         """
582         Remove all the controls.
583         """
584         self._controls = {}
585     
586     def stateChanged(self, id):
587         """
588         Slot called when the value of one of the stored widget changes. It emits
589         another signal.
590         """
591         self.emit(SIGNAL("stateChanged"), id)