44 QHBoxLayout ,
55 QLabel ,
66 QComboBox ,
7- QLineEdit ,
87 QPushButton ,
98 QCheckBox ,
9+ QCompleter ,
1010 QTreeWidget ,
1111 QTreeWidgetItem ,
1212 QHeaderView ,
@@ -23,6 +23,7 @@ def __init__(self, config: SysmonConfig) -> None:
2323 self .config = config
2424 self .current_event_id : int | None = None
2525 self .current_event_name : str = ""
26+ self .tree_meta_role = int (Qt .ItemDataRole .UserRole ) + 1
2627
2728 self .layout = QVBoxLayout ()
2829 self .setLayout (self .layout )
@@ -36,20 +37,37 @@ def __init__(self, config: SysmonConfig) -> None:
3637 self .group_box .setEditable (True )
3738 self .group_box .setInsertPolicy (QComboBox .InsertPolicy .NoInsert )
3839 self .group_box .setPlaceholderText ("Select or type rule group (optional)" )
40+ self .group_completer = QCompleter (self .group_box .model (), self .group_box )
41+ self .group_completer .setCaseSensitivity (Qt .CaseSensitivity .CaseInsensitive )
42+ self .group_completer .setFilterMode (Qt .MatchFlag .MatchContains )
43+ self .group_completer .setCompletionMode (QCompleter .CompletionMode .PopupCompletion )
44+ self .group_box .setCompleter (self .group_completer )
3945
4046 self .group_relation = QComboBox ()
4147 self .group_relation .addItems (["or" , "and" ])
4248
4349 self .field_box = QComboBox ()
50+ self .field_box .setEditable (True )
51+ self .field_box .setInsertPolicy (QComboBox .InsertPolicy .NoInsert )
52+ self .field_box .setPlaceholderText ("Select or type category..." )
53+ self .field_completer = QCompleter (self .field_box .model (), self .field_box )
54+ self .field_completer .setCaseSensitivity (Qt .CaseSensitivity .CaseInsensitive )
55+ self .field_completer .setFilterMode (Qt .MatchFlag .MatchContains )
56+ self .field_completer .setCompletionMode (QCompleter .CompletionMode .PopupCompletion )
57+ self .field_box .setCompleter (self .field_completer )
4458
4559 self .condition_box = QComboBox ()
4660 self .condition_box .addItems (["is" , "contains" , "begin with" , "end with" ])
4761
4862 self .value_preset_box = QComboBox ()
49- self .value_preset_box .setEditable (False )
50-
51- self .value_input = QLineEdit ()
52- self .value_input .setPlaceholderText ("Enter custom value..." )
63+ self .value_preset_box .setEditable (True )
64+ self .value_preset_box .setInsertPolicy (QComboBox .InsertPolicy .NoInsert )
65+ self .value_preset_box .setPlaceholderText ("Select or type value..." )
66+ self .value_completer = QCompleter (self .value_preset_box .model (), self .value_preset_box )
67+ self .value_completer .setCaseSensitivity (Qt .CaseSensitivity .CaseInsensitive )
68+ self .value_completer .setFilterMode (Qt .MatchFlag .MatchContains )
69+ self .value_completer .setCompletionMode (QCompleter .CompletionMode .PopupCompletion )
70+ self .value_preset_box .setCompleter (self .value_completer )
5371
5472 self .add_button = QPushButton ("Add Rule" )
5573 self .remove_button = QPushButton ("Remove Selected Rule" )
@@ -73,7 +91,6 @@ def __init__(self, config: SysmonConfig) -> None:
7391
7492 self .rule_row_2 = QHBoxLayout ()
7593 self .rule_row_2 .addWidget (self .value_preset_box )
76- self .rule_row_2 .addWidget (self .value_input )
7794
7895 self .layout .addWidget (self .title )
7996 self .layout .addLayout (self .group_row )
@@ -88,62 +105,62 @@ def __init__(self, config: SysmonConfig) -> None:
88105 self .add_button .clicked .connect (self .add_rule )
89106 self .remove_button .clicked .connect (self .remove_selected_rule )
90107 self .field_box .currentTextChanged .connect (self .load_value_presets_for_field )
91- self .group_box .currentIndexChanged .connect (self .on_group_selected )
92108 self .new_rules_only_toggle .stateChanged .connect (self .refresh_rules )
109+ self .rule_tree .itemClicked .connect (self .on_rule_tree_item_clicked )
93110
94111 def set_event (self , event_id : int , event_name : str ) -> None :
112+ self ._set_active_event (event_id , event_name )
113+ self .refresh_rules ()
114+
115+ def _set_active_event (self , event_id : int , event_name : str ) -> None :
95116 self .current_event_id = event_id
96117 self .current_event_name = event_name
97118 self .title .setText (f"{ event_id } - { event_name } " )
119+ self .group_box .setEditText ("" )
98120 self .load_fields_for_event ()
99121 self .refresh_group_options ()
100- self .refresh_rules ()
101122
102123 def refresh_group_options (self ) -> None :
124+ typed_text = self .group_box .currentText ()
103125 self .group_box .blockSignals (True )
104126 self .group_box .clear ()
105127 self .group_box .addItem ("" )
106- self .group_box .setItemData (0 , None , Qt .ItemDataRole .UserRole )
107128
108129 if self .current_event_id is None :
130+ self .group_box .setEditText (typed_text )
109131 self .group_box .blockSignals (False )
110132 return
111133
112134 event_config = self .config .events .get (self .current_event_id )
113135 if event_config is None :
136+ self .group_box .setEditText (typed_text )
114137 self .group_box .blockSignals (False )
115138 return
116139
140+ group_labels : list [str ] = []
117141 seen_group_ids : set [str ] = set ()
118142 for rule in event_config .rules :
119143 if not rule .group_id or rule .group_id in seen_group_ids :
120144 continue
121145 seen_group_ids .add (rule .group_id )
122146
123- group_name = rule .group_name or "Imported Rule"
124- group_relation = rule .group_relation or "or"
125- label = f"{ group_name } ({ group_relation } )"
126- self .group_box .addItem (label )
127- self .group_box .setItemData (
128- self .group_box .count () - 1 ,
129- {
130- "group_id" : rule .group_id ,
131- "group_name" : rule .group_name ,
132- "group_relation" : group_relation ,
133- },
134- Qt .ItemDataRole .UserRole ,
135- )
147+ group_name = (rule .group_name or "" ).strip ()
148+ if group_name :
149+ group_relation = (rule .group_relation or "or" ).strip ()
150+ group_labels .append (f"{ group_name } ({ group_relation } )" )
136151
152+ self .group_box .addItems (group_labels )
153+ self .group_box .setEditText (typed_text )
137154 self .group_box .blockSignals (False )
138155
139- def on_group_selected (self , * _args ) -> None :
140- selected_data = self . group_box . currentData ( Qt . ItemDataRole . UserRole )
141- if not isinstance ( selected_data , dict ) :
142- return
143-
144- selected_relation = selected_data . get ( "group_relation" )
145- if selected_relation in ( "or" , "and" ):
146- self . group_relation . setCurrentText ( selected_relation )
156+ def _split_group_text (self , raw_text : str ) -> tuple [ str , str | None ] :
157+ text = raw_text . strip ( )
158+ if text . endswith ( ")" ) and " (" in text :
159+ name_part , relation_part = text . rsplit ( " (" , 1 )
160+ relation = relation_part [: - 1 ]. strip (). lower ()
161+ if relation in ( "or" , "and" ):
162+ return name_part . strip (), relation
163+ return text , None
147164
148165 def load_fields_for_event (self ) -> None :
149166 from data .sysmon_fields import SYS_MON_FIELDS
@@ -167,12 +184,14 @@ def load_fields_for_event(self) -> None:
167184 def load_value_presets_for_field (self , field_name : str ) -> None :
168185 from data .sysmon_value_presets import SYS_MON_VALUE_PRESETS
169186
187+ typed_value = self .value_preset_box .currentText ()
170188 self .value_preset_box .clear ()
171189 self .value_preset_box .addItem ("" )
172190
173191 presets = SYS_MON_VALUE_PRESETS .get (field_name , [])
174192 if presets :
175193 self .value_preset_box .addItems (presets )
194+ self .value_preset_box .setCurrentText (typed_value )
176195
177196 def refresh_rules (self ) -> None :
178197 self .rule_tree .clear ()
@@ -212,6 +231,11 @@ def refresh_rules(self) -> None:
212231 )
213232 event_item .setTextAlignment (1 , Qt .AlignmentFlag .AlignRight | Qt .AlignmentFlag .AlignVCenter )
214233 event_item .setForeground (1 , QColor ("#90ee90" )) # light green counts
234+ event_item .setData (
235+ 0 ,
236+ self .tree_meta_role ,
237+ {"kind" : "event" , "event_id" : event_id },
238+ )
215239 self .rule_tree .addTopLevelItem (event_item )
216240
217241 grouped_parents : dict [str , QTreeWidgetItem ] = {}
@@ -226,11 +250,26 @@ def refresh_rules(self) -> None:
226250 grouped_parents [rule .group_id ] = QTreeWidgetItem (
227251 [f"Rule: { group_name } ({ group_relation } )" , "" ]
228252 )
253+ grouped_parents [rule .group_id ].setData (
254+ 0 ,
255+ self .tree_meta_role ,
256+ {
257+ "kind" : "group" ,
258+ "event_id" : event_id ,
259+ "group_name" : group_name ,
260+ "group_relation" : group_relation ,
261+ },
262+ )
229263 event_item .addChild (grouped_parents [rule .group_id ])
230264 parent_item = grouped_parents [rule .group_id ]
231265 else :
232266 if ungrouped_parent is None :
233267 ungrouped_parent = QTreeWidgetItem (["Ungrouped Rules" , "" ])
268+ ungrouped_parent .setData (
269+ 0 ,
270+ self .tree_meta_role ,
271+ {"kind" : "ungrouped" , "event_id" : event_id },
272+ )
234273 event_item .addChild (ungrouped_parent )
235274 parent_item = ungrouped_parent
236275
@@ -243,6 +282,11 @@ def refresh_rules(self) -> None:
243282 )
244283 item = QTreeWidgetItem ([rule_text , "" ])
245284 item .setData (0 , Qt .ItemDataRole .UserRole , (event_id , rule_index ))
285+ item .setData (
286+ 0 ,
287+ self .tree_meta_role ,
288+ {"kind" : "rule" , "event_id" : event_id , "rule_index" : rule_index },
289+ )
246290
247291 if not rule .imported :
248292 item .setBackground (0 , QColor ("#ffe6cc" )) # light orange
@@ -257,14 +301,60 @@ def refresh_rules(self) -> None:
257301 f'Total Exclude <span style="color:#90ee90">({ total_exclude } )</span>'
258302 )
259303
304+ def on_rule_tree_item_clicked (self , item : QTreeWidgetItem , _column : int ) -> None :
305+ meta = item .data (0 , self .tree_meta_role )
306+ if not isinstance (meta , dict ):
307+ return
308+
309+ kind = meta .get ("kind" )
310+ event_id = meta .get ("event_id" )
311+ if not isinstance (event_id , int ):
312+ return
313+
314+ event_config = self .config .events .get (event_id )
315+ if event_config is None :
316+ return
317+
318+ self ._set_active_event (event_id , event_config .event_name )
319+
320+ if kind == "group" :
321+ group_name = (meta .get ("group_name" ) or "" ).strip ()
322+ group_relation = (meta .get ("group_relation" ) or "or" ).strip ().lower ()
323+ if group_name :
324+ self .group_box .setEditText (f"{ group_name } ({ group_relation } )" )
325+ else :
326+ self .group_box .setEditText ("" )
327+ if group_relation in ("or" , "and" ):
328+ self .group_relation .setCurrentText (group_relation )
329+ return
330+
331+ if kind != "rule" :
332+ return
333+
334+ rule_index = meta .get ("rule_index" )
335+ if not isinstance (rule_index , int ) or not (0 <= rule_index < len (event_config .rules )):
336+ return
337+
338+ rule = event_config .rules [rule_index ]
339+ self .rule_type .setCurrentText (rule .rule_type )
340+ self .field_box .setCurrentText (rule .field_name )
341+ self .condition_box .setCurrentText (rule .condition )
342+
343+ if rule .group_name :
344+ relation = rule .group_relation or "or"
345+ self .group_box .setEditText (f"{ rule .group_name } ({ relation } )" )
346+ if relation in ("or" , "and" ):
347+ self .group_relation .setCurrentText (relation )
348+ else :
349+ self .group_box .setEditText ("" )
350+
351+ self .value_preset_box .setCurrentText (rule .value )
352+
260353 def add_rule (self ) -> None :
261354 if self .current_event_id is None :
262355 return
263356
264- custom_value = self .value_input .text ().strip ()
265- preset_value = self .value_preset_box .currentText ().strip ()
266-
267- value = custom_value if custom_value else preset_value
357+ value = self .value_preset_box .currentText ().strip ()
268358 if not value :
269359 return
270360
@@ -273,30 +363,30 @@ def add_rule(self) -> None:
273363 self .current_event_name ,
274364 )
275365
276- selected_group_data = self .group_box .currentData (Qt .ItemDataRole .UserRole )
277366 selected_group_text = self .group_box .currentText ().strip ()
367+ selected_group_name , selected_group_relation = self ._split_group_text (selected_group_text )
278368
279369 group_id : str | None = None
280370 group_name : str | None = None
281371 group_relation : str | None = None
282372
283- if isinstance (selected_group_data , dict ):
284- group_id = selected_group_data .get ("group_id" )
285- group_name = selected_group_data .get ("group_name" )
286- group_relation = selected_group_data .get ("group_relation" )
287- elif selected_group_text :
373+ if selected_group_name :
288374 existing_group = None
289375 for existing_rule in event_config .rules :
290376 if not existing_rule .group_id :
291377 continue
292- if (existing_rule .group_name or "" ).strip ().lower () == selected_group_text .lower ():
378+ if (existing_rule .group_name or "" ).strip ().lower () == selected_group_name .lower ():
293379 existing_group = existing_rule
294380 break
295381
296382 if existing_group is not None :
297383 group_id = existing_group .group_id
298384 group_name = existing_group .group_name
299- group_relation = existing_group .group_relation or self .group_relation .currentText ()
385+ group_relation = (
386+ existing_group .group_relation
387+ or selected_group_relation
388+ or self .group_relation .currentText ()
389+ )
300390 else :
301391 existing_ids = {
302392 existing_rule .group_id
@@ -309,8 +399,8 @@ def add_rule(self) -> None:
309399 next_index += 1
310400 group_id = f"user-group-{ next_index } "
311401
312- group_name = selected_group_text
313- group_relation = self .group_relation .currentText ()
402+ group_name = selected_group_name
403+ group_relation = selected_group_relation or self .group_relation .currentText ()
314404
315405 event_config .rules .append (
316406 RuleFilter (
@@ -324,8 +414,7 @@ def add_rule(self) -> None:
324414 )
325415 )
326416
327- self .value_input .clear ()
328- self .value_preset_box .setCurrentIndex (0 )
417+ self .value_preset_box .setEditText ("" )
329418 self .refresh_group_options ()
330419 self .refresh_rules ()
331420
0 commit comments