Anki/qt/aqt
Damien Elmes db5117ce1a fix webviews sometimes failing to load, by enabling cache
Fixes #983

This has been a long-standing issue that was infrequent enough on
developer machines that we weren't able to get to the bottom of it before
now. As luck would have it, the new ARM build had just the right timing
for this to occur every few invocations, and I was able to narrow it down
to the call that turns off the cache.

We don't really want the cache, so this is not a great solution. But I
ran into trouble when trying to figure out a better solution:

- Calling setHttpCacheType() earlier (eg immediately after creating the
page) fails.
- It even fails if you attempt to change the setting in the shared
default profile before any webviews are loaded:

```
    def setupMainWindow(self) -> None:
        QWebEngineProfile.defaultProfile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache)
```

- Creating a profile separately, and passing it into QWebEnginePage()
does work. But it introduces a warning each time a webview is deallocated,
and I fear it may lead to crashes:

```
Release of profile requested but WebEnginePage still not deleted. Expect troubles !
```

I tried various combinations of parents for the profile and page, and
turning web._page into an unretained property, but could not figure it out.
Some Googling pulls up a bunch of other people who seem to have hit similar
issues with PyQt. If anyone has any ideas, they'd be welcome; in the mean
time, I guess we're better off using up some of the user's disk
space than sometimes failing to load.

The profile-in-advance code I tried is below:

```
diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py
index 1c96112d8..4f3e91284 100644
--- a/qt/aqt/webview.py
+++ b/qt/aqt/webview.py
@@ -23,9 +23,49 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
 BridgeCommandHandler = Callable[[str], Any]

+def _create_profile(parent: QObject) -> QWebEngineProfile:
+    qwebchannel = ":/qtwebchannel/qwebchannel.js"
+    jsfile = QFile(qwebchannel)
+    if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
+        print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
+    jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
+    jsfile.close()
+
+    script = QWebEngineScript()
+    script.setSourceCode(
+        jstext
+        + """
+            var pycmd, bridgeCommand;
+            new QWebChannel(qt.webChannelTransport, function(channel) {
+                bridgeCommand = pycmd = function (arg, cb) {
+                    var resultCB = function (res) {
+                        // pass result back to user-provided callback
+                        if (cb) {
+                            cb(JSON.parse(res));
+                        }
+                    }
+
+                    channel.objects.py.cmd(arg, resultCB);
+                    return false;
+                }
+                pycmd("domDone");
+            });
+        """
+    )
+    script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
+    script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
+    script.setRunsOnSubFrames(False)
+
+    profile = QWebEngineProfile(parent)
+    profile.setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache)
+    profile.scripts().insert(script)
+    return profile
+
+
 class AnkiWebPage(QWebEnginePage):
-    def __init__(self, onBridgeCmd: BridgeCommandHandler) -> None:
-        QWebEnginePage.__init__(self)
+    def __init__(self, onBridgeCmd: BridgeCommandHandler, parent: QObject) -> None:
+        profile = _create_profile(parent)
+        QWebEnginePage.__init__(self, profile, parent)
         self._onBridgeCmd = onBridgeCmd
         self._setupBridge()
         self.open_links_externally = True
@@ -46,39 +86,6 @@ class AnkiWebPage(QWebEnginePage):
         self._channel.registerObject("py", self._bridge)
         self.setWebChannel(self._channel)

-        qwebchannel = ":/qtwebchannel/qwebchannel.js"
-        jsfile = QFile(qwebchannel)
-        if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly):
-            print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr)
-        jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8")
-        jsfile.close()
-
-        script = QWebEngineScript()
-        script.setSourceCode(
-            jstext
-            + """
-            var pycmd, bridgeCommand;
-            new QWebChannel(qt.webChannelTransport, function(channel) {
-                bridgeCommand = pycmd = function (arg, cb) {
-                    var resultCB = function (res) {
-                        // pass result back to user-provided callback
-                        if (cb) {
-                            cb(JSON.parse(res));
-                        }
-                    }
-
-                    channel.objects.py.cmd(arg, resultCB);
-                    return false;
-                }
-                pycmd("domDone");
-            });
-        """
-        )
-        script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
-        script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
-        script.setRunsOnSubFrames(False)
-        self.profile().scripts().insert(script)
-
     def javaScriptConsoleMessage(
         self,
         level: QWebEnginePage.JavaScriptConsoleMessageLevel,
@@ -228,7 +235,7 @@ class AnkiWebView(QWebEngineView):
     ) -> None:
         QWebEngineView.__init__(self, parent=parent)
         self.set_title(title)
-        self._page = AnkiWebPage(self._onBridgeCmd)
+        self._page = AnkiWebPage(self._onBridgeCmd, self)
         self._page.setBackgroundColor(
             self.get_window_bg_color(theme_manager.night_mode)
@@ -242,7 +249,6 @@ class AnkiWebView(QWebEngineView):
         self.requiresCol = True
         self.setPage(self._page)

-        self._page.profile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache)
         self.resetHandlers()
         self.allowDrops = False
         self._filterSet = False
```
2021-12-08 21:20:39 +10:00
..
browser Add browser action to create note copy (#1535) 2021-12-08 08:40:48 +10:00
data Fix custom CSS not being applied to scrollbars in night mode (#1525) 2021-12-05 08:20:42 +10:00
forms Add browser action to create note copy (#1535) 2021-12-08 08:40:48 +10:00
operations add unbury_cards() op 2021-12-08 09:44:47 +10:00
qt tweak qrc deprecation warning 2021-12-04 14:35:49 +10:00
__init__.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
_macos_helper.py implement a basic native macOS audio recorder 2021-12-07 18:48:24 +10:00
about.py Update about.py (#1533) 2021-12-07 09:01:30 +10:00
addcards.py Add browser action to create note copy (#1535) 2021-12-08 08:40:48 +10:00
addons.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
BUILD.bazel improve PyQt install 2021-10-23 10:56:17 +10:00
changenotetype.py Fix memory leak in AnkiWebView (#1510) 2021-11-29 12:31:37 +10:00
clayout.py Allow <audio> to play without user interaction in accordance to autoplay setting v2 (#1539) 2021-12-08 08:08:56 +10:00
colors.py move remaining Filter button items into sidebar 2021-02-05 18:58:22 +10:00
customstudy.py PEP8 pylib (#1443) 2021-10-22 20:39:49 +10:00
dbcheck.py PEP8 collection.py 2021-06-27 15:12:22 +10:00
deckbrowser.py retire the v1 scheduler 2021-11-24 14:12:56 +10:00
deckchooser.py Fix chooser label not being updated when current notetype/deck renamed (#1452) 2021-10-25 13:23:06 +10:00
deckconf.py convert invariant assertions to if statements 2021-11-25 17:47:50 +10:00
deckdescription.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
deckoptions.py work around 'which deck would you like' sticking around on Qt6/macOS 2021-12-06 18:24:38 +10:00
editcurrent.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
editor.py Fix bug(s) caused by deleting a notetype currently selected in AddCards (#1514) 2021-12-04 07:55:22 +10:00
emptycards.py Fix memory leak in AnkiWebView (#1510) 2021-11-29 12:31:37 +10:00
errors.py catch all recording errors; mention permission 2021-06-02 12:20:40 +10:00
exporting.py fix check for user exporting into data folder on Windows 2021-11-25 09:30:17 +10:00
fields.py Editor Field Descriptions (#1476) 2021-11-06 09:42:48 +10:00
filtered_deck.py convert invariant assertions to if statements 2021-11-25 17:47:50 +10:00
flags.py use Qt search path instead of resource system 2021-10-12 16:17:08 +10:00
gui_hooks.py fix qt/ pylints 2021-01-07 16:21:50 +10:00
hooks_gen.py symlink generated .py/.pyi into tree to fix Python code completion 2020-12-16 11:36:42 +10:00
importing.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
legacy.py run pyupgrade over codebase [python upgrade required] 2021-10-04 15:05:48 +10:00
main.py convert invariant assertions to if statements 2021-11-25 17:47:50 +10:00
mediacheck.py fix media trash throttling; decrease delay 2021-12-04 09:10:31 +10:00
mediasrv.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
mediasync.py PEP8 for rest of pylib (#1451) 2021-10-25 14:50:13 +10:00
modelchooser.py run pyupgrade over codebase [python upgrade required] 2021-10-04 15:05:48 +10:00
models.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
mpv.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
notetypechooser.py Remove all_names in notetypechooser (#1501) 2021-11-23 10:27:57 +10:00
overview.py avoid duplicate work in overview screen 2021-11-25 09:01:02 +10:00
package.py ignore a spurious mypy error 2021-10-28 19:19:37 +10:00
pinnedmodules.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
preferences.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
profiles.py convert invariant assertions to if statements 2021-11-25 17:47:50 +10:00
progress.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
py.typed mark anki and aqt modules as having typing info 2020-01-13 13:03:37 +10:00
reviewer.py Allow <audio> to play without user interaction in accordance to autoplay setting v2 (#1539) 2021-12-08 08:08:56 +10:00
schema_change_tracker.py add a bunch of return types 2021-02-01 23:53:23 +10:00
sound.py implement a basic native macOS audio recorder 2021-12-07 18:48:24 +10:00
stats.py Fix memory leak in AnkiWebView (#1510) 2021-11-29 12:31:37 +10:00
studydeck.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
switch.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
sync.py convert invariant assertions to if statements 2021-11-25 17:47:50 +10:00
tagedit.py switch to new-style PyQt scoped enums and Qt6 2021-10-15 12:57:19 +10:00
taglimit.py PEP8 for rest of pylib (#1451) 2021-10-25 14:50:13 +10:00
taskman.py avoid importing directly from PyQt5 where possible 2021-10-15 10:47:53 +10:00
theme.py implement a basic native macOS audio recorder 2021-12-07 18:48:24 +10:00
toolbar.py run pyupgrade over codebase [python upgrade required] 2021-10-04 15:05:48 +10:00
tts.py update platform checks (eg isWin -> is_win) + devMode 2021-11-25 09:06:16 +10:00
undo.py fix redo menu item showing undo text 2021-08-04 10:28:45 +10:00
update.py PEP8 for rest of pylib (#1451) 2021-10-25 14:50:13 +10:00
utils.py Add browser action to create note copy (#1535) 2021-12-08 08:40:48 +10:00
webview.py fix webviews sometimes failing to load, by enabling cache 2021-12-08 21:20:39 +10:00
wheel_description.txt move aqt_data into source folder; implement wheel building 2020-11-04 12:14:03 +10:00
winpaths.py run pyupgrade over codebase [python upgrade required] 2021-10-04 15:05:48 +10:00