Index: /trunk/src/VBox/ValidationKit/testmanager/core/base.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/base.py	(revision 65090)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/base.py	(revision 65091)
@@ -1164,4 +1164,5 @@
         self.sHover      = sHover;      ##< Optional hover/title string.
         self.fIrrelevant = fIrrelevant; ##< Irrelevant filter option, only present because it's selected
+        self.aoSubs      = [];          ##< References to FilterCriterion.oSub.aoPossible.
 
 
@@ -1190,5 +1191,5 @@
 
     def __init__(self, sName, sVarNm = None, sType = ksType_UInt, sState = ksState_NotSelected, sKind = ksKind_AnyOf,
-                 sTable = None, sColumn = None):
+                 sTable = None, sColumn = None, oSub = None):
         assert len(sVarNm) in (2,3); # required by wuimain.py
         self.sName      = sName;
@@ -1197,9 +1198,10 @@
         self.sType      = sType;
         self.sKind      = sKind;
-        self.aoSelected = []; # Single value, any type.
-        self.aoPossible = []; # type: list[FilterCriterionValueAndDescription]
+        self.aoSelected = [];       ##< Single value, any type.
+        self.aoPossible = [];       ##< type: list[FilterCriterionValueAndDescription]
         self.sTable     = sTable;
         self.sColumn    = sColumn;
-        self.fExpanded  = None; ##< Tristate (None, False, True) indicating whether the criterion should be shown/hid regardless.
+        self.fExpanded  = None;     ##< Tristate (None, False, True)
+        self.oSub       = oSub;     ##< type: FilterCriterion
 
 
@@ -1232,4 +1234,29 @@
         self.aCriteria = []; # type: list[FilterCriterion]
 
