diff --git a/data/categoryquery.ui b/data/categoryquery.ui
new file mode 100644
index 0000000..832d661
--- /dev/null
+++ b/data/categoryquery.ui
@@ -0,0 +1,412 @@
+
+
+ CategoryQueryTab_ui
+
+
+
+ 0
+ 0
+ 774
+ 846
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 772
+ 844
+
+
+
+
+ 0
+ 0
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 20
+
+
+
+
+ 11
+ 75
+ true
+
+
+
+ Categories
+
+
+
+ -
+
+
+
+ 0
+ 80
+
+
+
+ Optionally enter notes here about the query.
+
+
+ Enter notes here.
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 440
+ 20
+
+
+
+
+ -
+
+
+ Show:
+
+
+
+ -
+
+
+ Show or hide the search criteria (no settings are lost)
+
+
+ Criteria
+
+
+ true
+
+
+
+ -
+
+
+ Show or hide the notes field (no data is lost)
+
+
+ Notes
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+ Qt::Horizontal
+
+
+
+ Categories Browser
+
+
+
-
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Search Criteria
+
+
+
-
+
+
+ QDialogButtonBox::Apply
+
+
+
+ -
+
+
+
+ 16777215
+ 120
+
+
+
+ Category Name
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 3
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 150
+ 20
+
+
+
+
+ 250
+ 16777215
+
+
+
+
+ -
+
+
+ Use regular expressions to match the type's name.
+
+
+ Regex
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 1
+
+
+
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Results
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractScrollArea::AdjustIgnored
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Raw Results
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Monospace
+
+
+
+
+
+
+ QPlainTextEdit::NoWrap
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ splitter
+ notes
+ label
+ label_2
+ horizontalSpacer
+ criteria_expander
+ notes_expander
+
+
+
+
+ GetDetailsListView
+ QListView
+ setoolsgui/getdetailslist.h
+
+
+
+ criteria_expander
+ notes_expander
+ cats
+ name
+ name_regex
+ results_frame
+ table_results
+ raw_results
+ notes
+
+
+
+
+ notes_expander
+ toggled(bool)
+ notes
+ setVisible(bool)
+
+
+ 732
+ 20
+
+
+ 386
+ 754
+
+
+
+
+ criteria_expander
+ toggled(bool)
+ criteria_frame
+ setVisible(bool)
+
+
+ 583
+ 20
+
+
+ 496
+ 226
+
+
+
+
+
diff --git a/setoolsgui/apol/categoryquery.py b/setoolsgui/apol/categoryquery.py
new file mode 100644
index 0000000..a19a0bf
--- /dev/null
+++ b/setoolsgui/apol/categoryquery.py
@@ -0,0 +1,175 @@
+# Copyright 2016, Tresys Technology, LLC
+#
+# This file is part of SETools.
+#
+# SETools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# SETools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SETools. If not, see
+# .
+#
+
+import logging
+
+from PyQt5.QtCore import Qt, QSortFilterProxyModel, QStringListModel, QThread
+from PyQt5.QtGui import QPalette, QTextCursor
+from PyQt5.QtWidgets import QCompleter, QHeaderView, QMessageBox, QProgressDialog, QScrollArea
+from setools import CategoryQuery
+
+from ..logtosignal import LogHandlerToSignal
+from ..models import SEToolsListModel, invert_list_selection
+from ..mlsmodel import MLSComponentTableModel, category_detail
+from ..widget import SEToolsWidget
+from .queryupdater import QueryResultsUpdater
+
+
+class CategoryQueryTab(SEToolsWidget, QScrollArea):
+
+ """Category browser and query tab."""
+
+ def __init__(self, parent, policy, perm_map):
+ super(CategoryQueryTab, self).__init__(parent)
+ self.log = logging.getLogger(__name__)
+ self.policy = policy
+ self.query = CategoryQuery(policy)
+ self.setupUi()
+
+ def __del__(self):
+ self.thread.quit()
+ self.thread.wait(5000)
+ logging.getLogger("setools.categoryquery").removeHandler(self.handler)
+
+ def setupUi(self):
+ self.load_ui("categoryquery.ui")
+
+ # populate category list
+ self.category_model = SEToolsListModel(self)
+ self.category_model.item_list = sorted(self.policy.categories())
+ self.cats.setModel(self.category_model)
+
+ # set up results
+ self.table_results_model = MLSComponentTableModel(self)
+ self.sort_proxy = QSortFilterProxyModel(self)
+ self.sort_proxy.setSourceModel(self.table_results_model)
+ self.table_results.setModel(self.sort_proxy)
+ self.table_results.sortByColumn(0, Qt.AscendingOrder)
+
+ # setup indications of errors on level/range
+ self.orig_palette = self.name.palette()
+ self.error_palette = self.name.palette()
+ self.error_palette.setColor(QPalette.Base, Qt.red)
+ self.clear_name_error()
+
+ # set up processing thread
+ self.thread = QThread()
+ self.worker = QueryResultsUpdater(self.query, self.table_results_model)
+ self.worker.moveToThread(self.thread)
+ self.worker.raw_line.connect(self.raw_results.appendPlainText)
+ self.worker.finished.connect(self.update_complete)
+ self.worker.finished.connect(self.thread.quit)
+ self.thread.started.connect(self.worker.update)
+
+ # create a "busy, please wait" dialog
+ self.busy = QProgressDialog(self)
+ self.busy.setModal(True)
+ self.busy.setRange(0, 0)
+ self.busy.setMinimumDuration(0)
+ self.busy.canceled.connect(self.thread.requestInterruption)
+ self.busy.reset()
+
+ # update busy dialog from query INFO logs
+ self.handler = LogHandlerToSignal()
+ self.handler.message.connect(self.busy.setLabelText)
+ logging.getLogger("setools.categoryquery").addHandler(self.handler)
+
+ # Ensure settings are consistent with the initial .ui state
+ self.notes.setHidden(not self.notes_expander.isChecked())
+
+ # connect signals
+ self.cats.doubleClicked.connect(self.get_detail)
+ self.cats.get_detail.triggered.connect(self.get_detail)
+ self.name.textEdited.connect(self.clear_name_error)
+ self.name.editingFinished.connect(self.set_name)
+ self.name_regex.toggled.connect(self.set_name_regex)
+ self.buttonBox.clicked.connect(self.run)
+
+ #
+ # Category browser
+ #
+ def get_detail(self):
+ # .ui is set for single item selection.
+ index = self.cats.selectedIndexes()[0]
+ item = self.category_model.data(index, Qt.UserRole)
+
+ self.log.debug("Generating detail window for {0}".format(item))
+ category_detail(self, item)
+
+ #
+ # Name criteria
+ #
+ def clear_name_error(self):
+ self.name.setToolTip("Match the category name.")
+ self.name.setPalette(self.orig_palette)
+
+ def set_name(self):
+ try:
+ self.query.name = self.name.text()
+ except Exception as ex:
+ self.log.error("Category name error: {0}".format(ex))
+ self.name.setToolTip("Error: " + str(ex))
+ self.name.setPalette(self.error_palette)
+
+ def set_name_regex(self, state):
+ self.log.debug("Setting name_regex {0}".format(state))
+ self.query.name_regex = state
+ self.clear_name_error()
+ self.set_name()
+
+ #
+ # Results runner
+ #
+
+ def run(self, button):
+ # right now there is only one button.
+
+ # start processing
+ self.busy.setLabelText("Processing query...")
+ self.busy.show()
+ self.raw_results.clear()
+ self.thread.start()
+
+ def update_complete(self, count):
+ self.log.info("{0} categories found.".format(count))
+
+ # update sizes/location of result displays
+ if not self.busy.wasCanceled():
+ self.busy.setLabelText("Resizing the result table's columns; GUI may be unresponsive")
+ self.busy.repaint()
+ self.table_results.resizeColumnsToContents()
+ # If the attrs or alias column widths are too long, pull back
+ # to a reasonable size
+ header = self.table_results.horizontalHeader()
+ if header.sectionSize(1) > 400:
+ header.resizeSection(1, 400)
+ if header.sectionSize(2) > 400:
+ header.resizeSection(2, 400)
+
+ if not self.busy.wasCanceled():
+ self.busy.setLabelText("Resizing the result table's rows; GUI may be unresponsive")
+ self.busy.repaint()
+ self.table_results.resizeRowsToContents()
+
+ if not self.busy.wasCanceled():
+ self.busy.setLabelText("Moving the raw result to top; GUI may be unresponsive")
+ self.busy.repaint()
+ self.raw_results.moveCursor(QTextCursor.Start)
+
+ self.busy.reset()
diff --git a/setoolsgui/apol/chooseanalysis.py b/setoolsgui/apol/chooseanalysis.py
index 182b848..a66a415 100644
--- a/setoolsgui/apol/chooseanalysis.py
+++ b/setoolsgui/apol/chooseanalysis.py
@@ -24,6 +24,7 @@ from ..widget import SEToolsWidget
# Analysis tabs:
from .boolquery import BoolQueryTab
+from .categoryquery import CategoryQueryTab
from .commonquery import CommonQueryTab
from .constraintquery import ConstraintQueryTab
from .dta import DomainTransitionAnalysisTab
@@ -92,6 +93,7 @@ class ChooseAnalysis(SEToolsWidget, QDialog):
if mls:
rule_map["MLS Rules"] = MLSRuleQueryTab
+ components_map["Categories"] = CategoryQueryTab
# populate the item list:
self.analysisTypes.clear()
diff --git a/setoolsgui/mlsmodel.py b/setoolsgui/mlsmodel.py
new file mode 100644
index 0000000..d4c5861
--- /dev/null
+++ b/setoolsgui/mlsmodel.py
@@ -0,0 +1,77 @@
+# Copyright 2016, Tresys Technology, LLC
+#
+# This file is part of SETools.
+#
+# SETools is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# SETools is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with SETools. If not, see
+# .
+#
+from collections import defaultdict
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QPalette, QTextCursor
+
+from .details import DetailsPopup
+from .models import SEToolsTableModel
+
+
+def _mls_detail(parent, obj, objtype):
+ """
+ Create a dialog box for category or sensitivity details.
+
+ Parameters:
+ parent The parent Qt Widget
+ type_ The type
+ """
+
+ detail = DetailsPopup(parent, "{0} detail: {1}".format(objtype, obj))
+
+ aliases = sorted(obj.aliases())
+ detail.append_header("Aliases ({0}):".format(len(aliases)))
+ for a in aliases:
+ detail.append(" {0}".format(a))
+
+ detail.show()
+
+
+def category_detail(parent, obj):
+ """
+ Create a dialog box for category details.
+
+ Parameters:
+ parent The parent Qt Widget
+ type_ The type
+ """
+ _mls_detail(parent, obj, "Category")
+
+
+class MLSComponentTableModel(SEToolsTableModel):
+
+ """Table-based model for sensitivities and categories."""
+
+ headers = defaultdict(str, {0: "Name", 1: "Aliases"})
+
+ def data(self, index, role):
+ if self.resultlist:
+ row = index.row()
+ col = index.column()
+ item = self.resultlist[row]
+
+ if role == Qt.DisplayRole:
+ if col == 0:
+ return str(item)
+ elif col == 1:
+ return ", ".join(sorted(str(a) for a in item.aliases()))
+
+ elif role == Qt.UserRole:
+ return item