forked from Mirrors/apostrophe
146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
import webbrowser
|
|
|
|
import gi
|
|
|
|
gi.require_version('WebKit2', '4.0')
|
|
from gi.repository import WebKit2, GLib, GObject
|
|
|
|
|
|
class PreviewWebView(WebKit2.WebView):
|
|
"""A WebView that provides read/write access to scroll.
|
|
|
|
It does so using JavaScript, by continuously monitoring it while loaded.
|
|
The alternative is using a WebExtension and C-bindings (see reference), but that is more
|
|
complicated implementation-wise, as well as build-wise until we start building with Meson.
|
|
|
|
Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
|
|
"""
|
|
|
|
SYNC_SCROLL_SCALE_JS = """
|
|
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;
|
|
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__ = {
|
|
"scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
|
}
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.connect("size-allocate", self.on_size_allocate)
|
|
self.connect("decide-policy", self.on_decide_policy)
|
|
self.connect("load-changed", self.on_load_changed)
|
|
self.connect("load-failed", self.on_load_failed)
|
|
self.connect("destroy", self.on_destroy)
|
|
|
|
self.props.expand = True
|
|
|
|
self.scroll_scale = -1
|
|
|
|
self.state_loaded = False
|
|
self.state_load_failed = False
|
|
self.state_discard_read = False
|
|
self.state_dirty = False
|
|
self.state_waiting = False
|
|
|
|
self.timeout_id = None
|
|
|
|
def can_scroll(self):
|
|
return self.scroll_scale != -1
|
|
|
|
def get_scroll_scale(self):
|
|
return self.scroll_scale
|
|
|
|
def set_scroll_scale(self, scale):
|
|
self.state_dirty = scale != self.scroll_scale
|
|
self.scroll_scale = scale
|
|
self.state_loop()
|
|
|
|
def on_size_allocate(self, *_):
|
|
self.set_scroll_scale(self.scroll_scale)
|
|
|
|
def on_decide_policy(self, _web_view, decision, decision_type):
|
|
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION and \
|
|
decision.get_navigation_action().is_user_gesture():
|
|
webbrowser.open(decision.get_request().get_uri())
|
|
return True
|
|
return False
|
|
|
|
def on_load_changed(self, _web_view, event):
|
|
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
|
|
self.state_load_failed = False
|
|
self.state_discard_read = event == WebKit2.LoadEvent.STARTED and self.state_waiting
|
|
self.state_dirty = True
|
|
self.state_loop()
|
|
|
|
def on_load_failed(self, _web_view, _event):
|
|
self.state_loaded = False
|
|
self.state_load_failed = True
|
|
self.state_loop()
|
|
|
|
def on_destroy(self, _widget):
|
|
self.state_loaded = False
|
|
self.state_loop()
|
|
|
|
def sync_scroll_scale(self, scroll_scale, write):
|
|
self.state_waiting = True
|
|
self.run_javascript(
|
|
self.SYNC_SCROLL_SCALE_JS.format(scroll_scale, "true" if write else "false"),
|
|
None, self.finish_sync_scroll_scale)
|
|
|
|
def finish_sync_scroll_scale(self, _web_view, result):
|
|
self.state_waiting = False
|
|
result = self.run_javascript_finish(result)
|
|
self.state_loop(result.get_js_value().to_double())
|
|
|
|
def state_loop(self, scroll_scale=None, delay=16): # 16ms ~ 60hz
|
|
# Remove any pending callbacks
|
|
if self.timeout_id:
|
|
GLib.source_remove(self.timeout_id)
|
|
self.timeout_id = None
|
|
|
|
# Set scroll scale if specified, and the state is not dirty
|
|
if not self.state_discard_read and scroll_scale not in (None, self.scroll_scale):
|
|
self.scroll_scale = scroll_scale
|
|
if self.scroll_scale != -1:
|
|
self.emit("scroll-scale-changed", self.scroll_scale)
|
|
self.state_discard_read = False
|
|
|
|
# Handle the current state
|
|
if not self.state_loaded or self.state_load_failed or self.state_waiting:
|
|
return
|
|
elif self.state_dirty or delay == 0:
|
|
self.sync_scroll_scale(self.scroll_scale, self.state_dirty)
|
|
self.state_dirty = False
|
|
else:
|
|
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
|