+    def _initFromParamsWorker(self, oDisp, oCriterion):
+        """ Worker for initFromParams. """
+        if oCriterion.sType == FilterCriterion.ksType_UInt:
+            oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []);
+        elif oCriterion.sType == FilterCriterion.ksType_String:
+            oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
+            if len(oCriterion.aoSelected) > 100:
+                raise TMExceptionBase('Variable %s has %u value, max allowed is 100!'
+                                      % (oCriterion.sVarNm, len(oCriterion.aoSelected)));
+            for sValue in oCriterion.aoSelected:
+                if   len(sValue) > 64 \
+                  or '\'' in sValue \
+                  or sValue[-1] == '\\':
+                    raise TMExceptionBase('Variable %s has an illegal value "%s"!' % (oCriterion.sVarNm, sValue));
+        else:
+            assert False;
+        if len(oCriterion.aoSelected) > 0:
+            oCriterion.sState     = FilterCriterion.ksState_Selected;
+        else:
+            oCriterion.sState     = FilterCriterion.ksState_NotSelected;
+
+        if oCriterion.oSub is not None:
+            self._initFromParamsWorker(oDisp, oCriterion.oSub);
+        return;
+
     def initFromParams(self, oDisp): # type: (WuiDispatcherBase) -> self
         """
@@ -1240,22 +1267,5 @@
 
         for oCriterion in self.aCriteria:
-            if oCriterion.sType == FilterCriterion.ksType_UInt:
-                oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []);
-            elif oCriterion.sType == FilterCriterion.ksType_String:
-                oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
-                if len(oCriterion.aoSelected) > 100:
-                    raise TMExceptionBase('Variable %s has %u value, max allowed is 100!'
-                                          % (oCriterion.sVarNm, len(oCriterion.aoSelected)));
-                for sValue in oCriterion.aoSelected:
-                    if   len(sValue) > 64 \
-                      or '\'' in sValue \
-                      or sValue[-1] == '\\':
-                        raise TMExceptionBase('Variable %s has an illegal value "%s"!' % (oCriterion.sVarNm, sValue));
-            else:
-                assert False;
-            if len(oCriterion.aoSelected) > 0:
-                oCriterion.sState     = FilterCriterion.ksState_Selected;
-            else:
-                oCriterion.sState     = FilterCriterion.ksState_NotSelected;
+            self._initFromParamsWorker(oDisp, oCriterion);
         return self;
 
Index: /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65090)
+++ /trunk/src/VBox/ValidationKit/testmanager/core/testresults.py	(revision 65091)
@@ -660,7 +660,6 @@
     kiMisc                  = 12;
     kiOses                  = 13;
-    kiOsVersions            = 14;
-    kiPythonVersions        = 15;
-    kiFailReasons           = 16;
+    kiPythonVersions        = 14;
+    kiFailReasons           = 15;
 
     kiMisc_NestedPaging     =  0;
@@ -743,11 +742,9 @@
         assert self.aCriteria[self.kiMisc] is oCrit;
 
-        oCrit = FilterCriterion('OSes', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs');
+        oCrit = FilterCriterion('OS / Version', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs',
+                                oSub = FilterCriterion('OS Versions', sVarNm = 'ov',
+                                                       sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion'));
         self.aCriteria.append(oCrit);
         assert self.aCriteria[self.kiOses] is oCrit;
-
-        oCrit = FilterCriterion('OS Versions', sVarNm = 'ov', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion');
-        self.aCriteria.append(oCrit);
-        assert self.aCriteria[self.kiOsVersions] is oCrit;
 
         oCrit = FilterCriterion('Python', sVarNm = 'py', sTable = 'TestBoxesWithStrings', sColumn = 'iPythonHexVersion');
@@ -772,4 +769,25 @@
     };
 
+    def _getWhereWorker(self, iCrit, oCrit, sExtraIndent, iOmit):
+        """ Formats one - main or sub. """
+        sQuery = '';
+        if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit:
+            if iCrit == self.kiMisc:
+                for iValue in oCrit.aoSelected:
+                    if iValue in self.kdMiscConditions:
+                        sQuery += '%s   AND %s\n' % (sExtraIndent, self.kdMiscConditions[iValue],);
+            else:
+                if iCrit == self.kiMemory:
+                    sQuery += '%s   AND (%s.%s / 1024) IN (' % (sExtraIndent, oCrit.sTable, oCrit.sColumn,);
+                else:
+                    sQuery += '%s   AND %s.%s IN (' % (sExtraIndent, oCrit.sTable, oCrit.sColumn,);
+                if oCrit.sType == FilterCriterion.ksType_String:
+                    sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')\n';
+                else:
+                    sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected) + ')\n';
+            if oCrit.oSub is not None:
+                sQuery += self._getWhereWorker(iCrit | (((iCrit >> 8) + 1) << 8), oCrit.oSub, sExtraIndent, iOmit);
+        return sQuery;
+
     def getWhereConditions(self, sExtraIndent = '', iOmit = -1):
         """
@@ -779,18 +797,5 @@
         sQuery = '';
         for iCrit, oCrit in enumerate(self.aCriteria):
-            if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit:
-                if iCrit == self.kiMisc:
-                    for iValue in oCrit.aoSelected:
-                        if iValue in self.kdMiscConditions:
-                            sQuery += '%s   AND %s\n' % (sExtraIndent, self.kdMiscConditions[iValue],);
-                else:
-                    if iCrit == self.kiMemory:
-                        sQuery += '%s   AND (%s.%s / 1024) IN (' % (sExtraIndent, oCrit.sTable, oCrit.sColumn,);
-                    else:
-                        sQuery += '%s   AND %s.%s IN (' % (sExtraIndent, oCrit.sTable, oCrit.sColumn,);
-                    if oCrit.sType == FilterCriterion.ksType_String:
-                        sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')\n';
-                    else:
-                        sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected) + ')\n';
+            sQuery += self._getWhereWorker(iCrit, oCrit, sExtraIndent, iOmit);
         return sQuery;
 
@@ -1554,4 +1559,28 @@
                                                                                        fIrrelevant = True));
 
