Fix scroll jump when previewing documents with math

MathJax loads asynchronously and can alter the height of the document.
Altering the height alters the scroll.

Ensure MathJax is either unused or finished loading before reading
scroll from the preview.
github/fork/yochananmarqos/patch-1
Gonçalo Silva 2019-05-17 06:11:17 +01:00
parent a0638f6912
commit 21387ea6a7
1 changed files with 48 additions and 34 deletions

View File

@ -14,16 +14,37 @@ class WebView(WebKit2.WebView):
Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
""" """
GET_SCROLL_SCALE_JS = """ SYNC_SCROLL_SCALE_JS = """
e = document.documentElement;
e.scrollHeight > e.clientHeight ? e.scrollTop / (e.scrollHeight - e.clientHeight) : -1;
"""
SET_SCROLL_SCALE_JS = """
scale = {:.16f}; scale = {:.16f};
write = {};
// Configure MathJax.
if (typeof hasMathJax === "undefined") {{
hasMathJax = typeof MathJax !== "undefined";
if (hasMathJax) {{
MathJax.Hub.Config({{ messageStyle: "none" }});
}}
}}
// Figure out if scrollable and rendered.
e = document.documentElement; e = document.documentElement;
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; canScroll = e.scrollHeight > e.clientHeight;
""" wasRendered = typeof isRendered !== "undefined" && isRendered;
isRendered = wasRendered ||
hasMathJax && MathJax.Hub.queue.running == 0 && MathJax.Hub.queue.pending == 0;
// Write the current scroll if instructed or if it was just rendered.
if (canScroll && (write || isRendered && !wasRendered)) {{
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
}}
// Return the current scroll if scrollable and rendered, or -1.
if (canScroll && isRendered) {{
e.scrollTop / (e.scrollHeight - e.clientHeight);
}} else {{
-1;
}}
""".strip()
__gsignals__ = { __gsignals__ = {
"scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)), "scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
@ -38,11 +59,11 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.connect("destroy", self.on_destroy) self.connect("destroy", self.on_destroy)
self.scroll_scale = 0.0 self.scroll_scale = 0.0
self.pending_scroll_scale = None
self.state_loaded = False self.state_loaded = False
self.state_load_failed = False self.state_load_failed = False
self.state_discard_result = False self.state_discard_read = False
self.state_dirty = False
self.state_waiting = False self.state_waiting = False
self.timeout_id = None self.timeout_id = None
@ -51,14 +72,15 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
return self.scroll_scale return self.scroll_scale
def set_scroll_scale(self, scale): def set_scroll_scale(self, scale):
self.pending_scroll_scale = scale self.state_dirty = scale != self.scroll_scale
self.scroll_scale = scale
self.state_loop() self.state_loop()
def on_load_changed(self, _web_view, event): def on_load_changed(self, _web_view, event):
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
self.state_load_failed = False self.state_load_failed = False
self.state_discard_result = event == WebKit2.LoadEvent.STARTED and self.state_waiting self.state_discard_read = event == WebKit2.LoadEvent.STARTED and self.state_waiting
self.pending_scroll_scale = self.scroll_scale self.state_dirty = True
self.state_loop() self.state_loop()
def on_load_failed(self, _web_view, _event): def on_load_failed(self, _web_view, _event):
@ -73,23 +95,16 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.state_loaded = False self.state_loaded = False
self.state_loop() self.state_loop()
def read_scroll_scale(self): def sync_scroll_scale(self, scroll_scale, write):
self.state_waiting = True self.state_waiting = True
self.run_javascript( self.run_javascript(
self.GET_SCROLL_SCALE_JS, None, self.finish_read_scroll_scale) self.SYNC_SCROLL_SCALE_JS.format(scroll_scale, "true" if write else "false"),
None, self.finish_sync_scroll_scale)
def write_scroll_scale(self, scroll_scale): def finish_sync_scroll_scale(self, _web_view, result):
self.run_javascript(
self.SET_SCROLL_SCALE_JS.format(scroll_scale), None, None)
def finish_read_scroll_scale(self, _web_view, result):
self.state_waiting = False self.state_waiting = False
if not self.state_discard_result: result = self.run_javascript_finish(result)
result = self.run_javascript_finish(result) self.state_loop(result.get_js_value().to_double())
self.state_loop(result.get_js_value().to_double())
else:
self.state_discard_result = False
self.state_loop()
def state_loop(self, scroll_scale=None, delay=16): # 16ms ~ 60hz def state_loop(self, scroll_scale=None, delay=16): # 16ms ~ 60hz
# Remove any pending callbacks # Remove any pending callbacks
@ -98,18 +113,17 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.timeout_id = None self.timeout_id = None
# Set scroll scale if specified, and the state is not dirty # Set scroll scale if specified, and the state is not dirty
if scroll_scale not in (None, -1, self.scroll_scale): if not self.state_discard_read and scroll_scale not in (None, -1, self.scroll_scale):
self.scroll_scale = scroll_scale self.scroll_scale = scroll_scale
self.emit("scroll-scale-changed", self.scroll_scale) self.emit("scroll-scale-changed", self.scroll_scale)
else:
self.state_discard_read = False
# Handle the current state # Handle the current state
if not self.state_loaded or self.state_load_failed or self.state_waiting: if not self.state_loaded or self.state_load_failed or self.state_waiting:
return return
if self.pending_scroll_scale: elif self.state_dirty or delay == 0:
self.write_scroll_scale(self.pending_scroll_scale) self.sync_scroll_scale(self.scroll_scale, self.state_dirty)
self.pending_scroll_scale = None self.state_dirty = False
self.read_scroll_scale()
elif delay > 0:
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
else: else:
self.read_scroll_scale() self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)