report-xfstests.py: Move the test viewer into a new file
Also: - Add some custom CSS to the widgets - Add tooltips to each test result cell - Add a popover with failing test output - Improve filtering in place of the view switches Signed-off-by: Anna Schumaker <Anna.Schumaker@Netapp.com>
This commit is contained in:
parent
6f3c37e568
commit
55f3239edc
|
@ -1,258 +1,27 @@
|
|||
#!/usr/bin/python
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
|
||||
import bisect
|
||||
import pathlib
|
||||
import reporter
|
||||
import re
|
||||
import sys
|
||||
import xdg.BaseDirectory
|
||||
import xml.etree.ElementTree
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
|
||||
XFSTESTS = pathlib.Path(xdg.BaseDirectory.xdg_data_home) / "xfstests" / "date"
|
||||
|
||||
class TestCase(GObject.GObject):
|
||||
def __init__(self, name):
|
||||
GObject.GObject.__init__(self)
|
||||
self.name = name
|
||||
self.tests = dict()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "Test Name":
|
||||
return self.name
|
||||
match len(self.tests[key]) if key in self.tests else None:
|
||||
case None: return "Missing"
|
||||
case 0: return "Passed"
|
||||
case 1: return "Skipped"
|
||||
case _: return "Failed"
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
self.tests[key] = val
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __lt__(self, rhs):
|
||||
return self.name < rhs.name
|
||||
|
||||
def passed(self):
|
||||
return False not in [ len(elm) == 0 for elm in self.tests.values() ]
|
||||
|
||||
def failed(self):
|
||||
return 3 in [ len(elm) for elm in self.tests.values() ]
|
||||
|
||||
def skipped(self):
|
||||
return False not in [ len(elm) == 1 for elm in self.tests.values() ]
|
||||
|
||||
|
||||
class TestResultsModel(GObject.GObject, Gio.ListModel):
|
||||
def __init__(self):
|
||||
GObject.GObject.__init__(self)
|
||||
self.tests = [ ]
|
||||
self.columns = [ "Test Name" ]
|
||||
|
||||
def do_get_item_type(self): return GObject.TYPE_PYOBJECT
|
||||
def do_get_n_items(self): return len(self.tests)
|
||||
def do_get_item(self, n): return self.tests[n]
|
||||
def get_n_columns(self): return len(self.columns)
|
||||
def get_column_name(self, n): return self.columns[n]
|
||||
|
||||
def find_testcase(self, name):
|
||||
i = bisect.bisect(self.tests, name, key=str)
|
||||
if i > 0 and self.tests[i-1].name == name:
|
||||
return self.tests[i-1]
|
||||
self.tests.insert(i, TestCase(name))
|
||||
return self.tests[i]
|
||||
|
||||
def set_tests(self, path):
|
||||
rm = len(self.tests)
|
||||
self.columns = [ "Test Name" ]
|
||||
self.tests.clear()
|
||||
|
||||
for file in sorted(path.iterdir() if path else []):
|
||||
self.columns.append(file.stem)
|
||||
root = xml.etree.ElementTree.parse(file).getroot()
|
||||
|
||||
for elm in root:
|
||||
if elm.tag == "testcase":
|
||||
testcase = self.find_testcase(elm.attrib["name"])
|
||||
testcase[file.stem] = elm
|
||||
self.emit("items-changed", 0, rm, len(self.tests))
|
||||
|
||||
|
||||
class TestResultsFilter(Gtk.Filter):
|
||||
def __init__(self):
|
||||
Gtk.Filter.__init__(self)
|
||||
self.show_passing = True
|
||||
self.show_skipped = True
|
||||
self.regex = re.compile("")
|
||||
|
||||
def set_regex(self, text):
|
||||
change = Gtk.FilterChange.DIFFERENT
|
||||
if text in self.regex.pattern:
|
||||
change = Gtk.FilterChange.LESS_STRICT
|
||||
elif self.regex.pattern in text:
|
||||
change = Gtk.FilterChange.MORE_STRICT
|
||||
|
||||
self.regex = re.compile(text, re.I)
|
||||
self.changed(change)
|
||||
|
||||
def set_show_passing(self, val):
|
||||
self.show_passing = val
|
||||
self.set_show_changed(self.show_passing)
|
||||
|
||||
def set_show_skipped(self, val):
|
||||
self.show_skipped = val
|
||||
self.set_show_changed(self.show_skipped)
|
||||
|
||||
def set_show_changed(self, newval):
|
||||
match newval:
|
||||
case True: self.changed(Gtk.FilterChange.LESS_STRICT)
|
||||
case False: self.changed(Gtk.FilterChange.MORE_STRICT)
|
||||
|
||||
def do_match(self, item):
|
||||
if not self.show_passing and item.passed():
|
||||
return False
|
||||
if not self.show_skipped and item.skipped():
|
||||
return False
|
||||
if not self.show_passing and not self.show_skipped and not item.failed():
|
||||
return False
|
||||
return self.regex.search(item.name) != None
|
||||
|
||||
|
||||
class FilterPopover(Gtk.Popover):
|
||||
def __init__(self, parent):
|
||||
Gtk.Popover.__init__(self)
|
||||
self.set_child(Gtk.Label())
|
||||
self.set_autohide(False)
|
||||
self.set_parent(parent)
|
||||
|
||||
def popup(self, text):
|
||||
self.get_child().set_text(text)
|
||||
super().popup()
|
||||
|
||||
|
||||
class TestResultsFactory(Gtk.SignalListItemFactory):
|
||||
def __init__(self, column):
|
||||
Gtk.SignalListItemFactory.__init__(self)
|
||||
self.column = column
|
||||
self.connect("setup", self.setup)
|
||||
self.connect("bind", self.bind)
|
||||
self.connect("unbind", self.unbind)
|
||||
self.connect("teardown", self.teardown)
|
||||
|
||||
def setup(self, factory, listitem):
|
||||
listitem.set_child(Gtk.Label(xalign=0))
|
||||
|
||||
def bind(self, factory, listitem):
|
||||
label = listitem.get_child()
|
||||
text = listitem.get_item()[self.column]
|
||||
match text:
|
||||
case "Passed": label.set_markup("<span background='green'> Passed </span>")
|
||||
case "Skipped": label.set_markup("<span background='orange'> Skipped </span>")
|
||||
case "Failed": label.set_markup("<span background='red'> Failed </span>")
|
||||
case _: label.set_text(" " + text)
|
||||
|
||||
def unbind(self, factory, listitem):
|
||||
listitem.get_child().set_text("")
|
||||
|
||||
def teardown(self, factory, listitem):
|
||||
listitem.set_child(None)
|
||||
|
||||
|
||||
class TestWindow(Gtk.ScrolledWindow):
|
||||
def __init__(self):
|
||||
Gtk.ScrolledWindow.__init__(self, vexpand=True, hexpand=True,
|
||||
hscrollbar_policy=Gtk.PolicyType.NEVER)
|
||||
self.results = TestResultsModel()
|
||||
self.filter = Gtk.FilterListModel.new(self.results, TestResultsFilter())
|
||||
self.selection = Gtk.NoSelection.new(self.filter)
|
||||
self.view = Gtk.ColumnView.new(self.selection)
|
||||
self.set_child(self.view)
|
||||
if len(sys.argv) > 1:
|
||||
self.set_tests(pathlib.Path(sys.argv[1]))
|
||||
|
||||
def set_show_passing(self, show):
|
||||
self.filter.get_filter().set_show_passing(show)
|
||||
|
||||
def set_show_skipped(self, show):
|
||||
self.filter.get_filter().set_show_skipped(show)
|
||||
|
||||
def set_filter_regex(self, text):
|
||||
self.filter.get_filter().set_regex(text)
|
||||
|
||||
def set_tests(self, path):
|
||||
for column in [ c for c in self.view.get_columns() ]:
|
||||
self.view.remove_column(column)
|
||||
self.results.set_tests(path)
|
||||
for n in range(self.results.get_n_columns()):
|
||||
name = self.results.get_column_name(n)
|
||||
col = Gtk.ColumnViewColumn.new(name, TestResultsFactory(name))
|
||||
col.set_expand(n > 0)
|
||||
self.view.append_column(col)
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self):
|
||||
Gtk.Window.__init__(self, title="Xfstests Results")
|
||||
self.chooser = reporter.TestChooser()
|
||||
self.testview = reporter.TestView()
|
||||
self.passing = Gtk.Switch(halign=Gtk.Align.CENTER, active=True)
|
||||
self.lpass = Gtk.Label.new("Show Passing Tests")
|
||||
self.skipped = Gtk.Switch(halign=Gtk.Align.CENTER, active=True)
|
||||
self.lskip = Gtk.Label.new("Show Skipped Tests")
|
||||
self.search = Gtk.SearchEntry(placeholder_text="Type To Filter Tests")
|
||||
self.popover = FilterPopover(parent=self.search)
|
||||
self.xfstests = TestWindow()
|
||||
|
||||
self.left = Gtk.Grid(row_spacing=5)
|
||||
self.left.attach(self.chooser, 0, 0, 2, 1)
|
||||
self.left.attach(self.passing, 0, 1, 1, 1)
|
||||
self.left.attach(self.lpass, 1, 1, 1, 1)
|
||||
self.left.attach(self.skipped, 0, 2, 1, 1)
|
||||
self.left.attach(self.lskip, 1, 2, 1, 1)
|
||||
|
||||
self.right = Gtk.Box.new(Gtk.Orientation.VERTICAL, 5)
|
||||
self.right.append(self.testview)
|
||||
self.right.append(self.search)
|
||||
self.right.append(self.xfstests)
|
||||
|
||||
self.testview = reporter.TestViewer()
|
||||
self.child = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 5)
|
||||
self.child.append(self.left)
|
||||
self.child.append(self.right)
|
||||
self.child.append(self.chooser)
|
||||
self.child.append(self.testview)
|
||||
|
||||
self.lskip.set_xalign(0)
|
||||
self.lpass.set_xalign(0)
|
||||
self.set_default_size(1100, 750)
|
||||
self.set_default_size(1300, 800)
|
||||
self.set_child(self.child)
|
||||
|
||||
self.chooser.connect("test-selected", self.test_changed)
|
||||
self.passing.connect("notify::active", self.show_passing)
|
||||
self.skipped.connect("notify::active", self.show_skipped)
|
||||
self.search.connect("search-changed", self.search_changed)
|
||||
if len(sys.argv) > 1:
|
||||
self.testview.set_test_result(reporter.testchooser.Path(pathlib.Path(sys.argv[1])))
|
||||
|
||||
def test_changed(self, window, file):
|
||||
self.testview.set_test_result(file)
|
||||
self.xfstests.set_tests(file.path if file else None)
|
||||
|
||||
def show_passing(self, passing, param):
|
||||
self.xfstests.set_show_passing(passing.get_active())
|
||||
|
||||
def show_skipped(self, skipped, param):
|
||||
self.xfstests.set_show_skipped(skipped.get_active())
|
||||
|
||||
def search_changed(self, search):
|
||||
try:
|
||||
self.xfstests.set_filter_regex(search.get_text())
|
||||
self.search.remove_css_class("warning")
|
||||
self.popover.popdown()
|
||||
except re.error as e:
|
||||
self.search.add_css_class("warning")
|
||||
self.popover.popup(str(e))
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
|
|
|
@ -3,14 +3,19 @@ from . import common
|
|||
from . import testchooser
|
||||
from . import testproperties
|
||||
from . import testresults
|
||||
from . import testviewer
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
|
||||
SizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.VERTICAL)
|
||||
|
||||
class TestChooser(Gtk.Box):
|
||||
def __init__(self):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
self.stack = testchooser.Stack()
|
||||
self.append(Gtk.StackSwitcher(stack=self.stack))
|
||||
self.switcher = Gtk.StackSwitcher(stack=self.stack)
|
||||
SizeGroup.add_widget(self.switcher)
|
||||
self.append(self.switcher)
|
||||
self.append(self.stack)
|
||||
self.stack.connect("test-selected", self.on_test_selected)
|
||||
|
||||
|
@ -21,15 +26,22 @@ class TestChooser(Gtk.Box):
|
|||
def test_selected(self, path): pass
|
||||
|
||||
|
||||
class TestView(Gtk.Box):
|
||||
class TestViewer(Gtk.Box):
|
||||
def __init__(self):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
self.stack = testproperties.Stack()
|
||||
self.append(Gtk.StackSwitcher(stack=self.stack, halign=Gtk.Align.CENTER))
|
||||
self.viewer = testviewer.Viewer()
|
||||
self.switcher = Gtk.StackSwitcher(stack=self.stack, halign=Gtk.Align.CENTER)
|
||||
self.append(self.switcher)
|
||||
self.append(self.stack)
|
||||
self.append(self.viewer)
|
||||
self.append(testviewer.SearchEntry(self.viewer.get_filter()))
|
||||
SizeGroup.add_widget(self.switcher)
|
||||
|
||||
def set_test_result(self, file):
|
||||
self.stack.clear()
|
||||
self.viewer.clear()
|
||||
if file and file.get_is_test_result():
|
||||
results = testresults.TestResults(file.path)
|
||||
self.stack.show_properties(results)
|
||||
self.viewer.show_results(results)
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gtk
|
||||
import html
|
||||
import xml.etree.ElementTree
|
||||
|
||||
class PassingTest:
|
||||
def __init__(self, elm):
|
||||
self.time = elm.attrib["time"]
|
||||
|
||||
def timestr(self):
|
||||
(m, s) = divmod(int(self.time), 60)
|
||||
res = [ f"{s} second{'' if s == 1 else 's'}" ]
|
||||
if m > 0: res.insert(0, f"{m} minute{'' if m == 1 else 's'}")
|
||||
return ' and '.join(res)
|
||||
|
||||
|
||||
class SkippedTest:
|
||||
def __init__(self, elm):
|
||||
|
@ -16,11 +24,13 @@ class SkippedTest:
|
|||
class FailedTest:
|
||||
def __init__(self, elm):
|
||||
self.time = elm.attrib["time"]
|
||||
self.system_out = Gtk.TextBuffer()
|
||||
self.system_err = Gtk.TextBuffer()
|
||||
for e in elm:
|
||||
match e.tag:
|
||||
case "failure": self.message = e.attrib["message"]
|
||||
case "system-out": self.system_out = e.text
|
||||
case "system-err": self.system_err = e.text
|
||||
case "system-out": self.system_out.set_text(html.unescape(e.text))
|
||||
case "system-err": self.system_err.set_text(html.unescape(e.text))
|
||||
|
||||
|
||||
class TestCase(GObject.GObject):
|
||||
|
@ -30,7 +40,7 @@ class TestCase(GObject.GObject):
|
|||
self.versions = dict()
|
||||
|
||||
def __getitem__(self, key): return self.versions.get(key, None)
|
||||
def __setitem__(self, key, value): self.versions[key] = None
|
||||
def __setitem__(self, key, value): self.versions[key] = value
|
||||
|
||||
|
||||
class TestResults:
|
||||
|
|
207
reporter/testviewer.py
Normal file
207
reporter/testviewer.py
Normal file
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/python
|
||||
import itertools
|
||||
import re
|
||||
from . import testresults
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Gio
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
CSS = """
|
||||
@define-color darkorange #cc7000;
|
||||
@define-color darkred #8b0000;
|
||||
label.passing-test {
|
||||
background-color: green;
|
||||
padding: 4px;
|
||||
}
|
||||
label.skipped-test {
|
||||
background-color: darkorange;
|
||||
padding: 4px;
|
||||
}
|
||||
menubutton.failed-test>button {
|
||||
background: darkred;
|
||||
border-color: darkred;
|
||||
border-radius: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def markup_tooltip(status, color, message):
|
||||
return f"<span background='{color}'> Test {status} </span> {message}"
|
||||
|
||||
|
||||
class Label(Gtk.Label):
|
||||
def __init__(self, text):
|
||||
Gtk.Label.__init__(self, xalign=0)
|
||||
self.set_text(text)
|
||||
|
||||
|
||||
class PassingTest(Gtk.Label):
|
||||
def __init__(self, test):
|
||||
Gtk.Label.__init__(self)
|
||||
self.add_css_class("passing-test")
|
||||
self.set_text(f"{test.time} seconds")
|
||||
self.set_tooltip_markup(markup_tooltip("Passed", "green",
|
||||
f"in {test.timestr()}"))
|
||||
|
||||
|
||||
class SkippedTest(Gtk.Label):
|
||||
def __init__(self, test):
|
||||
Gtk.Label.__init__(self)
|
||||
self.add_css_class("skipped-test")
|
||||
self.set_text("Skipped")
|
||||
self.set_tooltip_markup(markup_tooltip("Skipped", "#cc7000", test.message))
|
||||
|
||||
|
||||
class TestOutput(Gtk.Box):
|
||||
def __init__(self, stdout, stderr):
|
||||
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
|
||||
self.add_css_class("popover")
|
||||
self.stack = Gtk.Stack(vexpand=True, transition_type=Gtk.StackTransitionType.OVER_LEFT_RIGHT)
|
||||
self.prepend(self.stack)
|
||||
self.prepend(Gtk.StackSwitcher(stack=self.stack))
|
||||
self.add_page("stdout", stdout)
|
||||
self.add_page("stderr", stderr)
|
||||
self.set_size_request(400, 500)
|
||||
|
||||
def add_page(self, name, buffer):
|
||||
view = Gtk.TextView(monospace=True)
|
||||
view.set_buffer(buffer)
|
||||
self.stack.add_titled(Gtk.ScrolledWindow(child=view), name, name)
|
||||
|
||||
|
||||
class FailedTest(Gtk.MenuButton):
|
||||
def __init__(self, test):
|
||||
Gtk.MenuButton.__init__(self, label="Failed")
|
||||
self.set_tooltip_markup(markup_tooltip("Failed", "#8b0000", test.message))
|
||||
self.set_popover(Gtk.Popover(child=TestOutput(test.system_out, test.system_err)))
|
||||
self.add_css_class("failed-test")
|
||||
|
||||
|
||||
class MissingTest(Gtk.Label):
|
||||
def __init__(self, test):
|
||||
Gtk.Label.__init__(self, sensitive=False)
|
||||
self.set_name("missing-test")
|
||||
self.set_text("Missing")
|
||||
self.set_tooltip_text("Test Data Is Missing")
|
||||
|
||||
|
||||
class Model(GObject.GObject, Gio.ListModel):
|
||||
def __init__(self, results):
|
||||
GObject.GObject.__init__(self)
|
||||
self.results = results
|
||||
self.columns = [ "Testcase" ] + self.results.versions
|
||||
self.tests = sorted(results.tests.keys())
|
||||
|
||||
def get_columns(self): return self.columns
|
||||
def do_get_item_type(self): return GObject.TYPE_PYOBJECT
|
||||
def do_get_n_items(self): return len(self.tests)
|
||||
def do_get_item(self, i): return self.results.tests[self.tests[i]]
|
||||
|
||||
|
||||
class Filter(Gtk.Filter):
|
||||
def __init__(self):
|
||||
Gtk.Filter.__init__(self)
|
||||
self.regex = re.compile("")
|
||||
|
||||
def set_regex(self, text):
|
||||
change = Gtk.FilterChange.DIFFERENT
|
||||
if text in self.regex.pattern:
|
||||
change = Gtk.FilterChange.LESS_STRICT
|
||||
elif self.regex.pattern in text:
|
||||
change = Gtk.FilterChange.MORE_STRICT
|
||||
|
||||
self.regex = re.compile(text, re.I)
|
||||
self.changed(change)
|
||||
|
||||
def do_match(self, testcase):
|
||||
search = [ f"name={testcase.name}" ]
|
||||
for key in sorted(testcase.versions.keys()):
|
||||
keys = [ key ] + key.split("-")
|
||||
match type(testcase[key]):
|
||||
case testresults.PassingTest: res = [ "passing", "passed" ]
|
||||
case testresults.SkippedTest: res = [ "skipped" ]
|
||||
case testresults.FailedTest: res = [ "failed", "failing" ]
|
||||
case _: res = [ "missing" ]
|
||||
for k in keys:
|
||||
for r in res:
|
||||
search.append(f"{k}={r}")
|
||||
return self.regex.search(" ".join(search)) != None
|
||||
|
||||
|
||||
class Factory(Gtk.SignalListItemFactory):
|
||||
def __init__(self, column):
|
||||
Gtk.SignalListItemFactory.__init__(self)
|
||||
self.column = column
|
||||
self.connect("bind", self.on_bind)
|
||||
self.connect("unbind", self.on_unbind)
|
||||
|
||||
def on_bind(self, factory, listitem):
|
||||
testcase = listitem.get_item()
|
||||
if self.column == "Testcase":
|
||||
child = Label(testcase.name)
|
||||
else:
|
||||
result = testcase[self.column]
|
||||
match type(result):
|
||||
case testresults.PassingTest: child = PassingTest(result)
|
||||
case testresults.SkippedTest: child = SkippedTest(result)
|
||||
case testresults.FailedTest: child = FailedTest(result)
|
||||
case _: child = child = MissingTest(result)
|
||||
listitem.set_child(child)
|
||||
|
||||
def on_unbind(self, factory, listitem):
|
||||
listitem.set_child(None)
|
||||
|
||||
|
||||
class Viewer(Gtk.ScrolledWindow):
|
||||
def __init__(self):
|
||||
Gtk.ScrolledWindow.__init__(self, hscrollbar_policy=Gtk.PolicyType.NEVER,
|
||||
hexpand=True, vexpand=True, margin_top=5)
|
||||
self.provider = Gtk.CssProvider()
|
||||
self.filter = Gtk.FilterListModel.new(filter=Filter())
|
||||
self.selection = Gtk.NoSelection.new(self.filter)
|
||||
self.columnview = Gtk.ColumnView.new(model=self.selection)
|
||||
self.columnview.add_css_class("data-table")
|
||||
|
||||
self.provider.load_from_data(CSS.encode())
|
||||
self.get_style_context().add_provider_for_display(self.get_display(), self.provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
self.set_child(self.columnview)
|
||||
|
||||
def get_filter(self):
|
||||
return self.filter.get_filter()
|
||||
|
||||
def clear(self):
|
||||
columns = self.columnview.get_columns()
|
||||
columns = [ columns.get_item(i) for i in range(columns.get_n_items()) ]
|
||||
for col in columns:
|
||||
self.columnview.remove_column(col)
|
||||
self.filter.set_model(None)
|
||||
|
||||
def show_results(self, results):
|
||||
if results:
|
||||
self.filter.set_model(Model(results))
|
||||
for i, name in enumerate(self.filter.get_model().get_columns()):
|
||||
column = Gtk.ColumnViewColumn.new(name, Factory(name))
|
||||
column.set_expand(i > 0)
|
||||
self.columnview.append_column(column)
|
||||
|
||||
|
||||
class SearchEntry(Gtk.SearchEntry):
|
||||
def __init__(self, filter):
|
||||
Gtk.SearchEntry.__init__(self, placeholder_text="Type To Filter",
|
||||
hexpand=True, margin_start=100, margin_end=100,
|
||||
margin_top=5, margin_bottom=5)
|
||||
self.popover = Gtk.Popover(child=Gtk.Label(), autohide=False)
|
||||
self.popover.set_parent(self)
|
||||
self.connect("search-changed", self.on_search_changed, filter)
|
||||
|
||||
def on_search_changed(self, search, filter):
|
||||
try:
|
||||
filter.set_regex(search.get_text())
|
||||
search.remove_css_class("warning")
|
||||
self.popover.popdown()
|
||||
except re.error as e:
|
||||
search.add_css_class("warning")
|
||||
self.popover.get_child().set_text(str(e))
|
||||
self.popover.popup()
|
Loading…
Reference in New Issue
Block a user