forked from Mirrors/apostrophe
Improve side-by-side experience
Includes multiple improvements to scroll syncing, preview re-render, layout separation, etcgithub/fork/yochananmarqos/patch-1
parent
5e770510ee
commit
2cb161307c
|
@ -59,7 +59,7 @@ body {
|
|||
word-wrap: break-word;
|
||||
max-width: 980px;
|
||||
margin: auto;
|
||||
padding: 2em;
|
||||
padding: 4em;
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkImage" id="pan-down">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">pan-down-symbolic</property>
|
||||
<property name="icon_size">2</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="preview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRevealer" id="preview_mode_revealer">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="transition_type">crossfade</property>
|
||||
<property name="transition_duration">750</property>
|
||||
<property name="reveal_child">True</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="preview_mode_button">
|
||||
<property name="label" translatable="yes">Full-Width</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Switch Preview Mode</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="image">pan-down</property>
|
||||
<property name="image_position">right</property>
|
||||
<property name="always_show_image">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
|
@ -28,9 +28,11 @@ from urllib.parse import unquote
|
|||
|
||||
import gi
|
||||
|
||||
from uberwriter.text_view_markup_handler import MarkupHandler
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
|
||||
from uberwriter import latex_to_PNG, text_view_markup_handler
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf
|
||||
from uberwriter import latex_to_PNG
|
||||
from uberwriter.settings import Settings
|
||||
|
||||
from uberwriter.fix_table import FixTable
|
||||
|
@ -360,8 +362,8 @@ class InlinePreview:
|
|||
|
||||
text = self.text_buffer.get_text(start_iter, end_iter, False)
|
||||
|
||||
math = text_view_markup_handler.regex["MATH"]
|
||||
link = text_view_markup_handler.regex["LINK"]
|
||||
math = MarkupHandler.regex["MATH"]
|
||||
link = MarkupHandler.regex["LINK"]
|
||||
|
||||
footnote = re.compile(r'\[\^([^\s]+?)\]')
|
||||
image = re.compile(r"!\[(.*?)\]\((.+?)\)")
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from uberwriter import helpers
|
||||
from uberwriter.theme import Theme
|
||||
|
||||
|
||||
class PreviewConverter:
|
||||
"""Converts markdown to html using a background thread."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.queue = Queue()
|
||||
worker = Thread(target=self.__do_convert, name="preview-converter")
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
|
||||
def convert(self, text, callback, *user_data):
|
||||
"""Converts text to html, calling callback when done.
|
||||
|
||||
The callback argument contains the result."""
|
||||
|
||||
self.queue.put((text, callback, user_data))
|
||||
|
||||
def stop(self):
|
||||
"""Stops the background worker. PreviewConverter shouldn't be used after this."""
|
||||
|
||||
self.queue.put((None, None))
|
||||
|
||||
def __do_convert(self):
|
||||
while True:
|
||||
while True:
|
||||
(text, callback, user_data) = self.queue.get()
|
||||
if text is None and callback is None:
|
||||
return
|
||||
if self.queue.empty():
|
||||
break
|
||||
|
||||
args = ['--standalone',
|
||||
'--mathjax',
|
||||
'--css=' + Theme.get_current().web_css_path,
|
||||
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
|
||||
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
|
||||
text = helpers.pandoc_convert(text, to="html5", args=args)
|
||||
|
||||
GLib.idle_add(callback, text, *user_data)
|
|
@ -0,0 +1,147 @@
|
|||
import math
|
||||
import webbrowser
|
||||
from enum import auto, IntEnum
|
||||
|
||||
import gi
|
||||
|
||||
from uberwriter.helpers import get_builder
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2
|
||||
|
||||
from uberwriter.preview_converter import PreviewConverter
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.web_view import WebView
|
||||
|
||||
|
||||
class Step(IntEnum):
|
||||
CONVERT_HTML = auto()
|
||||
LOAD_WEBVIEW = auto()
|
||||
RENDER = auto()
|
||||
|
||||
|
||||
class Previewer:
|
||||
def __init__(self, content, editor, text_view):
|
||||
self.content = content
|
||||
self.editor = editor
|
||||
self.text_view = text_view
|
||||
|
||||
self.web_view = None
|
||||
self.web_view_pending_html = None
|
||||
|
||||
builder = get_builder("Preview")
|
||||
self.preview = builder.get_object("preview")
|
||||
self.preview_mode_button = builder.get_object("preview_mode_button")
|
||||
self.preview_mode_button.get_style_context().add_class('toggle-button')
|
||||
|
||||
self.preview_converter = PreviewConverter()
|
||||
|
||||
self.web_scroll_handler_id = None
|
||||
self.text_scroll_handler_id = None
|
||||
self.text_changed_handler_id = None
|
||||
|
||||
self.settings = Settings.new()
|
||||
|
||||
self.loading = False
|
||||
self.showing = False
|
||||
|
||||
def show(self):
|
||||
self.__show()
|
||||
|
||||
def reload(self):
|
||||
if self.showing:
|
||||
self.show()
|
||||
|
||||
def __show(self, html=None, step=Step.CONVERT_HTML):
|
||||
if step == Step.CONVERT_HTML:
|
||||
# First step: convert text to HTML.
|
||||
buf = self.text_view.get_buffer()
|
||||
self.preview_converter.convert(
|
||||
buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False),
|
||||
self.__show, Step.LOAD_WEBVIEW)
|
||||
|
||||
elif step == Step.LOAD_WEBVIEW:
|
||||
# Second step: load HTML.
|
||||
self.loading = True
|
||||
|
||||
if not self.web_view:
|
||||
self.web_view = WebView()
|
||||
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.web_view.connect("load-changed", self.on_load_changed)
|
||||
|
||||
# All links will be opened in default browser, but local files are opened in apps.
|
||||
self.web_view.connect("decide-policy", self.on_click_link)
|
||||
|
||||
if self.web_view.is_loading():
|
||||
self.web_view_pending_html = html
|
||||
else:
|
||||
self.web_view.load_html(html, 'file://localhost/')
|
||||
|
||||
elif step == Step.RENDER:
|
||||
# Last and one-time step: show the web view.
|
||||
if self.showing:
|
||||
return
|
||||
self.showing = True
|
||||
|
||||
if self.settings.get_boolean("preview-side-by-side"):
|
||||
self.content.set_size_request(self.text_view.get_min_width() * 2, -1)
|
||||
self.web_view.set_scroll_scale(self.text_view.get_scroll_scale())
|
||||
self.web_scroll_handler_id = \
|
||||
self.web_view.connect("scroll-scale-changed", self.on_web_view_scrolled)
|
||||
self.text_scroll_handler_id = \
|
||||
self.text_view.connect("scroll-scale-changed", self.on_text_view_scrolled)
|
||||
self.text_changed_handler_id = \
|
||||
self.text_view.get_buffer().connect("changed", self.__show)
|
||||
else:
|
||||
self.content.remove(self.editor)
|
||||
|
||||
self.preview.pack_start(self.web_view, True, True, 0)
|
||||
self.content.add(self.preview)
|
||||
self.web_view.show()
|
||||
|
||||
def hide(self):
|
||||
if self.showing:
|
||||
self.showing = False
|
||||
|
||||
if self.settings.get_boolean("preview-side-by-side"):
|
||||
self.content.set_size_request(-1, -1)
|
||||
self.web_view.disconnect(self.web_scroll_handler_id)
|
||||
self.text_view.disconnect(self.text_scroll_handler_id)
|
||||
self.text_view.get_buffer().disconnect(self.text_changed_handler_id)
|
||||
else:
|
||||
self.content.add(self.editor)
|
||||
|
||||
self.content.remove(self.preview)
|
||||
self.preview.remove(self.web_view)
|
||||
|
||||
if self.loading:
|
||||
self.loading = False
|
||||
|
||||
self.web_view.destroy()
|
||||
self.web_view = None
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
if event == WebKit2.LoadEvent.FINISHED:
|
||||
self.loading = False
|
||||
if self.web_view_pending_html:
|
||||
self.__show(html=self.web_view_pending_html, step=Step.LOAD_WEBVIEW)
|
||||
self.web_view_pending_html = None
|
||||
else:
|
||||
self.__show(step=Step.RENDER)
|
||||
|
||||
def on_text_view_scrolled(self, _text_view, scale):
|
||||
if not math.isclose(scale, self.web_view.get_scroll_scale(), rel_tol=1e-5):
|
||||
self.web_view.set_scroll_scale(scale)
|
||||
|
||||
def on_web_view_scrolled(self, _web_view, scale):
|
||||
if not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5):
|
||||
self.text_view.set_scroll_scale(scale)
|
||||
|
||||
@staticmethod
|
||||
def on_click_link(web_view, decision, _decision_type):
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
webbrowser.open(web_view.get_uri())
|
||||
decision.ignore()
|
||||
return True
|
|
@ -33,32 +33,32 @@ class SearchAndReplace:
|
|||
uberwriter
|
||||
"""
|
||||
|
||||
def __init__(self, parentwindow, textview):
|
||||
def __init__(self, parentwindow, textview, builder):
|
||||
self.parentwindow = parentwindow
|
||||
self.textview = textview
|
||||
self.textbuffer = textview.get_buffer()
|
||||
|
||||
self.box = parentwindow.builder.get_object("searchbar_placeholder")
|
||||
self.box = builder.get_object("searchbar_placeholder")
|
||||
self.box.set_reveal_child(False)
|
||||
self.searchbar = parentwindow.builder.get_object("searchbar")
|
||||
self.searchentry = parentwindow.builder.get_object("searchentrybox")
|
||||
self.searchbar = builder.get_object("searchbar")
|
||||
self.searchentry = builder.get_object("searchentrybox")
|
||||
self.searchentry.connect('changed', self.search)
|
||||
self.searchentry.connect('activate', self.scrolltonext)
|
||||
self.searchentry.connect('key-press-event', self.key_pressed)
|
||||
|
||||
self.open_replace_button = parentwindow.builder.get_object("replace")
|
||||
self.open_replace_button = builder.get_object("replace")
|
||||
self.open_replace_button.connect("toggled", self.toggle_replace)
|
||||
|
||||
self.nextbutton = parentwindow.builder.get_object("next_result")
|
||||
self.prevbutton = parentwindow.builder.get_object("previous_result")
|
||||
self.regexbutton = parentwindow.builder.get_object("regex")
|
||||
self.casesensitivebutton = parentwindow.builder.get_object("case_sensitive")
|
||||
self.nextbutton = builder.get_object("next_result")
|
||||
self.prevbutton = builder.get_object("previous_result")
|
||||
self.regexbutton = builder.get_object("regex")
|
||||
self.casesensitivebutton = builder.get_object("case_sensitive")
|
||||
|
||||
self.replacebox = parentwindow.builder.get_object("replace_placeholder")
|
||||
self.replacebox = builder.get_object("replace_placeholder")
|
||||
self.replacebox.set_reveal_child(False)
|
||||
self.replace_one_button = parentwindow.builder.get_object("replace_one")
|
||||
self.replace_all_button = parentwindow.builder.get_object("replace_all")
|
||||
self.replaceentry = parentwindow.builder.get_object("replaceentrybox")
|
||||
self.replace_one_button = builder.get_object("replace_one")
|
||||
self.replace_all_button = builder.get_object("replace_all")
|
||||
self.replaceentry = builder.get_object("replaceentrybox")
|
||||
|
||||
self.replace_all_button.connect('clicked', self.replace_all)
|
||||
self.replace_one_button.connect('clicked', self.replace_clicked)
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import math
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
from gi.repository import GLib, Gio, Gtk
|
||||
|
||||
from uberwriter import helpers
|
||||
from uberwriter.helpers import get_builder
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.stats_counter import StatsCounter
|
||||
|
||||
|
|
|
@ -37,7 +37,8 @@ class TextView(Gtk.TextView):
|
|||
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'undo': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'redo': (GObject.SignalFlags.ACTION, None, ())
|
||||
'redo': (GObject.SignalFlags.ACTION, None, ()),
|
||||
'scroll-scale-changed': (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
font_sizes = [18, 17, 16, 15, 14] # Must match CSS selectors in gtk/base.css
|
||||
|
@ -136,6 +137,8 @@ class TextView(Gtk.TextView):
|
|||
if parent:
|
||||
parent.set_size_request(self.get_min_width(), 500)
|
||||
self.scroller = TextViewScroller(self, parent)
|
||||
parent.get_vadjustment().connect("changed", self.on_scroll_scale_changed)
|
||||
parent.get_vadjustment().connect("value-changed", self.on_scroll_scale_changed)
|
||||
else:
|
||||
self.scroller = None
|
||||
|
||||
|
@ -152,6 +155,9 @@ class TextView(Gtk.TextView):
|
|||
self.markup.apply()
|
||||
return False
|
||||
|
||||
def on_scroll_scale_changed(self, *_):
|
||||
self.emit("scroll-scale-changed", self.get_scroll_scale())
|
||||
|
||||
def set_focus_mode(self, focus_mode):
|
||||
"""Toggle focus mode.
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import gi
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2, GLib, GObject
|
||||
|
||||
|
||||
class WebView(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
|
||||
"""
|
||||
|
||||
GET_SCROLL_SCALE_JS = """
|
||||
e = document.documentElement;
|
||||
e.scrollHeight > e.clientHeight ? e.scrollTop / (e.scrollHeight - e.clientHeight) : 0;
|
||||
"""
|
||||
|
||||
SET_SCROLL_SCALE_JS = """
|
||||
scale = {:.16f};
|
||||
e = document.documentElement;
|
||||
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
|
||||
"""
|
||||
|
||||
__gsignals__ = {
|
||||
"scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.connect("load-changed", self.on_load_changed)
|
||||
self.connect("load-failed", self.on_load_failed)
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
self.scroll_scale = 0.0
|
||||
self.pending_scroll_scale = None
|
||||
|
||||
self.state_loaded = False
|
||||
self.state_load_failed = 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.pending_scroll_scale = scale
|
||||
self.state_loop()
|
||||
|
||||
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.pending_scroll_scale = self.scroll_scale
|
||||
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 read_scroll_scale(self):
|
||||
self.state_waiting = True
|
||||
self.run_javascript(
|
||||
self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale)
|
||||
|
||||
def write_scroll_scale(self, scroll_scale):
|
||||
self.run_javascript(
|
||||
self.SET_SCROLL_SCALE_JS.format(scroll_scale), None, None)
|
||||
|
||||
def 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 scroll_scale not in (None, self.scroll_scale):
|
||||
self.scroll_scale = scroll_scale
|
||||
self.emit("scroll-scale-changed", self.scroll_scale)
|
||||
|
||||
# Handle the current state
|
||||
if not self.state_loaded or self.state_load_failed or self.state_waiting:
|
||||
return
|
||||
if self.pending_scroll_scale:
|
||||
self.write_scroll_scale(self.pending_scroll_scale)
|
||||
self.pending_scroll_scale = None
|
||||
if delay > 0:
|
||||
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
|
||||
else:
|
||||
self.read_scroll_scale()
|
|
@ -1,134 +0,0 @@
|
|||
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()
|
||||
|
|
@ -19,20 +19,17 @@ import locale
|
|||
import logging
|
||||
import os
|
||||
import urllib
|
||||
import webbrowser
|
||||
from gettext import gettext as _
|
||||
|
||||
import gi
|
||||
|
||||
from uberwriter.export_dialog import Export
|
||||
from uberwriter.previewer import Previewer
|
||||
from uberwriter.stats_handler import StatsHandler
|
||||
from uberwriter.text_view import TextView
|
||||
from uberwriter.web_view_scroller import WebViewScroller
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('WebKit2', '4.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
|
||||
from gi.repository import WebKit2 as WebKit
|
||||
|
||||
import cairo
|
||||
|
||||
|
@ -74,8 +71,8 @@ class Window(Gtk.ApplicationWindow):
|
|||
title="Uberwriter")
|
||||
|
||||
# Set UI
|
||||
self.builder = get_builder('Window')
|
||||
root = self.builder.get_object("FullscreenOverlay")
|
||||
builder = get_builder('Window')
|
||||
root = builder.get_object("FullscreenOverlay")
|
||||
root.connect('style-updated', self.apply_current_theme)
|
||||
self.connect("delete-event", self.on_delete_called)
|
||||
self.add(root)
|
||||
|
@ -88,7 +85,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
# Headerbars
|
||||
self.headerbar = headerbars.MainHeaderbar(app)
|
||||
self.set_titlebar(self.headerbar.hb_container)
|
||||
self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app)
|
||||
self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app)
|
||||
|
||||
self.title_end = " – UberWriter"
|
||||
self.set_headerbar_title("New File" + self.title_end)
|
||||
|
@ -101,9 +98,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
self.accel_group = Gtk.AccelGroup()
|
||||
self.add_accel_group(self.accel_group)
|
||||
|
||||
self.content = self.builder.get_object('content')
|
||||
|
||||
self.scrolled_window = self.builder.get_object('editor_scrolledwindow')
|
||||
self.scrolled_window = builder.get_object('editor_scrolledwindow')
|
||||
self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window')
|
||||
|
||||
# Setup text editor
|
||||
|
@ -114,15 +109,16 @@ class Window(Gtk.ApplicationWindow):
|
|||
self.text_view.grab_focus()
|
||||
self.scrolled_window.add(self.text_view)
|
||||
|
||||
# Stats stats counter
|
||||
self.stats_revealer = self.builder.get_object('editor_stats_revealer')
|
||||
self.stats_button = self.builder.get_object('editor_stats_button')
|
||||
self.stats_button.get_style_context().add_class('stats-button')
|
||||
# Setup stats counter
|
||||
self.stats_revealer = builder.get_object('editor_stats_revealer')
|
||||
self.stats_button = builder.get_object('editor_stats_button')
|
||||
self.stats_button.get_style_context().add_class('toggle-button')
|
||||
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
|
||||
|
||||
# Setup preview webview
|
||||
self.web_view = None
|
||||
self.web_view_scroller = None
|
||||
# Setup preview
|
||||
content = builder.get_object('content')
|
||||
editor = builder.get_object('editor')
|
||||
self.previewer = Previewer(content, editor, self.text_view)
|
||||
|
||||
# Setup header/stats bar hide after 3 seconds
|
||||
self.top_bottom_bars_visible = True
|
||||
|
@ -145,8 +141,8 @@ class Window(Gtk.ApplicationWindow):
|
|||
###
|
||||
# Sidebar initialization test
|
||||
###
|
||||
self.paned_window = self.builder.get_object("main_paned")
|
||||
self.sidebar_box = self.builder.get_object("sidebar_box")
|
||||
self.paned_window = builder.get_object("main_paned")
|
||||
self.sidebar_box = builder.get_object("sidebar_box")
|
||||
self.sidebar = Sidebar(self)
|
||||
self.sidebar_box.hide()
|
||||
|
||||
|
@ -154,7 +150,7 @@ class Window(Gtk.ApplicationWindow):
|
|||
# Search and replace initialization
|
||||
# Same interface as Sidebar ;)
|
||||
###
|
||||
self.searchreplace = SearchAndReplace(self, self.text_view)
|
||||
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
|
||||
|
||||
# Set current theme
|
||||
self.apply_current_theme()
|
||||
|
@ -459,70 +455,14 @@ class Window(Gtk.ApplicationWindow):
|
|||
"""
|
||||
|
||||
if state.get_boolean():
|
||||
self.show_web_view()
|
||||
self.previewer.show()
|
||||
else:
|
||||
self.show_text_editor()
|
||||
self.previewer.hide()
|
||||
|
||||
return True
|
||||
|
||||
def show_text_editor(self):
|
||||
# Remove web view
|
||||
if self.settings.get_boolean("preview-side-by-side"):
|
||||
self.set_size_request(-1, -1)
|
||||
self.content.remove(self.web_view)
|
||||
else:
|
||||
self.scrolled_window.remove(self.scrolled_window.get_child())
|
||||
self.scrolled_window.add(self.text_view)
|
||||
self.text_view.show()
|
||||
self.queue_draw()
|
||||
|
||||
# Sync scroll between web view and text view
|
||||
self.text_view.set_scroll_scale(self.web_view_scroller.get_scroll_scale())
|
||||
|
||||
# Destroy web view to clean up resources
|
||||
self.web_view.destroy()
|
||||
self.web_view = None
|
||||
self.web_view_scroller = None
|
||||
|
||||
def show_web_view(self, loaded=False):
|
||||
if loaded:
|
||||
# Sync scroll between text view and web view
|
||||
self.web_view_scroller.set_scroll_scale(self.text_view.get_scroll_scale())
|
||||
|
||||
# Show web view
|
||||
if self.settings.get_boolean("preview-side-by-side"):
|
||||
self.content.add(self.web_view)
|
||||
self.set_size_request(self.text_view.get_min_width() * 2, -1)
|
||||
else:
|
||||
self.scrolled_window.remove(self.scrolled_window.get_child())
|
||||
self.scrolled_window.add(self.web_view)
|
||||
self.web_view.show()
|
||||
self.queue_draw()
|
||||
else:
|
||||
args = ['--standalone',
|
||||
'--mathjax',
|
||||
'--css=' + Theme.get_current().web_css_path,
|
||||
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
|
||||
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
|
||||
output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args)
|
||||
|
||||
if self.web_view is None:
|
||||
self.web_view = WebKit.WebView()
|
||||
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
self.web_view_scroller = WebViewScroller(self.web_view)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.web_view.connect("load-changed", self.on_preview_load_change)
|
||||
|
||||
# This saying that all links will be opened in default browser, \
|
||||
# but local files are opened in appropriate apps:
|
||||
self.web_view.connect("decide-policy", self.on_click_link)
|
||||
|
||||
self.web_view.load_html(output, 'file://localhost/')
|
||||
|
||||
def reload_preview(self):
|
||||
if self.web_view:
|
||||
self.show_web_view()
|
||||
self.previewer.reload()
|
||||
|
||||
def load_file(self, filename=None):
|
||||
"""Open File from command line or open / open recent etc."""
|
||||
|
@ -701,17 +641,3 @@ class Window(Gtk.ApplicationWindow):
|
|||
self.filename = None
|
||||
base_path = "/"
|
||||
self.settings.set_value("open-file-path", GLib.Variant("s", base_path))
|
||||
|
||||
def on_preview_load_change(self, _web_view, event):
|
||||
"""swaps text editor with preview once the load is complete
|
||||
"""
|
||||
if event == WebKit.LoadEvent.FINISHED:
|
||||
self.show_web_view(loaded=True)
|
||||
|
||||
def on_click_link(self, web_view, decision, _decision_type):
|
||||
"""provide ability for self.webview to open links in default browser
|
||||
"""
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
webbrowser.open(web_view.get_uri())
|
||||
decision.ignore()
|
||||
return True # Don't let the event "bubble up"
|
||||
|
|
Loading…
Reference in New Issue