+        def workerDoFetchNested():
+            """ Does the tedious result fetching and handling of missing bits. """
+            oCrit.aoPossible = [];
+            oCrit.oSub.aoPossible = [];
+            dLeft    = { oValue: 1 for oValue in oCrit.aoSelected };
+            dSubLeft = { oValue: 1 for oValue in oCrit.oSub.aoSelected };
+            oMain    = None;
+            for aoRow in self._oDb.fetchAll():
+                if oMain is None or oMain.oValue != aoRow[0]:
+                    oMain = FilterCriterionValueAndDescription(aoRow[0], aoRow[1], 0);
+                    oCrit.aoPossible.append(oMain);
+                    if aoRow[0] in dLeft:
+                        del dLeft[aoRow[0]];
+                oCurSub = FilterCriterionValueAndDescription(aoRow[2], aoRow[3], aoRow[4]);
+                oCrit.oSub.aoPossible.append(oCurSub);
+                if aoRow[2] in dLeft:
+                    del dSubLeft[aoRow[2]];
+
+                oMain.aoSubs.append(oCurSub);
+                oMain.cTimes += aoRow[4];
+
+            if len(dLeft) > 0:
+                pass; ## @todo
+
         # Statuses.
         oCrit = oFilter.aCriteria[TestResultFilter.kiTestStatus];
@@ -1606,8 +1635,10 @@
         workerDoFetch(TestBoxLogic);
 
-        # Testbox OSes.
+        # Testbox OSes and versions.
         oCrit = oFilter.aCriteria[TestResultFilter.kiOses];
         self._oDb.execute('SELECT TestBoxesWithStrings.idStrOs,\n'
                           '       TestBoxesWithStrings.sOs,\n'
+                          '       TestBoxesWithStrings.idStrOsVersion,\n'
+                          '       TestBoxesWithStrings.sOsVersion,\n'
                           '       SUM(TestBoxGenIDs.cTimes)\n'
                           'FROM   ( SELECT TestSets.idGenTestBox,\n'
@@ -1622,27 +1653,12 @@
                           '       LEFT OUTER JOIN TestBoxesWithStrings\n'
                           '                    ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
-                          'GROUP BY TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n'
-                          'ORDER BY TestBoxesWithStrings.sOs\n' );
-        workerDoFetch(None, fIdIsName = True);
-
-        # Testbox OS versions .
-        oCrit = oFilter.aCriteria[TestResultFilter.kiOsVersions];
-        self._oDb.execute('SELECT TestBoxesWithStrings.idStrOsVersion,\n'
-                          '       TestBoxesWithStrings.sOsVersion,\n'
-                          '       SUM(TestBoxGenIDs.cTimes)\n'
-                          'FROM   ( SELECT TestSets.idGenTestBox     AS idGenTestBox,\n'
-                          '                COUNT(TestSets.idTestSet) AS cTimes\n'
-                          '         FROM   TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOsVersions) +
-                          ''.join('                , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) +
-                          '         WHERE  ' + self._getTimePeriodQueryPart(tsNow, sPeriod, '        ') +
-                          oFilter.getWhereConditions(iOmit = TestResultFilter.kiOsVersions) +
-                          oReportModel.getExtraSubjectWhereExpr() +
-                          '         GROUP BY TestSets.idGenTestBox\n'
-                          '       ) AS TestBoxGenIDs\n'
-                          '       LEFT OUTER JOIN TestBoxesWithStrings\n'
-                          '                    ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
-                          'GROUP BY TestBoxesWithStrings.idStrOsVersion, TestBoxesWithStrings.sOsVersion\n'
-                          'ORDER BY TestBoxesWithStrings.sOsVersion\n' );
-        workerDoFetch(None, fIdIsName = True);
+                          'GROUP BY TestBoxesWithStrings.idStrOs,\n'
+                          '         TestBoxesWithStrings.sOs,\n'
+                          '         TestBoxesWithStrings.idStrOsVersion,\n'
+                          '         TestBoxesWithStrings.sOsVersion\n'
+                          'ORDER BY TestBoxesWithStrings.sOs,\n'
+                          '         TestBoxesWithStrings.sOsVersion\n'
+                           );
+        workerDoFetchNested();
 
         # Testbox CPU/OS architectures.
