apostrophe/uberwriter/web_view_scroller.py

135 lines
4.3 KiB
Python

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2, GLib
class WebViewScroller:
"""Provides read/write scrolling functionality to a WebView.
It does so using JavaScript, by continuously monitoring it while loaded and focused.
The alternative is using a WebExtension and C-bindings (see reference), but that is more
complicated implementation-wise, and build-wise, at least we start building with Meson.
Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
"""
SETUP_SROLL_SCALE_JS = """
const e = document.documentElement;
function get_scroll_scale() {
if (document.readyState !== "complete" || e.clientHeight === 0) {
return null;
} else if (e.scrollHeight <= e.clientHeight) {
return 0;
} else {
return e.scrollTop / (e.scrollHeight - e.clientHeight);
}
}
function set_scroll_scale(scale) {
if (document.readyState !== "complete") {
window.addEventListener("load", function() { set_scroll_scale(scale); });
} else if (e.clientHeight === 0) {
window.addEventListener("resize", function() { set_scroll_scale(scale); });
} else if (e.scrollHeight > e.clientHeight) {
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
}
return get_scroll_scale();
}
get_scroll_scale();
""".strip()
GET_SCROLL_SCALE_JS = "get_scroll_scale();"
SET_SCROLL_SCALE_JS = "set_scroll_scale({:.16f});"
def __init__(self, webview):
super().__init__()
self.webview = webview
self.webview.connect("focus-in-event", self.on_focus_changed)
self.webview.connect("focus-out-event", self.on_focus_changed)
self.webview.connect("load-changed", self.on_load_changed)
self.webview.connect("destroy", self.on_destroy)
self.scroll_scale = 0
self.state_loaded = False
self.state_setup = False
self.state_focused = True
self.state_dirty = False
self.state_waiting = False
self.timeout_id = None
def get_scroll_scale(self):
return self.scroll_scale
def set_scroll_scale(self, scale):
self.scroll_scale = scale
self.state_dirty = True
self.state_loop()
def on_focus_changed(self, _webview, event):
self.state_focused = event.in_
self.state_loop()
def on_load_changed(self, _webview, event):
self.state_loaded = event == WebKit2.LoadEvent.FINISHED
self.state_loop()
def on_destroy(self, _widget):
self.state_loaded = False
self.state_focused = False
self.state_loop()
self.webview = None
def setup_scroll_state(self):
self.state_waiting = True
self.state_setup = True
self.webview.run_javascript(
self.SETUP_SROLL_SCALE_JS, None, self.sync_scroll_scale)
def read_scroll_scale(self):
self.state_waiting = True
self.webview.run_javascript(
self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale)
def write_scroll_scale(self):
self.state_waiting = True
self.state_dirty = False
self.webview.run_javascript(
self.SET_SCROLL_SCALE_JS.format(self.scroll_scale), None, self.sync_scroll_scale)
def sync_scroll_scale(self, _webview, result):
self.state_waiting = False
result = self.webview.run_javascript_finish(result)
self.state_loop(result.get_js_value().to_double())
def state_loop(self, scroll_scale=None, read_delay=500):
# 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 scroll_scale is not None and not self.state_dirty:
self.scroll_scale = scroll_scale
# Handle the current state
if self.state_waiting:
return
elif not self.state_loaded:
return
elif not self.state_setup:
self.setup_scroll_state()
elif self.state_dirty:
self.write_scroll_scale()
elif self.state_focused:
if read_delay > 0:
self.timeout_id = GLib.timeout_add(read_delay, self.state_loop, None, 0)
else:
self.read_scroll_scale()