Skip to content

Commit 86cded7

Browse files
committed
fixed anki sporadically crashing (#321)
1 parent abb4f54 commit 86cded7

File tree

4 files changed

+75
-69
lines changed

4 files changed

+75
-69
lines changed

ankimorphs/ankimorphs_globals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
# Semantic Versioning https://semver.org/
8-
__version__ = "4.0.1"
8+
__version__ = "4.0.2"
99

1010
DEV_MODE: bool = False
1111

ankimorphs/generators/generators_window.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
from functools import partial
45
from pathlib import Path
56
from typing import Callable
67

@@ -9,6 +10,7 @@
910
from aqt.operations import QueryOp
1011
from aqt.qt import ( # pylint:disable=no-name-in-module
1112
QAbstractItemView,
13+
QCheckBox,
1214
QDialog,
1315
QDir,
1416
QFileDialog,
@@ -53,6 +55,7 @@ def __init__(
5355
self.am_extra_settings = AnkiMorphsExtraSettings()
5456
self.am_extra_settings.beginGroup(extra_settings_keys.Dialogs.GENERATORS_WINDOW)
5557

58+
self.checkboxes: list[QCheckBox] = []
5659
self._input_files: list[Path] = []
5760
self._morphemizers: list[Morphemizer] = morphemizer_utils.get_all_morphemizers()
5861
self._setup_morphemizers()
@@ -129,7 +132,7 @@ def _setup_input_field(self) -> None:
129132
self.ui.inputDirLineEdit.setText(stored_input_dir)
130133

131134
self.ui.inputDirLineEdit.textEdited.connect(
132-
lambda: self.ui.loadFilesPushButton.setEnabled(True)
135+
partial(self.ui.loadFilesPushButton.setEnabled, True)
133136
)
134137

135138
def _setup_geometry(self) -> None:
@@ -148,7 +151,7 @@ def _setup_checkboxes(self) -> None:
148151
self._setup_preprocess_checkboxes()
149152

150153
def _setup_file_extension_checkboxes(self) -> None:
151-
checkboxes = [
154+
self.checkboxes = [
152155
self.ui.assFilesCheckBox,
153156
self.ui.epubFilesCheckBox,
154157
self.ui.htmlFilesCheckBox,
@@ -194,9 +197,9 @@ def _setup_file_extension_checkboxes(self) -> None:
194197
self.ui.txtFilesCheckBox.setChecked(stored_txt_checkbox)
195198
self.ui.vttFilesCheckBox.setChecked(stored_vtt_checkbox)
196199

197-
for checkbox in checkboxes:
200+
for checkbox in self.checkboxes:
198201
checkbox.clicked.connect(
199-
lambda: self.ui.loadFilesPushButton.setEnabled(True)
202+
partial(self.ui.loadFilesPushButton.setEnabled, True)
200203
)
201204

202205
def _setup_preprocess_checkboxes(self) -> None:

ankimorphs/settings/settings_note_filters_tab.py

Lines changed: 65 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

docs/src/contributors.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Matt Vs Japan, mortii, Vilhelm-Ian, cocowash, xuiqzy, xofm31, zeroeightysix, wol
1717
Vilhelm-Ian, CodeWithMa, ashprice, aleksejrs, HQYang1979, soliviantar, buster-blue, rymiel, BorisNA, wrinkledeth,
1818
cocowash, asayake-b5, quietmansoath, MichaelPetre, xofm31, knoebelja, xuiqzy, Jcuhfehl, fuquasteve, pallas42, syfgk,
1919
jahnke, jsteel44, iwouldrathernotusegithub, tanhoaian01, drkthomp, Kirchheim, zeroeightysix, Gardengul, wolearyc,
20-
Pedrubik2000, RyanMcEntire, BobvanSchendel, khanguyenwk, buqamura, Rct567, rwmpelstilzchen, bie-zheng, IncontinentCell.
20+
Pedrubik2000, RyanMcEntire, BobvanSchendel, khanguyenwk, buqamura, Rct567, rwmpelstilzchen, bie-zheng, IncontinentCell,
21+
mdraves91.
2122

2223
### MorphMan (v5.0-qt6-alpha.1)
2324

0 commit comments

Comments
 (0)