@@ -1770,5 +1786,5 @@
         workerDoFetch(None, fIdIsName = True);
         for oCur in oCrit.aoPossible:
-            oCur.sDesc = TestBoxData.formatPythonVersionEx(oCur.oValue);
+            oCur.sDesc = TestBoxData.formatPythonVersionEx(oCur.oValue); # pylint: disable=redefined-variable-type
 
         # Testcases (see getTestCases).
Index: /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65090)
+++ /trunk/src/VBox/ValidationKit/testmanager/htdocs/css/common.css	(revision 65091)
@@ -324,5 +324,4 @@
 }
 
-
 /* Filters: */
 #side-filters p:first-child {
@@ -333,5 +332,5 @@
 }
 
-#side-filters dd.sf-collapsable {
+#side-filters dd.sf-collapsible {
     display:        block;
 }
@@ -378,7 +377,25 @@
 }
 
-#side-filters ul {
+#side-filters dd > ul {
     max-height:     22em;
     overflow:       auto;
+}
+
+#side-filters ul ul {
+    margin-left:    1.4em;
+}
+
+#side-filters li {
+    padding-top:    1px;
+    padding-bottom: 1px;
+    overflow-wrap:  break-word;
+}
+
+ul.sf-checkbox-collapsible {
+    display:        block;
+}
+
+ul.sf-checkbox-expandable {
+    display:        none;
 }
 
@@ -407,7 +424,9 @@
     margin-right:   3px;
     margin-bottom:  0.5em;
+    font-family:    Times New, Times, serif;
     font-size:      0.86em;
+    font-style:     normal;
+    font-weight:    normal;
     line-height:    1.2em;
-    font-style:     normal;
     text-align:     center;
 }
Index: /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 65090)
+++ /trunk/src/VBox/ValidationKit/testmanager/htdocs/js/common.js	(revision 65091)
@@ -353,5 +353,5 @@
 
 
-/** @name Collapsable / Expandable items
+/** @name Collapsible / Expandable items
  * @{
  */
@@ -359,10 +359,10 @@
 
 /**
- * Toggles the collapsable / expandable state of a parent DD and DT unclke.
+ * Toggles the collapsible / expandable state of a parent DD and DT uncle.
  *
  * @returns true
  * @param   oAnchor             The anchor object.
  */
-function toggleCollapsableDtDd(oAnchor)
+function toggleCollapsibleDtDd(oAnchor)
 {
     var oParent = oAnchor.parentElement;
@@ -377,5 +377,5 @@
     var sNewClass;
     var sNewChar;
-    if (     sClass.substr(-11) == 'collapsable')
+    if (     sClass.substr(-11) == 'collapsible')
     {
         sNewClass = sClass.substr(0, sClass.length - 11) + 'expandable';
@@ -384,10 +384,10 @@
     else if (sClass.substr(-10) == 'expandable')
     {
-        sNewClass = sClass.substr(0, sClass.length - 10) + 'collapsable';
+        sNewClass = sClass.substr(0, sClass.length - 10) + 'collapsible';
         sNewChar  = '\u25BC'; /* black down-pointing triangle */
     }
     else
     {
-        console.log('toggleCollapsableParent: Invalid class: ' + sClass);
+        console.log('toggleCollapsibleParent: Invalid class: ' + sClass);
         return true;
     }
@@ -400,4 +400,29 @@
     if (oDdElement)
         oDdElement.className = sNewClass;
+    return true;
+}
+
+/**
+ * Shows/hides a sub-category UL according to checkbox status.
+ *
+ * The checkbox is expected to be within a label element or something.
+ *
+ * @returns true
+ * @param   oInput          The input checkbox.
+ */
+function toggleCollapsibleCheckbox(oInput)
+{
+    var oParent = oInput.parentElement;
+
+    /* Find the UL sibling element. */
+    var oUlElement = oParent.nextSibling;
+    while (oUlElement != null && oUlElement.tagName != 'UL')
+        oUlElement = oUlElement.nextSibling;
+
+    /* Change the visibility. */
+    if (oInput.checked)
+        oUlElement.className = oUlElement.className.replace('expandable', 'collapsible');
+    else
+        oUlElement.className = oUlElement.className.replace('collapsible', 'expandable');
     return true;
 }
Index: /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
===================================================================
--- /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65090)
+++ /trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py	(revision 65091)
@@ -933,8 +933,13 @@
         for oCrit in oFilter.aCriteria:
             if len(oCrit.aoPossible) > 0:
