This series of patches changes how the software selection in anaconda works so that it is completely driven by new metadata in comps.
HOW IT'S SUPPOSED TO WORK -------------------------
The metadata in comps is of the form:
<installclass> <id>gnome-desktop</id> <name>GNOME Desktop</name> <description>GNOME is a desktop.</description> <display_order>10</display_order> <grouplist> <groupid>base-x</groupid> <groupid>base</groupid> <groupid>core</groupid> <groupid>fonts</groupid> ... </grouplist> <optionlist> <groupid>sound-and-video</groupid> <groupid>office</groupid> <groupid>eclipse</groupid> <groupid>games</groupid> <groupid>design-suite</groupid> <groupid>electronic-lab</groupid> </optionlist> </installclass> <installclass> <id>minimal</id> <name>Minimal installation</name> <description>A bare-bones shell environment, useful for building your own custom system.</description> <display_order>05</display_order> <grouplist> <groupid>core</groupid> </grouplist> <optionlist> <groupid>cloud-infrastructure</groupid> <groupid>clustering</groupid> <groupid>directory-server</groupid> <groupid>dns-server</groupid> ... </optionlist> </installclass>
For any interactive installation, the user will first pick an installclass from the left column. Once they've done that, the right column will populate with the combination of: 1) any groupids from the 'optionlist' for that install class 2) any groups in comps that are marked as 'uservisible' and the user can select any of these to add onto their installation.
The second of these is for some level of backwards compatibility with existing third-party repositories.
TESTING/FUTURE WORK -------------------
Tested via qemu via a test repository with a modified comps file: (http://notting.fedorapeople.org/foorepo) This repo has no packages, so not tested for full installation, but should work as a demonstration. Full deployment of this feature would likely involve some more drastic changes to the comps file once this format is agreed upon.
This has not been wired into: - pykickstart/kickstart files
Should be doable; need to pick a reasonable character to use as a signifier. ('^'? '$'?)
- yum via commands (there is no 'yum installclass-install minimal', for example.)
CAVEATS -------
If we go this route, we would need to land, in order: - yum changes - comps changes - anaconda changes
Derived distributions would have to adopt the new metadata in comps to work with anaconda.
COMMENTS? ---------
Please, comment away.
yum changes: comps.py | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+)
anaconda changes: packaging/__init__.py | 21 +++++++++ packaging/yumpayload.py | 102 +++++++++++++++++++++++++++++++++++++++++++++- ui/gui/spokes/software.py | 93 ++++++++++++++++++++--------------------- ui/gui/spokes/software.ui | 22 +++++---- 4 files changed, 178 insertions(+), 60 deletions(-)
Install classes are a construct for use in anaconda for installing the system. They consist of: - id - name - description - display order - a list of groups that make up that install class - a list of groups that are options for that install class
Expected usage case is that anaconda will offer for you to pick an install class, and once you've done so, offer you the list of options to optionally install. --- yum/comps.py | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+)
diff --git a/yum/comps.py b/yum/comps.py index 65f6d5e..880d373 100755 --- a/yum/comps.py +++ b/yum/comps.py @@ -272,6 +272,128 @@ class Group(CompsObj): return msg
+class InstallClass(CompsObj): + """ Installation class object parsed from group data in each repo, and merged """ + + def __init__(self, elem=None): + self.name = "" + self.classid = None + self.description = "" + self.translated_name = {} + self.translated_description = {} + self.display_order = 1024 + self._groups = {} + self._options = {} + + if elem: + self.parse(elem) + + def _groupiter(self): + return self._groups.keys() + + groups = property(_groupiter) + + def _optioniter(self): + return self._options.keys() + + options = property(_optioniter) + + def parse(self, elem): + for child in elem: + if child.tag == 'id': + myid = child.text + if self.classid is not None: + raise CompsException + self.classid = myid + + elif child.tag == 'name': + text = child.text + if text: + text = text.encode('utf8') + + lang = child.attrib.get(lang_attr) + if lang: + self.translated_name[lang] = text + else: + self.name = text + + elif child.tag == 'description': + text = child.text + if text: + text = text.encode('utf8') + + lang = child.attrib.get(lang_attr) + if lang: + self.translated_description[lang] = text + else: + self.description = text + + elif child.tag == 'grouplist': + self.parse_group_list(child) + + elif child.tag == 'optionlist': + self.parse_option_list(child) + + elif child.tag == 'display_order': + self.display_order = parse_number(child.text) + + def parse_group_list(self, grouplist_elem): + for child in grouplist_elem: + if child.tag == 'groupid': + groupid = child.text + self._groups[groupid] = 1 + + def parse_option_list(self, optionlist_elem): + for child in optionlist_elem: + if child.tag == 'groupid': + optionid = child.text + self._options[optionid] = 1 + + def add(self, obj): + """Add another category object to this object""" + + for grp in obj.groups: + self._groups[grp] = 1 + + for grp in obj.options: + self._options[grp] = 1 + + # name and description translations + for lang in obj.translated_name: + if lang not in self.translated_name: + self.translated_name[lang] = obj.translated_name[lang] + + for lang in obj.translated_description: + if lang not in self.translated_description: + self.translated_description[lang] = obj.translated_description[lang] + + def xml(self): + """write out an xml stanza for the installclass object""" + msg =""" + <installclass> + <id>%s</id> + <display_order>%s</display_order>\n""" % (self.classid, self.display_order) + + msg +=""" <name>%s</name>\n""" % self.name + for (lang, val) in self.translated_name.items(): + msg += """ <name xml:lang="%s">%s</name>\n""" % (lang, val) + + msg += """ <description>%s</description>\n""" % self.description + for (lang, val) in self.translated_description.items(): + msg += """ <description xml:lang="%s">%s</description>\n""" % (lang, val) + + msg += """ <grouplist>\n""" + for grp in self.groups: + msg += """ <groupid>%s</groupid>\n""" % grp + msg += """ </grouplist>\n""" + msg += """ <optionlist>\n""" + for grp in self.options: + msg += """ <optionid>%s</optionid>\n""" % grp + msg += """ </optionlist>\n""" + msg += """ </installclass>\n""" + + return msg + class Category(CompsObj): """ Category object parsed from group data in each repo. and merged. """
@@ -376,6 +498,7 @@ class Category(CompsObj): class Comps(object): def __init__(self, overwrite_groups=False): self._groups = {} + self._installclasses = {} self._categories = {} self.compscount = 0 self.overwrite_groups = overwrite_groups @@ -388,12 +511,18 @@ class Comps(object): grps.sort(key=lambda x: (x.display_order, x.name)) return grps
+ def get_installclasses(self): + classes = self._installclasses.values() + classes.sort(key=lambda x: (x.display_order, x.name)) + return classes + def get_categories(self): cats = self._categories.values() cats.sort(key=lambda x: (x.display_order, x.name)) return cats
groups = property(get_groups) + installclasses = property(get_installclasses) categories = property(get_categories)
def has_group(self, grpid): @@ -447,6 +576,57 @@ class Comps(object):
return returns.values()
+ def has_installclass(self, classid): + exists = self.return_installclasses(classid) + + if exists: + return True + + return False + + def return_installclass(self, classid): + """Return the first group which matches""" + classes = self.return_installclasses(classid) + if classes: + return classes[0] + + return None + + def return_installclasses(self, class_pattern, case_sensitive=False): + """return all installclasses which match either by glob or exact match""" + returns = {} + + for item in class_pattern.split(','): + item = item.strip() + if item in self._installclasses: + thisclass = self._installclasses[item] + returns[thisclass.classid] = thisclass + continue + + if case_sensitive: + match = re.compile(fnmatch.translate(item)).match + else: + match = re.compile(fnmatch.translate(item), flags=re.I).match + + done = False + for aclass in self.installclasses: + for name in aclass.name, aclass.classid, aclass.ui_name: + if match(name): + done = True + returns[aclass.classid] = aclass + break + if done: + continue + + # If we didn't match to anything in the current locale, try others + for aclass in self.installclasses: + for name in aclass.translated_name.values(): + if match(name): + returns[aclass.classid] = aclass + break + + return returns.values() + # This is close to returnPackages() etc. API ... need to std. these names # the above return_groups uses different, but equal, API. def return_categories(self, pattern, ignore_case=True): @@ -490,6 +670,13 @@ class Comps(object): else: self._groups[group.groupid] = group
+ def add_installclass(self, installclass): + if installclass.classid in self._installclasses: + thatclass = self._installclasses[installclass.classid] + thatclass.add(installclass) + else: + self._installclasses[installclass.classid] = installclass + def add_category(self, category): if category.categoryid in self._categories: thatcat = self._categories[category.categoryid] @@ -520,6 +707,9 @@ class Comps(object): if elem.tag == "group": group = Group(elem) self.add_group(group) + if elem.tag == "installclass": + installclass = InstallClass(elem) + self.add_installclass(installclass) if elem.tag == "category": category = Category(elem) self.add_category(category) @@ -595,6 +785,13 @@ def main(): for pkg in group.packages: print ' ' + pkg
+ for installclass in p.installclasses: + print installclass.name + for group in installclass.groups: + print ' ' + group + for group in installclass.options: + print ' *' + group + for category in p.categories: print category.name for group in category.groups:
Other things may have descriptions later. --- pyanaconda/packaging/__init__.py | 2 +- pyanaconda/packaging/yumpayload.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py index ca23f53..b93b88b 100644 --- a/pyanaconda/packaging/__init__.py +++ b/pyanaconda/packaging/__init__.py @@ -236,7 +236,7 @@ class Payload(object): def groups(self): raise NotImplementedError()
- def description(self, groupid): + def groupDescription(self, groupid): raise NotImplementedError()
def groupSelected(self, groupid): diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py index 8aea2fb..2cecb9b 100644 --- a/pyanaconda/packaging/yumpayload.py +++ b/pyanaconda/packaging/yumpayload.py @@ -737,7 +737,7 @@ reposdir=%s
return groups
- def description(self, groupid): + def groupDescription(self, groupid): """ Return name/description tuple for the group specified by id. """ groups = self._yumGroups if not groups:
--- pyanaconda/ui/gui/spokes/software.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/software.ui b/pyanaconda/ui/gui/spokes/software.ui index 20bbaa3..cd8ba9f 100644 --- a/pyanaconda/ui/gui/spokes/software.ui +++ b/pyanaconda/ui/gui/spokes/software.ui @@ -70,7 +70,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property> - <property name="label" translatable="yes">DESKTOP</property> + <property name="label" translatable="yes">Choose your installation type</property> <attributes> <attribute name="weight" value="bold"/> </attributes> @@ -152,7 +152,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property> - <property name="label" translatable="yes">ADD-ONS</property> + <property name="label" translatable="yes">Choose your add-ons</property> <attributes> <attribute name="weight" value="bold"/> </attributes>
diff --git a/pyanaconda/ui/gui/spokes/software.ui b/pyanaconda/ui/gui/spokes/software.ui index 20bbaa3..cd8ba9f 100644 --- a/pyanaconda/ui/gui/spokes/software.ui +++ b/pyanaconda/ui/gui/spokes/software.ui @@ -70,7 +70,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property>
<property name="label" translatable="yes">DESKTOP</property>
<property name="label" translatable="yes">Choose your installation type</property> <attributes> <attribute name="weight" value="bold"/> </attributes>
I don't really like "installation type" here. Do you see the left hand side containing any non-desktops besides minimal? Because the wording you've chosen here makes it look like that's what you're setting up for, whereas our design discussions were pretty well centered around the concept of choosing a desktop environment.
- Chris
Chris Lumens (clumens@redhat.com) said:
diff --git a/pyanaconda/ui/gui/spokes/software.ui b/pyanaconda/ui/gui/spokes/software.ui index 20bbaa3..cd8ba9f 100644 --- a/pyanaconda/ui/gui/spokes/software.ui +++ b/pyanaconda/ui/gui/spokes/software.ui @@ -70,7 +70,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property>
<property name="label" translatable="yes">DESKTOP</property>
<property name="label" translatable="yes">Choose your installation type</property> <attributes> <attribute name="weight" value="bold"/> </attributes>I don't really like "installation type" here. Do you see the left hand side containing any non-desktops besides minimal? Because the wording you've chosen here makes it look like that's what you're setting up for, whereas our design discussions were pretty well centered around the concept of choosing a desktop environment.
Yes, especially for a server class installation. Basically, by hardcoding 'desktop' here we're making implied assumptions about the product anaconda is installing - I want to avoid any of these in anaconda itself.
Bill
--- pyanaconda/packaging/__init__.py | 19 ++++++++ pyanaconda/packaging/yumpayload.py | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+)
diff --git a/pyanaconda/packaging/__init__.py b/pyanaconda/packaging/__init__.py index b93b88b..b62c541 100644 --- a/pyanaconda/packaging/__init__.py +++ b/pyanaconda/packaging/__init__.py @@ -230,6 +230,25 @@ class Payload(object): raise NotImplementedError()
### + ### METHODS FOR WORKING WITH INSTALLCLASSES + ### + @property + def installclasses(self): + raise NotImplementedError() + + def classSelected(self, classid): + raise NotImplementedError() + + def classDescription(self, classid): + raise NotImplementedError() + + def selectClass(self, classid): + raise NotImplementedError() + + def deselectClass(self, classid): + raise NotImplementedError() + + ### ### METHODS FOR WORKING WITH GROUPS ### @property diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py index 2cecb9b..3bd85e8 100644 --- a/pyanaconda/packaging/yumpayload.py +++ b/pyanaconda/packaging/yumpayload.py @@ -707,6 +707,94 @@ reposdir=%s self._packages = []
### + ### METHODS FOR WORKING WITH INSTALLCLASSES + ### + @property + def installclasses(self): + """ List of installclass ids. """ + from yum.Errors import RepoError + from yum.Errors import GroupsError + + installclasses = [] + yum_groups = self._yumGroups + if yum_groups: + with _yum_lock: + installclasses = [i.classid for i in yum_groups.get_installclasses()] + + return installclasses + + def classSelected(self, classid): + groups = self._yumGroups + if not groups: + return False + + with _yum_lock: + if not groups.has_installclass(classid): + raise NoSuchGroup(classid) + + installclass = groups.return_installclass(classid) + for group in installclass.groups: + if not self.groupSelected(group): + return False + return True + + def classHasOption(self, classid, grpid): + groups = self._yumGroups + if not groups: + return False + + with _yum_lock: + if not groups.has_installclass(classid): + raise NoSuchGroup(classid) + + installclass = groups.return_installclass(classid) + if grpid in installclass.options: + return True + return False + + def classDescription(self, classid): + """ Return name/description tuple for the installclass specified by id. """ + groups = self._yumGroups + if not groups: + return (classid, classid) + + with _yum_lock: + if not groups.has_installclass(classid): + raise NoSuchGroup(classid) + + installclass = groups.return_installclass(classid) + + return (installclass.ui_name, installclass.ui_description) + + def selectClass(self, classid): + groups = self._yumGroups + if not groups: + return + + with _yum_lock: + if not groups.has_installclass(classid): + raise NoSuchGroup(classid) + + installclass = groups.return_installclass(classid) + for group in installclass.groups: + self.selectGroup(group) + + def deselectClass(self, classid): + groups = self._yumGroups + if not groups: + return + + with _yum_lock: + if not groups.has_installclass(classid): + raise NoSuchGroup(classid) + + installclass = groups.return_installclass(classid) + for group in installclass.groups: + self.deselectGroup(group) + for group in installclass.options: + self.deselectGroup(group) + + ### ### METHODS FOR WORKING WITH GROUPS ### @property
--- pyanaconda/packaging/yumpayload.py | 12 ++++++++++++ 1 file changed, 12 insertions(+)
diff --git a/pyanaconda/packaging/yumpayload.py b/pyanaconda/packaging/yumpayload.py index 3bd85e8..7d8eef4 100644 --- a/pyanaconda/packaging/yumpayload.py +++ b/pyanaconda/packaging/yumpayload.py @@ -839,6 +839,18 @@ reposdir=%s
return (group.ui_name, group.ui_description)
+ def _isGroupVisible(self, groupid): + groups = self._yumGroups + if not groups: + return False + + with _yum_lock: + if not groups.has_group(groupid): + return False + + group = groups.return_group(groupid) + return group.user_visible + def _selectYumGroup(self, groupid, default=True, optional=False): # select the group in comps pkg_types = ['mandatory']
- s/desktop/installClass/ - use the list of options for the install class, plus any user visible groups, to populate the add-on list once an install class is selected --- pyanaconda/ui/gui/spokes/software.py | 88 ++++++++++++++++------------------ pyanaconda/ui/gui/spokes/software.ui | 18 +++---- 2 files changed, 50 insertions(+), 56 deletions(-)
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py index 9e56a4b..d002c40 100644 --- a/pyanaconda/ui/gui/spokes/software.py +++ b/pyanaconda/ui/gui/spokes/software.py @@ -35,7 +35,7 @@ from .source import AdditionalReposDialog __all__ = ["SoftwareSelectionSpoke"]
class SoftwareSelectionSpoke(NormalSpoke): - builderObjects = ["addonStore", "desktopStore", "softwareWindow"] + builderObjects = ["addonStore", "classStore", "softwareWindow"] mainWidgetName = "softwareWindow" uiFile = "spokes/software.ui"
@@ -51,7 +51,7 @@ class SoftwareSelectionSpoke(NormalSpoke):
self.selectedGroups = [] self.excludedGroups = [] - self.desktop = None + self.installclass = None
self._addRepoDialog = AdditionalReposDialog(self.data)
@@ -61,23 +61,16 @@ class SoftwareSelectionSpoke(NormalSpoke): # part of its operation. So this is fine. from pyanaconda.threads import threadMgr, AnacondaThread
- row = self._get_selected_desktop() + row = self._get_selected_installclass() if not row: return
self.payload.data.packages.groupList = [] - self.payload.selectGroup(row[2]) + self.payload.selectClass(row[2]) + self.installclass = row[2] for group in self.selectedGroups: self.payload.selectGroup(group)
- # select some stuff people will want with their desktop - # XXX this is only a placeholder until the new group metadata is in - # place - if row[2] != "base": - groups = ['base-x', 'fonts'] - for group in [g for g in groups if g not in self.excludedGroups]: - self.payload.selectGroup(group) - communication.send_not_ready(self.__class__.__name__) threadMgr.add(AnacondaThread(name="AnaCheckSoftwareThread", target=self.checkSoftwareSelection)) @@ -111,7 +104,7 @@ class SoftwareSelectionSpoke(NormalSpoke): if flags.automatedInstall: return packagesSeen and processingDone else: - return self._get_selected_desktop() is not None and processingDone + return self._get_selected_installclass() is not None and processingDone
@property def ready(self): @@ -136,7 +129,7 @@ class SoftwareSelectionSpoke(NormalSpoke): if threadMgr.get("AnaPayloadMDThread") or self.payload.baseRepo is None: return _("Installation source not set up")
- row = self._get_selected_desktop() + row = self._get_selected_installclass() if not row: # Kickstart installs with %packages will have a row selected, unless # they did an install without a desktop environment. This should @@ -146,7 +139,7 @@ class SoftwareSelectionSpoke(NormalSpoke):
return _("Nothing selected")
- return self.payload.description(row[2])[0] + return self.payload.classDescription(row[2])[0]
def initialize(self): from pyanaconda.threads import threadMgr, AnacondaThread @@ -194,52 +187,51 @@ class SoftwareSelectionSpoke(NormalSpoke): if mdGatherThread: mdGatherThread.join()
- self._desktopStore = self.builder.get_object("desktopStore") - self._desktopStore.clear() + self._classStore = self.builder.get_object("classStore") + self._classStore.clear() + + clasess = [] + for installclass in self.payload.installclasses: + (name, desc) = self.payload.classDescription(installclass) + + itr = self._classStore.append([installclass == self.installclass, "<b>%s</b>\n%s" % (name, desc), installclass]) + if installclass == self.installclass: + sel = self.builder.get_object("installclassSelector") + sel.select_iter(itr) + self.refreshAddons() + + def refreshAddons(self): self._addonStore = self.builder.get_object("addonStore") self._addonStore.clear() + if self.installclass: + for grp in self.payload.groups: + if self.payload.classHasOption(self.installclass, grp) or self.payload._isGroupVisible(grp): + (name, desc) = self.payload.groupDescription(grp) + selected = self.payload.groupSelected(grp) + + self._addonStore.append([selected, "<b>%s</b>\n%s" % (name, desc), grp])
- desktops = [] - for grp in self.payload.groups: - # Throw out language support groups and critical-path stuff. - if grp.endswith("-support") or grp.startswith("critical-path-"): - continue - # Throw out core, which should always be selected. - elif grp == "core": - continue - elif grp == "base" or grp.endswith("-desktop"): - (name, desc) = self.payload.description(grp) - selected = self.payload.groupSelected(grp) - - itr = self._desktopStore.append([selected, "<b>%s</b>\n%s" % (name, desc), grp]) - if selected: - sel = self.builder.get_object("desktopSelector") - sel.select_iter(itr) - self.desktop = grp - - desktops.append(grp) - else: - (name, desc) = self.payload.description(grp) - selected = self.payload.groupSelected(grp) - - self._addonStore.append([selected, "<b>%s</b>\n%s" % (name, desc), grp]) - - self.selectedGroups = [g.name for g in self.data.packages.groupList - if g.name not in desktops] + self.selectedGroups = [g.name for g in self.data.packages.groupList] self.excludedGroups = [g.name for g in self.data.packages.excludedGroupList]
# Returns the row in the store corresponding to what's selected on the # left hand panel, or None if nothing's selected. - def _get_selected_desktop(self): - desktopView = self.builder.get_object("desktopView") - (store, itr) = desktopView.get_selection().get_selected() + def _get_selected_installclass(self): + installclassView = self.builder.get_object("installclassView") + (store, itr) = installclassView.get_selection().get_selected() if not itr: return None
- return self._desktopStore[itr] + return self._classStore[itr]
# Signal handlers + def on_installclass_chosen(self, blah): + row = self._get_selected_installclass() + if row: + self.installclass = row[2] + self.refreshAddons() + def on_row_toggled(self, renderer, path): selected = not self._addonStore[path][0] group = self._addonStore[path][2] diff --git a/pyanaconda/ui/gui/spokes/software.ui b/pyanaconda/ui/gui/spokes/software.ui index cd8ba9f..ecfd996 100644 --- a/pyanaconda/ui/gui/spokes/software.ui +++ b/pyanaconda/ui/gui/spokes/software.ui @@ -12,7 +12,7 @@ <column type="gchararray"/> </columns> </object> - <object class="GtkListStore" id="desktopStore"> + <object class="GtkListStore" id="classStore"> <columns> <!-- column-name repoSelected --> <column type="gboolean"/> @@ -60,7 +60,7 @@ <property name="spacing">24</property> <property name="homogeneous">True</property> <child> - <object class="GtkBox" id="desktopBox"> + <object class="GtkBox" id="installclassBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> @@ -82,7 +82,7 @@ </packing> </child> <child> - <object class="GtkScrolledWindow" id="desktopScrolledWindow"> + <object class="GtkScrolledWindow" id="installclassScrolledWindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">never</property> @@ -93,25 +93,27 @@ <property name="visible">True</property> <property name="can_focus">False</property> <child> - <object class="GtkTreeView" id="desktopView"> + <object class="GtkTreeView" id="installclassView"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="margin_left">6</property> <property name="margin_right">6</property> <property name="margin_top">6</property> <property name="margin_bottom">6</property> - <property name="model">desktopStore</property> + <property name="model">classStore</property> <property name="headers_visible">False</property> <property name="headers_clickable">False</property> <property name="search_column">0</property> <child internal-child="selection"> - <object class="GtkTreeSelection" id="desktopSelector"/> + <object class="GtkTreeSelection" id="installclassSelector"> + <signal name="changed" handler="on_installclass_chosen" swapped="no"/> + </object> </child> <child> - <object class="GtkTreeViewColumn" id="desktopDescCol"> + <object class="GtkTreeViewColumn" id="installclassDescCol"> <property name="title" translatable="yes">column</property> <child> - <object class="GtkCellRendererText" id="desktopDescRenderer"> + <object class="GtkCellRendererText" id="installclassDescRenderer"> <property name="width">250</property> <property name="yalign">0</property> <property name="wrap_mode">word-char</property>
--- pyanaconda/ui/gui/spokes/software.py | 5 +++++ 1 file changed, 5 insertions(+)
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py index d002c40..730f861 100644 --- a/pyanaconda/ui/gui/spokes/software.py +++ b/pyanaconda/ui/gui/spokes/software.py @@ -48,6 +48,7 @@ class SoftwareSelectionSpoke(NormalSpoke): NormalSpoke.__init__(self, *args, **kwargs) self._error = False self._tx_id = None + self._selectFlag = False
self.selectedGroups = [] self.excludedGroups = [] @@ -65,6 +66,7 @@ class SoftwareSelectionSpoke(NormalSpoke): if not row: return
+ self._selectFlag = False self.payload.data.packages.groupList = [] self.payload.selectClass(row[2]) self.installclass = row[2] @@ -214,6 +216,7 @@ class SoftwareSelectionSpoke(NormalSpoke): self.selectedGroups = [g.name for g in self.data.packages.groupList] self.excludedGroups = [g.name for g in self.data.packages.excludedGroupList] + self._selectFlag = True
# Returns the row in the store corresponding to what's selected on the # left hand panel, or None if nothing's selected. @@ -227,6 +230,8 @@ class SoftwareSelectionSpoke(NormalSpoke):
# Signal handlers def on_installclass_chosen(self, blah): + if not self._selectFlag: + return row = self._get_selected_installclass() if row: self.installclass = row[2]
I know you were posting this hoping to hear about all sorts of problems. Sorry, I think it looks pretty good to me.
My primary concern is a stupid one - do we really want to use "installclass"? That already has a vague enough meaning in anaconda as it is.
For any interactive installation, the user will first pick an installclass from the left column. Once they've done that, the right column will populate with the combination of:
- any groupids from the 'optionlist' for that install class
- any groups in comps that are marked as 'uservisible'
and the user can select any of these to add onto their installation.
The second of these is for some level of backwards compatibility with existing third-party repositories.
It's always hard to tell certain things from looking at UI patches. What does the right side start out as when you first pull up this spoke?
Also, it sounds like someone's going to have to go through comps and mark everything as uservisible=False that would appear as an option of some class, correct?
This has not been wired into:
- pykickstart/kickstart files
Should be doable; need to pick a reasonable character to use as a signifier. ('^'? '$'?)
I don't have any sense that one character is more meaningful than any other. We can just pick something. Wiring it into pykickstart should be easy, or I've not done my job.
- Chris
Chris Lumens (clumens@redhat.com) said:
I know you were posting this hoping to hear about all sorts of problems. Sorry, I think it looks pretty good to me.
My primary concern is a stupid one - do we really want to use "installclass"? That already has a vague enough meaning in anaconda as it is.
Yeah, it's just a word that I had around. "Base package set" is the concept, but that's awfully long to use as the name of it. "Choice"? "Basis"?
For any interactive installation, the user will first pick an installclass from the left column. Once they've done that, the right column will populate with the combination of:
- any groupids from the 'optionlist' for that install class
- any groups in comps that are marked as 'uservisible'
and the user can select any of these to add onto their installation.
The second of these is for some level of backwards compatibility with existing third-party repositories.
It's always hard to tell certain things from looking at UI patches. What does the right side start out as when you first pull up this spoke?
Empty.
Also, it sounds like someone's going to have to go through comps and mark everything as uservisible=False that would appear as an option of some class, correct?
Yes, once we get the format nailed down, more drastic comps reorgs can land.
This has not been wired into:
- pykickstart/kickstart files
Should be doable; need to pick a reasonable character to use as a signifier. ('^'? '$'?)
I don't have any sense that one character is more meaningful than any other. We can just pick something. Wiring it into pykickstart should be easy, or I've not done my job.
On the yum lists, James suggested just re-using '@' and assuming that there would never be groups and classes with the same name - that may be impractical.
Bill
anaconda-patches@lists.fedorahosted.org