1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
# SPDX-License-Identifier: LGPL-2.1-only
from collections import OrderedDict
from contextlib import suppress
import typing
from PyQt6 import QtWidgets
import setools
from .criteria import OptionsPlacement
from .name import NameWidget
from .ranged import RangedWidget
LEVEL_VALIDATION: typing.Final[str] = r"[A-Za-z0-9.,_:]+"
RANGE_VALIDATION: typing.Final[str] = r"[A-Za-z0-9.,_:]+ ?(- ?[A-Za-z0-9.,_:]+)?"
__all__ = ("MLSLevelName", "MLSRangeName")
class MLSLevelName(NameWidget):
"""
Widget providing a QLineEdit that saves the input to the attributes
of the specified query. This supports inputs of MLS levels.
"""
def __init__(self, title: str, query: setools.PolicyQuery, attrname: str, /, *,
required: bool = False, enable_opts: bool = False,
options_placement: OptionsPlacement = OptionsPlacement.RIGHT,
parent: QtWidgets.QWidget | None = None):
# Not much we can do here. Leave all validation to the query.
super().__init__(title, query, attrname, [], LEVEL_VALIDATION, enable_regex=False,
options_placement=options_placement,
required=required, parent=parent)
# the rstrip("_") below is to avoid names like "range__overlap"
self.criteria_opts = OrderedDict[str, QtWidgets.QRadioButton]()
if enable_opts:
equ = QtWidgets.QRadioButton("Equal", parent=self)
equ.setChecked(True)
equ.setToolTip("The statement will match if the criteria is equal.")
equ.toggled.connect(self._update_radio_opts)
self.criteria_opts[""] = equ
dom = QtWidgets.QRadioButton("Dominate", parent=self)
dom.setChecked(False)
dom.setToolTip("The statement will match if the criteria dominates the level.")
dom.toggled.connect(self._update_radio_opts)
self.criteria_opts[f"{attrname.rstrip('_')}_dom"] = dom
domby = QtWidgets.QRadioButton("Dominated By", parent=self)
domby.setChecked(False)
domby.setToolTip("The statement will match if the criteria is dominated by the level.")
domby.toggled.connect(self._update_radio_opts)
self.criteria_opts[f"{attrname.rstrip('_')}_domby"] = domby
incomp = QtWidgets.QRadioButton("Imcomparable", parent=self)
incomp.setChecked(False)
incomp.setToolTip("The statement will match if the criteria is "
"incomparable to the level.")
incomp.toggled.connect(self._update_radio_opts)
self.criteria_opts[f"{attrname.rstrip('_')}_incomp"] = incomp
# place option radio buttons
match options_placement:
case OptionsPlacement.BELOW:
self.top_layout.addWidget(equ, 1, 0, 1, 1)
self.top_layout.addWidget(dom, 1, 1, 1, 1)
self.top_layout.addWidget(domby, 2, 0, 1, 1)
self.top_layout.addWidget(incomp, 2, 1, 1, 1)
case OptionsPlacement.RIGHT:
self.top_layout.addWidget(equ, 0, 1, 1, 1)
self.top_layout.addWidget(dom, 0, 2, 1, 1)
self.top_layout.addWidget(domby, 1, 1, 1, 1)
self.top_layout.addWidget(incomp, 1, 2, 1, 1)
case _:
raise AssertionError("Invalid options placement, this is an SETools bug.")
def _update_radio_opts(self, value: bool = True) -> None:
"""Update the query based on the range opts radio button state."""
if not value:
return # only apply updates once per radio button switch
for name, w in self.criteria_opts.items():
if name: # empty means equal, which is the default
self.log.debug(f"Setting {name} to {w.isChecked()}")
setattr(self.query, name, w.isChecked())
def save(self, settings: dict) -> None:
super().save(settings)
for name, w in self.criteria_opts.items():
if name:
settings[name] = w.isChecked()
def load(self, settings: dict) -> None:
for name, w in self.criteria_opts.items():
if name:
with suppress(AttributeError, KeyError):
w.setChecked(settings[name])
super().load(settings)
class MLSRangeName(RangedWidget):
"""
Widget providing a QLineEdit that saves the input to the attributes
of the specified query. This supports inputs of MLS ranges.
"""
def __init__(self, title: str, query: setools.PolicyQuery, attrname: str, /, *,
required: bool = False,
enable_range_opts: bool = False,
options_placement: OptionsPlacement = OptionsPlacement.RIGHT,
parent: QtWidgets.QWidget | None = None):
super().__init__(title, query, attrname, validation=RANGE_VALIDATION,
enable_range_opts=enable_range_opts, required=required,
options_placement=options_placement, parent=parent)
if __name__ == '__main__':
import sys
import logging
import warnings
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s|%(levelname)s|%(name)s|%(message)s')
warnings.simplefilter("default")
q = setools.MLSRuleQuery(setools.SELinuxPolicy())
app = QtWidgets.QApplication(sys.argv)
mw = QtWidgets.QMainWindow()
widget = MLSRangeName("Test Range", q, "default", enable_range_opts=True, parent=mw)
widget.setToolTip("test tooltip")
widget.setWhatsThis("test what's this")
mw.setCentralWidget(widget)
mw.resize(widget.size())
whatsthis = QtWidgets.QWhatsThis.createAction(mw)
mw.menuBar().addAction(whatsthis) # type: ignore[union-attr]
mw.setStatusBar(QtWidgets.QStatusBar(mw))
mw.show()
sys.exit(app.exec())
|