-                if   oCrit.sState == oCrit.ksState_Selected \
-                  or len(oCrit.aoPossible) <= 2 \
+                if   (    oCrit.oSub is None \
+                      and (   oCrit.sState == oCrit.ksState_Selected \
+                           or len(oCrit.aoPossible) <= 2)) \
+                  or (    oCrit.oSub is not None \
+                      and (   oCrit.sState == oCrit.ksState_Selected \
+                           or oCrit.oSub.sState == oCrit.ksState_Selected \
+                           or (len(oCrit.aoPossible) <= 2 and len(oCrit.oSub.aoPossible) <= 2))) \
                   or oCrit.fExpanded is True:
-                    sClass = 'sf-collapsable';
+                    sClass = 'sf-collapsible';
                     sChar  = '&#9660;';
                 else:
@@ -942,5 +947,5 @@
                     sChar  = '&#9654;';
 
-                sHtml += u'  <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsableDtDd(this);">%s'\
+                sHtml += u'  <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsibleDtDd(this);">%s'\
                          u' %s</a></dt>\n' \
                          u'  <dd class="%s">\n' \
@@ -950,12 +955,29 @@
                 for oDesc in oCrit.aoPossible:
                     fChecked = oDesc.oValue in oCrit.aoSelected;
-                    sHtml += u'    <li%s%s><input type="checkbox" name="%s" value="%s"%s/>%s%s</li>\n' \
+                    sHtml += u'    <li%s%s><label><input type="checkbox" name="%s" value="%s"%s%s/>%s%s</label>\n' \
                            % ( ' class="side-filter-irrelevant"' if oDesc.fIrrelevant else '',
-                               ' title="%s"' % (webutils.escapeAttr(oDesc.sHover,) if oDesc.sHover is not None else ''),
-                               oCrit.sVarNm, oDesc.oValue, ' checked' if fChecked else '',
+                               (' title="%s"' % (webutils.escapeAttr(oDesc.sHover,)) if oDesc.sHover is not None else ''),
+                               oCrit.sVarNm,
+                               oDesc.oValue,
+                               ' checked' if fChecked else '',
+                               ' onclick="toggleCollapsibleCheckbox(this);"' if oDesc.aoSubs is not None else '',
                                webutils.escapeElem(oDesc.sDesc),
                                '<span class="side-filter-count"> [%u]</span>' % (oDesc.cTimes) if oDesc.cTimes is not None
                                else '', );
-
+                    if oDesc.aoSubs is not None:
+                        sHtml += u'     <ul class="sf-checkbox-%s">\n' % ('collapsible' if fChecked else 'expandable', );
+                        for oSubDesc in oDesc.aoSubs:
+                            fSubChecked = oSubDesc.oValue in oCrit.oSub.aoSelected;
+                            sHtml += u'     <li%s%s><label><input type="checkbox" name="%s" value="%s"%s/>%s%s</label>\n' \
+                                   % ( ' class="side-filter-irrelevant"' if oSubDesc.fIrrelevant else '',
+                                       ' title="%s"' % ( webutils.escapeAttr(oSubDesc.sHover,) if oSubDesc.sHover is not None
+                                                         else ''),
+                                       oCrit.oSub.sVarNm, oSubDesc.oValue, ' checked' if fSubChecked else '',
+                                       webutils.escapeElem(oSubDesc.sDesc),
+                                       '<span class="side-filter-count"> [%u]</span>' % (oSubDesc.cTimes)
+                                       if oSubDesc.cTimes is not None else '', );
+
+                        sHtml += u'     </ul>\n';
+                    sHtml += u'    </li>';
                 sHtml += u'   </ul>\n' \
                          u'  </dd>\n';
