@@ -91,9 +91,20 @@ def __init__( # pylint:disable=too-many-arguments
9191 None
9292 )
9393
94- self ._shown_reset_tags_note_type_warning : bool = False
95- self ._shown_reset_tags_field_warning : bool = False
96- self ._shown_reset_tags_morphemizer_warning : bool = False
94+ # key = source combobox
95+ self .reset_tags_warning_shown = {
96+ "field" : False ,
97+ "note type" : False ,
98+ "morphemizer" : False ,
99+ }
100+
101+ # Dynamically added widgets in the rows can be randomly garbage collected
102+ # if there are no persistent references to them outside the function that creates them.
103+ # This dict acts as a workaround to that problem.
104+ self .widget_references_by_row : list [tuple [Any , ...]] = []
105+
106+ # needed to prevent garbage collection
107+ self .selection_model : QItemSelectionModel | None = None
97108
98109 self .populate ()
99110 self .setup_buttons ()
@@ -144,6 +155,7 @@ def populate(self, use_default_config: bool = False) -> None:
144155 else :
145156 filters = self ._config .filters
146157
158+ self ._clear_note_filters_table ()
147159 self ._setup_note_filters_table (filters )
148160
149161 def setup_buttons (self ) -> None :
@@ -158,16 +170,15 @@ def setup_buttons(self) -> None:
158170 # disable while no rows are selected
159171 self ._on_no_row_selected ()
160172
161- selection_model = self .ui .note_filters_table .selectionModel ()
162- assert selection_model is not None
173+ self .selection_model = self .ui .note_filters_table .selectionModel ()
174+ assert self .selection_model is not None
175+ self .selection_model .selectionChanged .connect (self ._on_selection_changed )
163176
164- selection_model .selectionChanged .connect (
165- lambda : self ._on_selection_changed (selection_model )
166- )
177+ def _on_selection_changed (self ) -> None :
178+ assert self .selection_model is not None
167179
168- def _on_selection_changed (self , selection_model : QItemSelectionModel ) -> None :
169- selected_rows = selection_model .selectedRows ()
170- selected_indexes = selection_model .selectedIndexes ()
180+ selected_rows = self .selection_model .selectedRows ()
181+ selected_indexes = self .selection_model .selectedIndexes ()
171182
172183 if len (selected_indexes ) == 1 or len (selected_rows ) == 1 :
173184 self ._on_row_selected ()
@@ -280,8 +291,20 @@ def _delete_row(self) -> None:
280291 if confirmed :
281292 selected_row = self .ui .note_filters_table .currentRow ()
282293 self .ui .note_filters_table .removeRow (selected_row )
294+
295+ # prevents memory leaks
296+ del self .widget_references_by_row [selected_row ]
297+
283298 self .notify_subscribers ()
284299
300+ def _clear_note_filters_table (self ) -> None :
301+ """
302+ Prevents Memory Leaks
303+ """
304+ self .widget_references_by_row .clear ()
305+ self .ui .note_filters_table .clearContents ()
306+ self .ui .note_filters_table .setRowCount (0 ) # uses removeRows()
307+
285308 def _set_note_filters_table_row (
286309 self , row : int , config_filter : AnkiMorphsConfigFilter
287310 ) -> None :
@@ -292,10 +315,12 @@ def _set_note_filters_table_row(
292315 note_type_cbox .setProperty ("previousIndex" , note_type_cbox .currentIndex ())
293316 selected_note_type : str = note_type_cbox .itemText (note_type_cbox .currentIndex ())
294317
318+ tags_filter_widget = QTableWidgetItem (json .dumps (config_filter .tags ))
319+
295320 field_cbox = self ._setup_fields_cbox (config_filter , selected_note_type )
296321 field_cbox .setProperty ("previousIndex" , field_cbox .currentIndex ())
297322 field_cbox .currentIndexChanged .connect (
298- lambda index : self ._potentially_reset_tags_field (
323+ lambda index : self ._potentially_reset_tags (
299324 new_index = index ,
300325 combo_box = field_cbox ,
301326 reason_for_reset = "field" ,
@@ -304,10 +329,10 @@ def _set_note_filters_table_row(
304329
305330 # Fields are dependent on note-type
306331 note_type_cbox .currentIndexChanged .connect (
307- lambda index : self ._update_fields_cbox (field_cbox , note_type_cbox )
332+ lambda _ : self ._update_fields_cbox (field_cbox , note_type_cbox )
308333 )
309334 note_type_cbox .currentIndexChanged .connect (
310- lambda index : self ._potentially_reset_tags_note_type (
335+ lambda index : self ._potentially_reset_tags (
311336 new_index = index ,
312337 combo_box = note_type_cbox ,
313338 reason_for_reset = "note type" ,
@@ -318,7 +343,7 @@ def _set_note_filters_table_row(
318343 morphemizer_cbox = self ._setup_morphemizer_cbox (config_filter )
319344 morphemizer_cbox .setProperty ("previousIndex" , morphemizer_cbox .currentIndex ())
320345 morphemizer_cbox .currentIndexChanged .connect (
321- lambda index : self ._potentially_reset_tags_morphemizer (
346+ lambda index : self ._potentially_reset_tags (
322347 new_index = index ,
323348 combo_box = morphemizer_cbox ,
324349 reason_for_reset = "morphemizer" ,
@@ -341,7 +366,7 @@ def _set_note_filters_table_row(
341366 self .ui .note_filters_table .setItem (
342367 row ,
343368 self ._note_filter_tags_column ,
344- QTableWidgetItem ( json . dumps ( config_filter . tags )) ,
369+ tags_filter_widget ,
345370 )
346371 self .ui .note_filters_table .setCellWidget (
347372 row , self ._note_filter_field_column , field_cbox
@@ -359,64 +384,40 @@ def _set_note_filters_table_row(
359384 row , self ._note_filter_modify_column , modify_checkbox
360385 )
361386
362- def _potentially_reset_tags_note_type (
363- self , new_index : int , combo_box : QComboBox , reason_for_reset : str
364- ) -> None :
365- if self ._shown_reset_tags_note_type_warning is False :
366- did_show_warning : bool = self ._potentially_reset_tags (
367- new_index = new_index ,
368- combo_box = combo_box ,
369- reason_for_reset = reason_for_reset ,
387+ # store widgets persistently to prevent garbage collection
388+ self .widget_references_by_row .append (
389+ (
390+ note_type_cbox ,
391+ tags_filter_widget ,
392+ field_cbox ,
393+ morphemizer_cbox ,
394+ morph_priority_cbox ,
395+ read_checkbox ,
396+ modify_checkbox ,
370397 )
371- if did_show_warning :
372- self ._shown_reset_tags_note_type_warning = True
373-
374- def _potentially_reset_tags_field (
375- self , new_index : int , combo_box : QComboBox , reason_for_reset : str
376- ) -> None :
377- if self ._shown_reset_tags_field_warning is False :
378- did_show_warning : bool = self ._potentially_reset_tags (
379- new_index = new_index ,
380- combo_box = combo_box ,
381- reason_for_reset = reason_for_reset ,
382- )
383- if did_show_warning :
384- self ._shown_reset_tags_field_warning = True
385-
386- def _potentially_reset_tags_morphemizer (
387- self , new_index : int , combo_box : QComboBox , reason_for_reset : str
388- ) -> None :
389- if self ._shown_reset_tags_morphemizer_warning is False :
390- did_show_warning : bool = self ._potentially_reset_tags (
391- new_index = new_index ,
392- combo_box = combo_box ,
393- reason_for_reset = reason_for_reset ,
394- )
395- if did_show_warning :
396- self ._shown_reset_tags_morphemizer_warning = True
398+ )
397399
398400 def _potentially_reset_tags (
399401 self , new_index : int , combo_box : QComboBox , reason_for_reset : str
400- ) -> bool :
402+ ) -> None :
401403 """
402404 To prevent annoying the user, we only want to show the warning dialog once
403405 per combobox, per setting.
404-
405- Returns True if warning dialog was shown, otherwise returns False.
406406 """
407407
408- if new_index == 0 :
409- # we can ignore the "(none)" selection
410- return False
408+ if not self .reset_tags_warning_shown .get (reason_for_reset , False ):
409+ if new_index == 0 : # Ignore the "(none)" selection
410+ return
411+
412+ previous_index = combo_box .property ("previousIndex" )
413+ if previous_index == 0 : # Skip if no change
414+ return
411415
412- previous_index = combo_box .property ("previousIndex" )
413- if previous_index != 0 :
414416 if self ._want_to_reset_am_tags (reason_for_reset ):
415417 tags_and_queue_utils .reset_am_tags (parent = self ._parent )
416- return True
417418
418- combo_box . setProperty ( "previousIndex" , new_index )
419- return False
419+ self . reset_tags_warning_shown [ reason_for_reset ] = True
420+ combo_box . setProperty ( "previousIndex" , new_index )
420421
421422 def _setup_note_type_cbox (self , config_filter : AnkiMorphsConfigFilter ) -> QComboBox :
422423 note_type_cbox = QComboBox (self .ui .note_filters_table )
@@ -484,8 +485,9 @@ def _setup_morph_priority_cbox(
484485 morph_priority_cbox .setCurrentIndex (morph_priority_cbox_index )
485486 return morph_priority_cbox
486487
487- @staticmethod
488- def _update_fields_cbox (field_cbox : QComboBox , note_type_cbox : QComboBox ) -> None :
488+ def _update_fields_cbox (
489+ self , field_cbox : QComboBox , note_type_cbox : QComboBox
490+ ) -> None :
489491 """
490492 When the note type selection changes we repopulate the fields list,
491493 and we set the selected field to (none)
0 commit comments