From 3bbbdf95e14c1638b216fbcb61cdbc22679a6f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 1 May 2019 16:37:57 +0100 Subject: [PATCH 01/27] Add .idea to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 564b95d..4ed3eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ flatpak/* data/ui/shortcut_handlers *.ui~ .vscode/ +.idea/ *.glade~ dist/uberwriter-2.0b0-py3.7.egg builddir/* From 9238a82d4df8124eb956f31b5e36b2c24085af4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 1 May 2019 16:40:14 +0100 Subject: [PATCH 02/27] Fix #154 --- uberwriter/text_view_markup_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py index 029871a..fd3a9a2 100644 --- a/uberwriter/text_view_markup_handler.py +++ b/uberwriter/text_view_markup_handler.py @@ -37,13 +37,13 @@ class MarkupHandler: "BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"), "STRIKETHROUGH": re.compile(r"~~.+?~~"), "LINK": re.compile(r"(\[).*(\]\(.+?\))"), - "HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n", re.MULTILINE), + "HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n\n", re.MULTILINE), "LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE), "NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE), "NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[.)]) .+", re.MULTILINE), "BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:>|(?:> )+).+", re.MULTILINE), "HEADER": re.compile(r"^[ ]{0,3}(#{1,6}) [^\n]+", re.MULTILINE), - "HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE), + "HEADER_UNDER": re.compile(r"^\n[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE), "CODE": re.compile(r"(?:^|\n)[ ]{0,3}(([`~]{3}).+?[ ]{0,3}\2)(?:\n|$)", re.DOTALL), "TABLE": re.compile(r"^[\-+]{5,}\n(.+?)\n[\-+]{5,}\n", re.DOTALL), "MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"), From 241ba567e47093496a9d3ae662a7e8ce96a093e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 1 May 2019 18:55:24 +0100 Subject: [PATCH 03/27] Fix character count with horizontal rules Pandoc's conversion to plain text converts horizontal rules to a sequence of 72 dashes. This update ensures that subsequent dashes are ignored when counting characters. --- uberwriter/stats_counter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py index fa4b103..adeac08 100644 --- a/uberwriter/stats_counter.py +++ b/uberwriter/stats_counter.py @@ -11,8 +11,11 @@ from uberwriter import helpers class StatsCounter: """Counts characters, words, sentences and read time using a background thread.""" - # Regexp that matches any character, except for newlines and subsequent spaces. - CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))") + # Regexp that matches characters, with the following exceptions: + # * Newlines + # * Sequential spaces + # * Sequential dashes + CHARACTERS = re.compile(r"[^\s-]|(?:[^\S\n](?!\s)|-(?![-\n]))") # Regexp that matches Asian letters, general symbols and hieroglyphs, # as well as sequences of word characters optionally containing non-word characters in-between. From 63b20d0f3cfb1516cf3fc85b8c978256d630a0f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 21 Apr 2019 21:54:50 +0100 Subject: [PATCH 04/27] Match breakpoints --- data/media/css/web/base.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/media/css/web/base.css b/data/media/css/web/base.css index 65dfbb2..059ba28 100644 --- a/data/media/css/web/base.css +++ b/data/media/css/web/base.css @@ -45,7 +45,7 @@ html { } } -@media screen and (min-width: 1000px) { +@media screen and (min-width: 1280px) { html { font-size: 18px; } @@ -57,7 +57,7 @@ body { font-family: "Fira Sans", fira-sans, sans-serif, color-emoji; line-height: 1.5; word-wrap: break-word; - max-width: 978px; + max-width: 980px; margin: auto; padding: 2em; } From 562cc7e200fce0e56997c8cb6c63afe4a710efa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 21 Apr 2019 01:23:44 +0100 Subject: [PATCH 05/27] Sync scroll between text view and web view Scrolling is synced via scroll percentage. This works for most cases, but breaks down on very large or complex documents. It is consistent with the approach other editors use (eg. iA Writer), but in the future we should explore alternatives that don't incur in edge cases. The syncing itself is done via JavaScript. It could be argued that a `WebExtension` is the better approach, but it is considerably more complex for such a simple use case and it would be painful to implement until UberWriter's build system is updated, since it requires implementing a C extension. Fixes #55 --- uberwriter/scroller.py | 48 ----------- uberwriter/text_view.py | 84 +++++++++---------- uberwriter/text_view_scroller.py | 109 +++++++++++++++++++++++++ uberwriter/web_view_scroller.py | 134 +++++++++++++++++++++++++++++++ uberwriter/window.py | 48 +++++++---- 5 files changed, 310 insertions(+), 113 deletions(-) delete mode 100644 uberwriter/scroller.py create mode 100644 uberwriter/text_view_scroller.py create mode 100644 uberwriter/web_view_scroller.py diff --git a/uberwriter/scroller.py b/uberwriter/scroller.py deleted file mode 100644 index 518167f..0000000 --- a/uberwriter/scroller.py +++ /dev/null @@ -1,48 +0,0 @@ -class Scroller: - def __init__(self, scrolled_window, source_pos, target_pos): - super().__init__() - - self.scrolled_window = scrolled_window - self.source_pos = source_pos - self.target_pos = target_pos - self.duration = max(200, (target_pos - source_pos) / 50) * 1000 - - self.is_started = False - self.is_setup = False - self.start_time = 0 - self.end_time = 0 - self.tick_callback_id = 0 - - def start(self): - self.is_started = True - self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick) - - def end(self): - self.scrolled_window.remove_tick_callback(self.tick_callback_id) - self.is_started = False - - def setup(self, time): - self.start_time = time - self.end_time = time + self.duration - self.is_setup = True - - def on_tick(self, widget, frame_clock): - def ease_out_cubic(value): - return pow(value - 1, 3) + 1 - - now = frame_clock.get_frame_time() - if not self.is_setup: - self.setup(now) - - if now < self.end_time: - time = float(now - self.start_time) / float(self.end_time - self.start_time) - else: - time = 1 - self.end() - - time = ease_out_cubic(time) - pos = self.source_pos + (time * (self.target_pos - self.source_pos)) - widget.get_vadjustment().props.value = pos - return True - - diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 7436372..e70d8e8 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -3,9 +3,9 @@ import gi from uberwriter.inline_preview import InlinePreview from uberwriter.text_view_format_inserter import FormatInserter from uberwriter.text_view_markup_handler import MarkupHandler +from uberwriter.text_view_scroller import TextViewScroller from uberwriter.text_view_undo_redo_handler import UndoRedoHandler from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT -from uberwriter.scroller import Scroller gi.require_version('Gtk', '3.0') gi.require_version('Gspell', '1') @@ -85,6 +85,7 @@ class TextView(Gtk.TextView): # Scrolling self.scroller = None + self.connect('parent-set', self.on_parent_set) self.get_buffer().connect('mark-set', self.on_mark_set) # Focus mode @@ -105,14 +106,41 @@ class TextView(Gtk.TextView): text_buffer = self.get_buffer() text_buffer.set_text(text) + def get_scroll_scale(self): + return self.scroller.get_scroll_scale() if self.scroller else 0 + + def set_scroll_scale(self, scale): + if self.scroller: + self.scroller.set_scroll_scale(scale) + def on_text_changed(self, *_): self.markup.apply() - GLib.idle_add(self.scroll_to) + self.smooth_scroll_to() def on_size_allocate(self, *_): self.update_vertical_margin() self.markup.update_margins_indents() + def on_parent_set(self, *_): + parent = self.get_parent() + if parent: + self.scroller = TextViewScroller(self, parent) + else: + self.scroller = None + + def on_mark_set(self, _text_buffer, _location, mark, _data=None): + if mark.get_name() == 'insert': + self.markup.apply() + self.smooth_scroll_to(mark) + elif mark.get_name() == 'gtk_drag_target': + self.smooth_scroll_to(mark) + return True + + def on_button_release_event(self, _widget, _event): + if self.focus_mode: + self.markup.apply() + return False + def set_focus_mode(self, focus_mode): """Toggle focus mode. @@ -123,7 +151,7 @@ class TextView(Gtk.TextView): self.gspell_view.set_inline_spell_checking(not focus_mode) self.update_vertical_margin() self.markup.apply() - self.scroll_to() + self.smooth_scroll_to() def update_vertical_margin(self): if self.focus_mode: @@ -134,11 +162,6 @@ class TextView(Gtk.TextView): self.props.top_margin = 80 self.props.bottom_margin = 64 - def on_button_release_event(self, _widget, _event): - if self.focus_mode: - self.markup.apply() - return False - def set_hemingway_mode(self, hemingway_mode): """Toggle hemingway mode. @@ -158,48 +181,13 @@ class TextView(Gtk.TextView): self.get_buffer().set_text('') self.undo_redo.clear() - def scroll_to(self, mark=None): + def smooth_scroll_to(self, mark=None): """Scrolls if needed to ensure mark is visible. If mark is unspecified, the cursor is used.""" - margin = 32 - scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__) - if not scrolled_window: + if self.scroller is None: return - va = scrolled_window.get_vadjustment() - if va.props.page_size < margin * 2: - return - - text_buffer = self.get_buffer() - if mark: - mark_iter = text_buffer.get_iter_at_mark(mark) - else: - mark_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert()) - mark_rect = self.get_iter_location(mark_iter) - - pos_y = mark_rect.y + mark_rect.height + self.props.top_margin - pos = pos_y - va.props.value - target_pos = None - if self.focus_mode: - if pos != (va.props.page_size * 0.5): - target_pos = pos_y - (va.props.page_size * 0.5) - elif pos > va.props.page_size - margin: - target_pos = pos_y - va.props.page_size + margin - elif pos < margin: - target_pos = pos_y - margin - - if self.scroller and self.scroller.is_started: - self.scroller.end() - if target_pos: - self.scroller = Scroller(scrolled_window, va.props.value, target_pos) - self.scroller.start() - - def on_mark_set(self, _text_buffer, _location, mark, _data=None): - if mark.get_name() == 'insert': - self.markup.apply() - if self.focus_mode: - self.scroll_to(mark) - elif mark.get_name() == 'gtk_drag_target': - self.scroll_to(mark) - return True + if mark is None: + mark = self.get_buffer().get_insert() + GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode) diff --git a/uberwriter/text_view_scroller.py b/uberwriter/text_view_scroller.py new file mode 100644 index 0000000..70f4c95 --- /dev/null +++ b/uberwriter/text_view_scroller.py @@ -0,0 +1,109 @@ +from gi.repository import Gtk + + +class TextViewScroller: + def __init__(self, text_view, scrolled_window): + super().__init__() + + self.text_view = text_view + self.scrolled_window = scrolled_window + self.smooth_scroller = None + + def get_scroll_scale(self): + vap = self.scrolled_window.get_vadjustment().props + if vap.upper > vap.page_size: + return vap.value / (vap.upper - vap.page_size) + else: + return 0 + + def set_scroll_scale(self, scale): + vap = self.scrolled_window.get_vadjustment().props + vap.value = (vap.upper - vap.page_size) * scale + + def scroll_to_mark(self, mark, center): + """Scrolls until mark is visible, if needed.""" + + target_pos = self.get_target_pos_for_mark(mark, center) + if target_pos: + self.scrolled_window.get_vadjustment().set_value(target_pos) + + def smooth_scroll_to_mark(self, mark, center): + """Smoothly scrolls until mark is visible, if needed.""" + + if self.smooth_scroller and self.smooth_scroller.is_started: + self.smooth_scroller.end() + + target_pos = self.get_target_pos_for_mark(mark, center) + if target_pos: + source_pos = self.scrolled_window.get_vadjustment().props.value + self.smooth_scroller = SmoothScroller(self.scrolled_window, source_pos, target_pos) + self.smooth_scroller.start() + + def get_target_pos_for_mark(self, mark, center): + margin = 32 + + mark_iter = self.text_view.get_buffer().get_iter_at_mark(mark) + mark_rect = self.text_view.get_iter_location(mark_iter) + + vap = self.scrolled_window.get_vadjustment().props + + pos_y = mark_rect.y + mark_rect.height + self.text_view.props.top_margin + pos_viewport_y = pos_y - vap.value + target_pos = None + if center: + if pos_viewport_y != vap.page_size / 2: + target_pos = pos_y - (vap.page_size / 2) + elif pos_viewport_y > vap.page_size - margin: + target_pos = pos_y - vap.page_size + margin + elif pos_viewport_y < margin: + target_pos = pos_y - margin - mark_rect.height + + return target_pos + + +class SmoothScroller: + def __init__(self, scrolled_window, source_pos, target_pos): + super().__init__() + + self.scrolled_window = scrolled_window + self.source_pos = source_pos + self.target_pos = target_pos + self.duration = max(100, (target_pos - source_pos) / 50) * 1000 + + self.is_started = False + self.is_setup = False + self.start_time = 0 + self.end_time = 0 + self.tick_callback_id = 0 + + def start(self): + self.is_started = True + self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick) + + def end(self): + self.scrolled_window.remove_tick_callback(self.tick_callback_id) + self.is_started = False + + def setup(self, time): + self.start_time = time + self.end_time = time + self.duration + self.is_setup = True + + def on_tick(self, widget, frame_clock): + def ease_out_cubic(value): + return pow(value - 1, 3) + 1 + + now = frame_clock.get_frame_time() + if not self.is_setup: + self.setup(now) + + if now < self.end_time: + time = float(now - self.start_time) / float(self.end_time - self.start_time) + else: + time = 1 + self.end() + + time = ease_out_cubic(time) + pos = self.source_pos + (time * (self.target_pos - self.source_pos)) + widget.get_vadjustment().props.value = pos + return True diff --git a/uberwriter/web_view_scroller.py b/uberwriter/web_view_scroller.py new file mode 100644 index 0000000..2c757c0 --- /dev/null +++ b/uberwriter/web_view_scroller.py @@ -0,0 +1,134 @@ +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() + diff --git a/uberwriter/window.py b/uberwriter/window.py index 10cdb97..5ef61de 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -27,6 +27,7 @@ import gi from uberwriter.export_dialog import Export 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 @@ -108,7 +109,8 @@ class Window(Gtk.ApplicationWindow): self.text_view.grab_focus() # Setup preview webview - self.preview_webview = None + self.web_view = None + self.web_view_scroller = None self.scrolled_window = self.builder.get_object('editor_scrolledwindow') self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window') @@ -503,25 +505,36 @@ class Window(Gtk.ApplicationWindow): """ if state.get_boolean(): - self.show_preview() + self.show_web_view() else: self.show_text_editor() return True def show_text_editor(self): + # Swap web view with text view self.scrolled_window.remove(self.scrolled_window.get_child()) self.scrolled_window.add(self.text_view) self.text_view.show() - self.preview_webview.destroy() - self.preview_webview = None self.queue_draw() - def show_preview(self, loaded=False): + # 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()) + + # Swap text view with web view self.scrolled_window.remove(self.scrolled_window.get_child()) - self.scrolled_window.add(self.preview_webview) - self.preview_webview.show() + self.scrolled_window.add(self.web_view) + self.web_view.show() self.queue_draw() else: args = ['--standalone', @@ -531,22 +544,23 @@ class Window(Gtk.ApplicationWindow): '--lua-filter=' + helpers.get_script_path('task-list.lua')] output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args) - if self.preview_webview is None: - self.preview_webview = WebKit.WebView() - self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True) + 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.preview_webview.connect("load-changed", self.on_preview_load_change) + 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.preview_webview.connect("decide-policy", self.on_click_link) + self.web_view.connect("decide-policy", self.on_click_link) - self.preview_webview.load_html(output, 'file://localhost/') + self.web_view.load_html(output, 'file://localhost/') def reload_preview(self): - if self.preview_webview: - self.show_preview() + if self.web_view: + self.show_web_view() def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" @@ -726,11 +740,11 @@ class Window(Gtk.ApplicationWindow): base_path = "/" self.settings.set_value("open-file-path", GLib.Variant("s", base_path)) - def on_preview_load_change(self, webview, event): + 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_preview(loaded=True) + 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 From bc23fa9b0b101401c710032dc212ef97a64bb23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Mon, 22 Apr 2019 01:31:32 +0100 Subject: [PATCH 06/27] Make characters-per-line configurable This is in preparation for the side-by-side preview, where the editor needs to become more adaptable. It indirectly fixes #141, as users can now change the desired line-length, although there is no UI setting for it. --- data/de.wolfvollprecht.UberWriter.gschema.xml | 7 +++ data/media/css/gtk/base.css | 16 +++++- uberwriter/text_view.py | 57 +++++++++++++++++-- uberwriter/window.py | 50 +--------------- 4 files changed, 74 insertions(+), 56 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index e7d4bfc..eccc8bf 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -69,6 +69,13 @@ Which statistic is shown on the main window. + + 66 + Characters per line + + Maximum number of characters per line within the editor. + + diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css index ff0736d..f1707dc 100644 --- a/data/media/css/gtk/base.css +++ b/data/media/css/gtk/base.css @@ -33,11 +33,23 @@ font-size: 16px; } -.uberwriter-window.small .uberwriter-editor { +.uberwriter-window .uberwriter-editor.size14 { font-size: 14px; } -.uberwriter-window.large .uberwriter-editor { +.uberwriter-window .uberwriter-editor.size15 { + font-size: 15px; +} + +.uberwriter-window .uberwriter-editor.size16 { + font-size: 16px; +} + +.uberwriter-window .uberwriter-editor.size17 { + font-size: 17px; +} + +.uberwriter-window .uberwriter-editor.size18 { font-size: 18px; } diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index e70d8e8..f30e4ab 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -39,7 +39,9 @@ class TextView(Gtk.TextView): 'redo': (GObject.SignalFlags.ACTION, None, ()) } - def __init__(self): + font_sizes = [18, 17, 16, 15, 14] # Must match CSS selectors in gtk/base.css + + def __init__(self, line_chars): super().__init__() # Appearance @@ -49,9 +51,15 @@ class TextView(Gtk.TextView): self.set_pixels_inside_wrap(8) self.get_style_context().add_class('uberwriter-editor') + # Text sizing + self.props.halign = Gtk.Align.FILL + self.font_size = 16 + self.line_chars = line_chars + self.get_style_context().add_class('size16') + # General behavior - self.get_buffer().connect('changed', self.on_text_changed) self.connect('size-allocate', self.on_size_allocate) + self.get_buffer().connect('changed', self.on_text_changed) # Spell checking self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self) @@ -113,18 +121,22 @@ class TextView(Gtk.TextView): if self.scroller: self.scroller.set_scroll_scale(scale) + def on_size_allocate(self, *_): + self.update_horizontal_margin() + self.update_vertical_margin() + self.markup.update_margins_indents() + def on_text_changed(self, *_): self.markup.apply() self.smooth_scroll_to() - def on_size_allocate(self, *_): - self.update_vertical_margin() - self.markup.update_margins_indents() - def on_parent_set(self, *_): parent = self.get_parent() if parent: self.scroller = TextViewScroller(self, parent) + # Request a size that fits the minimum font size comfortably. + parent.set_size_request( + self.pad_chars(self.font_sizes[-1]) * self.font_width(self.font_sizes[-1]), 500) else: self.scroller = None @@ -153,6 +165,26 @@ class TextView(Gtk.TextView): self.markup.apply() self.smooth_scroll_to() + def update_horizontal_margin(self): + width = self.get_allocation().width + + # Ensure the appropriate font size is being used + for size in self.font_sizes: + min_width = (self.line_chars + self.pad_chars(size) + 1) * self.font_width(size) - 1 + if width >= min_width: + if size != self.font_size: + self.font_size = size + for s in self.font_sizes: + self.get_style_context().remove_class("size{}".format(s)) + self.get_style_context().add_class("size{}".format(size)) + break + + # Apply margin with the remaining space to allow for markup + line_width = (self.line_chars + 1) * int(self.font_width(self.font_size)) - 1 + horizontal_margin = (width - line_width) / 2 + self.props.left_margin = horizontal_margin + self.props.right_margin = horizontal_margin + def update_vertical_margin(self): if self.focus_mode: height = self.get_allocation().height @@ -191,3 +223,16 @@ class TextView(Gtk.TextView): if mark is None: mark = self.get_buffer().get_insert() GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode) + + def pad_chars(self, font_size): + """Returns the amount of character padding for font_size. + + Markup can use up to 6 in normal conditions.""" + + return 8 * (1 + font_size - self.font_sizes[-1]) + + @staticmethod + def font_width(font_size): + """Returns the font width for a given size. Specific to Fira Mono.""" + + return font_size * 1 / 1.6 diff --git a/uberwriter/window.py b/uberwriter/window.py index 5ef61de..c9e62a2 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -77,9 +77,10 @@ class Window(Gtk.ApplicationWindow): self.builder = get_builder('Window') root = self.builder.get_object("FullscreenOverlay") root.connect('style-updated', self.apply_current_theme) + self.connect("delete-event", self.on_delete_called) self.add(root) - self.set_default_size(900, 500) + self.set_default_size(1000, 600) # Preferences self.settings = Settings.new() @@ -155,11 +156,6 @@ class Window(Gtk.ApplicationWindow): ### self.searchreplace = SearchAndReplace(self, self.text_view) - # Window resize - self.window_resize(self) - self.connect("configure-event", self.window_resize) - self.connect("delete-event", self.on_delete_called) - # Set current theme self.apply_current_theme() self.get_style_context().add_class('uberwriter-window') @@ -233,48 +229,6 @@ class Window(Gtk.ApplicationWindow): self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() - def window_resize(self, window, event=None): - """set paddings dependant of the window size - """ - - # Ensure the window receiving the event is the one we care about, ie. the main window. - # On Wayland (bug?), sub-windows such as the recents popover will also trigger this. - if event and event.window != window.get_window(): - return - - # Adjust text editor width depending on window width, so that: - # - The number of characters per line is adequate (http://webtypography.net/2.1.2) - # - The number of characters stays constant while resizing the window / font - # - There is enough text margin for MarkupBuffer to apply indents / negative margins - # - # TODO: Avoid hard-coding. Font size is clearer than unclear dimensions, but not ideal. - w_width = event.width if event else window.get_allocation().width - if w_width < 900: - font_size = 14 - self.get_style_context().add_class("small") - self.get_style_context().remove_class("large") - - elif w_width < 1280: - font_size = 16 - self.get_style_context().remove_class("small") - self.get_style_context().remove_class("large") - - else: - font_size = 18 - self.get_style_context().remove_class("small") - self.get_style_context().add_class("large") - - font_width = int(font_size * 1/1.6) # Ratio specific to Fira Mono - width = 67 * font_width - 1 # 66 characters - horizontal_margin = 8 * font_width # 8 characters - width_request = width + horizontal_margin * 2 - - if self.text_view.props.width_request != width_request: - self.text_view.props.width_request = width_request - self.text_view.set_left_margin(horizontal_margin) - self.text_view.set_right_margin(horizontal_margin) - self.scrolled_window.props.width_request = width_request - # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document From 5e770510ee98fc70d5a88de75d28fa3a40c788c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Mon, 22 Apr 2019 02:19:57 +0100 Subject: [PATCH 07/27] Add support for side-by-side preview Fixes #59 --- data/de.wolfvollprecht.UberWriter.gschema.xml | 7 ++ data/media/css/gtk/base.css | 6 +- data/ui/Window.ui | 118 ++++++++++-------- uberwriter/text_view.py | 40 +++--- uberwriter/window.py | 56 +++++---- 5 files changed, 128 insertions(+), 99 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index eccc8bf..7ac4dbd 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -76,6 +76,13 @@ Maximum number of characters per line within the editor. + + false + Side-by-side preview + + Show the preview side by side, instead of full-width. + + diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css index f1707dc..41323d8 100644 --- a/data/media/css/gtk/base.css +++ b/data/media/css/gtk/base.css @@ -96,7 +96,7 @@ padding: 0; } -.stats-counter { +.stats-button { color: alpha(@theme_fg_color, 0.6); background-color: @theme_base_color; text-shadow: inherit; @@ -116,8 +116,8 @@ transition: 100ms ease-in; } -.stats-counter:hover, -.stats-counter:checked { +.stats-button :hover, +.stats-button :checked { color: @theme_fg_color; background-color: mix(@theme_base_color, @theme_bg_color, 0.5); } diff --git a/data/ui/Window.ui b/data/ui/Window.ui index bca48f4..fc9c411 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -8,32 +8,32 @@ 1 10 - + + True + False + edit-find-replace-symbolic + + True False go-up-symbolic - + True False go-down-symbolic - + + True + False + pan-down-symbolic + 2 + + True False gtk-spell-check - - - text/plain - text/x-markdown - - - - True - False - edit-find-replace-symbolic - FullscreenOverlay True @@ -43,10 +43,10 @@ True False - + True True - True + False 200 @@ -70,45 +70,17 @@ - + True False - True + True - + True False - crossfade - 750 - True - - - 0 Words - True - True - True - Show Statistics - end - - - - - 0 - 1 - - - - - True - False - True - True - natural - natural - none + vertical - 500 True True True @@ -119,16 +91,52 @@ + + False + True + 0 + + + + + True + False + crossfade + 750 + True + + + 0 Words + True + True + True + Show Statistics + end + pan-down + right + True + + + + + False + True + 1 + - 0 - 0 + False + True + 0 + + + - False + True False @@ -191,7 +199,7 @@ True True Previous Match - amunt + go-up False @@ -205,7 +213,7 @@ True True Next Match - avall + go_down False @@ -264,7 +272,7 @@ True True Open Replace - reemplaza + edit-find-replace False @@ -345,7 +353,7 @@ True True True - ortografia1 + spell-check False diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index f30e4ab..7c38176 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -12,6 +12,7 @@ gi.require_version('Gspell', '1') from gi.repository import Gtk, Gdk, GObject, GLib, Gspell import logging + LOGGER = logging.getLogger('uberwriter') @@ -53,8 +54,8 @@ class TextView(Gtk.TextView): # Text sizing self.props.halign = Gtk.Align.FILL - self.font_size = 16 self.line_chars = line_chars + self.font_size = 16 self.get_style_context().add_class('size16') # General behavior @@ -133,10 +134,8 @@ class TextView(Gtk.TextView): def on_parent_set(self, *_): parent = self.get_parent() if parent: + parent.set_size_request(self.get_min_width(), 500) self.scroller = TextViewScroller(self, parent) - # Request a size that fits the minimum font size comfortably. - parent.set_size_request( - self.pad_chars(self.font_sizes[-1]) * self.font_width(self.font_sizes[-1]), 500) else: self.scroller = None @@ -169,18 +168,17 @@ class TextView(Gtk.TextView): width = self.get_allocation().width # Ensure the appropriate font size is being used - for size in self.font_sizes: - min_width = (self.line_chars + self.pad_chars(size) + 1) * self.font_width(size) - 1 - if width >= min_width: - if size != self.font_size: - self.font_size = size - for s in self.font_sizes: - self.get_style_context().remove_class("size{}".format(s)) - self.get_style_context().add_class("size{}".format(size)) + for font_size in self.font_sizes: + if width >= self.get_min_width(font_size) or font_size == self.font_sizes[-1]: + if font_size != self.font_size: + self.font_size = font_size + for fs in self.font_sizes: + self.get_style_context().remove_class("size{}".format(fs)) + self.get_style_context().add_class("size{}".format(font_size)) break # Apply margin with the remaining space to allow for markup - line_width = (self.line_chars + 1) * int(self.font_width(self.font_size)) - 1 + line_width = (self.line_chars + 1) * int(self.get_char_width(self.font_size)) - 1 horizontal_margin = (width - line_width) / 2 self.props.left_margin = horizontal_margin self.props.right_margin = horizontal_margin @@ -224,15 +222,23 @@ class TextView(Gtk.TextView): mark = self.get_buffer().get_insert() GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode) - def pad_chars(self, font_size): + def get_min_width(self, font_size=None): + """Returns the minimum width of this text view.""" + + if font_size is None: + font_size = self.font_sizes[-1] + return (self.line_chars + self.get_pad_chars(font_size) + 1) \ + * self.get_char_width(font_size) - 1 + + def get_pad_chars(self, font_size): """Returns the amount of character padding for font_size. - Markup can use up to 6 in normal conditions.""" + Markup can use up to 7 in normal conditions.""" return 8 * (1 + font_size - self.font_sizes[-1]) @staticmethod - def font_width(font_size): - """Returns the font width for a given size. Specific to Fira Mono.""" + def get_char_width(font_size): + """Returns the font width for a given size. Note: specific to Fira Mono!""" return font_size * 1 / 1.6 diff --git a/uberwriter/window.py b/uberwriter/window.py index c9e62a2..0ee9b59 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -101,29 +101,29 @@ 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.get_style_context().add_class('uberwriter-scrolled-window') + # Setup text editor - self.text_view = TextView() - self.text_view.props.halign = Gtk.Align.CENTER + self.text_view = TextView(self.settings.get_int("characters-per-line")) self.text_view.connect('focus-out-event', self.focus_out) self.text_view.get_buffer().connect('changed', self.on_text_changed) self.text_view.show() 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') + self.stats_handler = StatsHandler(self.stats_button, self.text_view) # Setup preview webview self.web_view = None self.web_view_scroller = None - self.scrolled_window = self.builder.get_object('editor_scrolledwindow') - self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window') - self.scrolled_window.add(self.text_view) - self.editor_viewport = self.builder.get_object('editor_viewport') - - # Stats counter - self.stats_counter_revealer = self.builder.get_object('stats_counter_revealer') - self.stats_button = self.builder.get_object('stats_counter') - self.stats_button.get_style_context().add_class('stats-counter') - self.stats_handler = StatsHandler(self.stats_button, self.text_view) - # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True self.was_motion = True @@ -145,7 +145,7 @@ class Window(Gtk.ApplicationWindow): ### # Sidebar initialization test ### - self.paned_window = self.builder.get_object("main_pained") + self.paned_window = self.builder.get_object("main_paned") self.sidebar_box = self.builder.get_object("sidebar_box") self.sidebar = Sidebar(self) self.sidebar_box.hide() @@ -466,10 +466,14 @@ class Window(Gtk.ApplicationWindow): return True def show_text_editor(self): - # Swap web view with text view - self.scrolled_window.remove(self.scrolled_window.get_child()) - self.scrolled_window.add(self.text_view) - self.text_view.show() + # 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 @@ -485,9 +489,13 @@ class Window(Gtk.ApplicationWindow): # Sync scroll between text view and web view self.web_view_scroller.set_scroll_scale(self.text_view.get_scroll_scale()) - # Swap text view with web view - self.scrolled_window.remove(self.scrolled_window.get_child()) - self.scrolled_window.add(self.web_view) + # 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: @@ -590,7 +598,7 @@ class Window(Gtk.ApplicationWindow): and self.buffer_modified_for_status_bar and self.text_view.props.has_focus): # pylint: disable=no-member # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) - self.stats_counter_revealer.set_reveal_child(False) + self.stats_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) self.top_bottom_bars_visible = False self.buffer_modified_for_status_bar = False @@ -612,7 +620,7 @@ class Window(Gtk.ApplicationWindow): if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar if self.top_bottom_bars_visible is False: - self.stats_counter_revealer.set_reveal_child(True) + self.stats_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.top_bottom_bars_visible = True @@ -624,7 +632,7 @@ class Window(Gtk.ApplicationWindow): """events called when the window losses focus """ if self.top_bottom_bars_visible is False: - self.stats_counter_revealer.set_reveal_child(True) + self.stats_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 self.top_bottom_bars_visible = True From 2cb161307c17e789eba42a7529b2f9f587dbef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 25 Apr 2019 15:57:06 +0100 Subject: [PATCH 08/27] Improve side-by-side experience Includes multiple improvements to scroll syncing, preview re-render, layout separation, etc --- data/media/css/web/base.css | 2 +- data/ui/Preview.ui | 47 ++++++++++ uberwriter/inline_preview.py | 10 ++- uberwriter/preview_converter.py | 49 +++++++++++ uberwriter/previewer.py | 147 +++++++++++++++++++++++++++++++ uberwriter/search_and_replace.py | 26 +++--- uberwriter/stats_handler.py | 6 -- uberwriter/text_view.py | 8 +- uberwriter/web_view.py | 104 ++++++++++++++++++++++ uberwriter/web_view_scroller.py | 134 ---------------------------- uberwriter/window.py | 112 ++++------------------- 11 files changed, 393 insertions(+), 252 deletions(-) create mode 100644 data/ui/Preview.ui create mode 100644 uberwriter/preview_converter.py create mode 100644 uberwriter/previewer.py create mode 100644 uberwriter/web_view.py delete mode 100644 uberwriter/web_view_scroller.py diff --git a/data/media/css/web/base.css b/data/media/css/web/base.css index 059ba28..ae24b4a 100644 --- a/data/media/css/web/base.css +++ b/data/media/css/web/base.css @@ -59,7 +59,7 @@ body { word-wrap: break-word; max-width: 980px; margin: auto; - padding: 2em; + padding: 4em; } a { diff --git a/data/ui/Preview.ui b/data/ui/Preview.ui new file mode 100644 index 0000000..7b8c934 --- /dev/null +++ b/data/ui/Preview.ui @@ -0,0 +1,47 @@ + + + + + + True + False + pan-down-symbolic + 2 + + + True + False + vertical + + + + + + True + False + crossfade + 750 + True + + + Full-Width + True + True + True + Switch Preview Mode + end + pan-down + right + True + + + + + False + True + end + 1 + + + + diff --git a/uberwriter/inline_preview.py b/uberwriter/inline_preview.py index ae5f9d9..75dbaee 100644 --- a/uberwriter/inline_preview.py +++ b/uberwriter/inline_preview.py @@ -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"!\[(.*?)\]\((.+?)\)") diff --git a/uberwriter/preview_converter.py b/uberwriter/preview_converter.py new file mode 100644 index 0000000..07fba58 --- /dev/null +++ b/uberwriter/preview_converter.py @@ -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) diff --git a/uberwriter/previewer.py b/uberwriter/previewer.py new file mode 100644 index 0000000..309db20 --- /dev/null +++ b/uberwriter/previewer.py @@ -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 diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 24fb1d9..462db2e 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -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) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py index 8fce782..83c4d89 100644 --- a/uberwriter/stats_handler.py +++ b/uberwriter/stats_handler.py @@ -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 diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 7c38176..abbd6ec 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -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. diff --git a/uberwriter/web_view.py b/uberwriter/web_view.py new file mode 100644 index 0000000..ca69c7a --- /dev/null +++ b/uberwriter/web_view.py @@ -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() diff --git a/uberwriter/web_view_scroller.py b/uberwriter/web_view_scroller.py deleted file mode 100644 index 2c757c0..0000000 --- a/uberwriter/web_view_scroller.py +++ /dev/null @@ -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() - diff --git a/uberwriter/window.py b/uberwriter/window.py index 0ee9b59..fbdfb40 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -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" From f72f61ae7dee9ade158a2a542f000906e2b9f3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 25 Apr 2019 16:21:33 +0100 Subject: [PATCH 09/27] Fix stats' i18n --- uberwriter/stats_handler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py index 83c4d89..3129d85 100644 --- a/uberwriter/stats_handler.py +++ b/uberwriter/stats_handler.py @@ -60,15 +60,15 @@ class StatsHandler: def get_text_for_stat(self, stat): if stat == 0: - return _("{:n} Characters".format(self.characters)) + return _("{:n} Characters").format(self.characters) elif stat == 1: - return _("{:n} Words".format(self.words)) + return _("{:n} Words").format(self.words) elif stat == 2: - return _("{:n} Sentences".format(self.sentences)) + return _("{:n} Sentences").format(self.sentences) elif stat == 3: - return _("{:n} Paragraphs".format(self.paragraphs)) + return _("{:n} Paragraphs").format(self.paragraphs) elif stat == 4: - return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time)) + return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time) else: raise ValueError("Unknown stat {}".format(stat)) From 65e70288439e76ad57e46d882b6b323fbf1ef4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 25 Apr 2019 23:41:43 +0100 Subject: [PATCH 10/27] Add toggle between various preview modes Including: * Full-width (original) * Half-width * Half-height * Windowed --- data/de.wolfvollprecht.UberWriter.gschema.xml | 15 ++- data/media/css/gtk/base.css | 6 +- data/ui/Preview.ui | 4 + data/ui/Window.ui | 3 + uberwriter/application.py | 16 ++- .../{previewer.py => preview_handler.py} | 82 +++++------ uberwriter/preview_renderer.py | 127 ++++++++++++++++++ uberwriter/stats_handler.py | 18 ++- uberwriter/web_view.py | 20 +-- uberwriter/window.py | 15 ++- 10 files changed, 230 insertions(+), 76 deletions(-) rename uberwriter/{previewer.py => preview_handler.py} (58%) create mode 100644 uberwriter/preview_renderer.py diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 7ac4dbd..c9e45ec 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -10,6 +10,13 @@ + + + + + + + @@ -76,11 +83,11 @@ Maximum number of characters per line within the editor. - - false - Side-by-side preview + + "full-width" + Preview mode - Show the preview side by side, instead of full-width. + How to display the preview. diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css index 41323d8..5d2711a 100644 --- a/data/media/css/gtk/base.css +++ b/data/media/css/gtk/base.css @@ -96,7 +96,7 @@ padding: 0; } -.stats-button { +.toggle-button { color: alpha(@theme_fg_color, 0.6); background-color: @theme_base_color; text-shadow: inherit; @@ -116,8 +116,8 @@ transition: 100ms ease-in; } -.stats-button :hover, -.stats-button :checked { +.toggle-button:hover, +.toggle-button:checked { color: @theme_fg_color; background-color: mix(@theme_base_color, @theme_bg_color, 0.5); } diff --git a/data/ui/Preview.ui b/data/ui/Preview.ui index 7b8c934..a084329 100644 --- a/data/ui/Preview.ui +++ b/data/ui/Preview.ui @@ -11,6 +11,7 @@ True False + True vertical @@ -33,6 +34,9 @@ pan-down right True + diff --git a/data/ui/Window.ui b/data/ui/Window.ui index fc9c411..7f6e72c 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -115,6 +115,9 @@ pan-down right True + diff --git a/uberwriter/application.py b/uberwriter/application.py index 4a0f34c..62e38f1 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -120,10 +120,18 @@ class Application(Gtk.Application): stat_default = self.settings.get_string("stat-default") action = Gio.SimpleAction.new_stateful( - "stat_default", GLib.VariantType.new('s'), GLib.Variant.new_string(stat_default)) + "stat_default", GLib.VariantType.new("s"), GLib.Variant.new_string(stat_default)) action.connect("activate", self.on_stat_default) self.add_action(action) + # Preview Menu + + preview_mode = self.settings.get_string("preview-mode") + action = Gio.SimpleAction.new_stateful( + "preview_mode", GLib.VariantType.new("s"), GLib.Variant.new_string(preview_mode)) + action.connect("activate", self.on_preview_mode) + self.add_action(action) + # Shortcuts # TODO: be aware that a couple of shortcuts are defined in base.css @@ -180,6 +188,8 @@ class Application(Gtk.Application): self.window.reload_preview() elif key == "stat-default": self.window.update_default_stat() + elif key == "preview-mode": + self.window.update_preview_mode() def on_new(self, _action, _value): self.window.new_document() @@ -250,6 +260,10 @@ class Application(Gtk.Application): action.set_state(value) self.settings.set_string("stat-default", value.get_string()) + def on_preview_mode(self, action, value): + action.set_state(value) + self.settings.set_string("preview-mode", value.get_string()) + # ~ if __name__ == "__main__": # ~ app = Application() # ~ app.run(sys.argv) diff --git a/uberwriter/previewer.py b/uberwriter/preview_handler.py similarity index 58% rename from uberwriter/previewer.py rename to uberwriter/preview_handler.py index 309db20..cf9afa3 100644 --- a/uberwriter/previewer.py +++ b/uberwriter/preview_handler.py @@ -5,12 +5,12 @@ from enum import auto, IntEnum import gi from uberwriter.helpers import get_builder +from uberwriter.preview_renderer import PreviewRenderer 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 @@ -20,38 +20,31 @@ class Step(IntEnum): RENDER = auto() -class Previewer: - def __init__(self, content, editor, text_view): - self.content = content - self.editor = editor +class PreviewHandler: + def __init__(self, window, content, editor, text_view): 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') + preview = builder.get_object("preview") + mode_button = builder.get_object("preview_mode_button") self.preview_converter = PreviewConverter() + self.preview_renderer = PreviewRenderer( + window, content, editor, text_view, preview, mode_button) 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 + self.shown = 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. @@ -80,41 +73,37 @@ class Previewer: self.web_view.load_html(html, 'file://localhost/') elif step == Step.RENDER: - # Last and one-time step: show the web view. - if self.showing: + # Last and one-time step: show the preview. + if self.shown: return - self.showing = True + self.shown = 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.web_view.set_scroll_scale(self.text_view.get_scroll_scale()) - self.preview.pack_start(self.web_view, True, True, 0) - self.content.add(self.preview) - self.web_view.show() + self.text_changed_handler_id =\ + self.text_view.get_buffer().connect("changed", self.__show) + 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.preview_renderer.show(self.web_view) + + def reload(self): + if self.shown: + self.show() def hide(self): - if self.showing: - self.showing = False + if self.shown: + self.shown = 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.text_view.set_scroll_scale(self.web_view.get_scroll_scale()) - self.content.remove(self.preview) - self.preview.remove(self.web_view) + self.text_view.get_buffer().disconnect(self.text_changed_handler_id) + self.text_view.disconnect(self.text_scroll_handler_id) + self.web_view.disconnect(self.web_scroll_handler_id) + + self.preview_renderer.hide(self.web_view) if self.loading: self.loading = False @@ -122,6 +111,9 @@ class Previewer: self.web_view.destroy() self.web_view = None + def update_preview_mode(self): + self.preview_renderer.update_mode(self.web_view) + def on_load_changed(self, _web_view, event): if event == WebKit2.LoadEvent.FINISHED: self.loading = False @@ -132,11 +124,11 @@ class Previewer: 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): + if self.shown and 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): + if self.shown and not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5): self.text_view.set_scroll_scale(scale) @staticmethod diff --git a/uberwriter/preview_renderer.py b/uberwriter/preview_renderer.py new file mode 100644 index 0000000..5283cac --- /dev/null +++ b/uberwriter/preview_renderer.py @@ -0,0 +1,127 @@ +from gettext import gettext as _ + +from gi.repository import Gtk, Gio, GLib + +from uberwriter.settings import Settings + + +class PreviewRenderer: + """Renders the preview according to the user selected mode.""" + + # Must match the order/index defined in gschema.xml + FULL_WIDTH = 0 + HALF_WIDTH = 1 + HALF_HEIGHT = 2 + WINDOWED = 3 + + def __init__(self, main_window, content, editor, text_view, preview, mode_button): + self.content = content + self.editor = editor + self.text_view = text_view + self.preview = preview + self.mode_button = mode_button + self.mode_button.connect("clicked", self.show_mode_popover) + self.popover = None + self.settings = Settings.new() + self.main_window = main_window + self.window = None + self.mode = self.settings.get_enum("preview-mode") + + def show(self, web_view): + self.preview.pack_start(web_view, True, True, 0) + + if self.mode == self.FULL_WIDTH: + self.content.remove(self.editor) + self.content.add(self.preview) + + elif self.mode == self.HALF_WIDTH: + self.content.set_orientation(Gtk.Orientation.HORIZONTAL) + self.content.set_size_request(self.text_view.get_min_width() * 2, -1) + self.content.add(self.preview) + + elif self.mode == self.HALF_HEIGHT: + self.content.set_orientation(Gtk.Orientation.VERTICAL) + self.content.set_size_request(-1, 800) + self.content.add(self.preview) + + elif self.mode == self.WINDOWED: + self.window = Gtk.Window(title=_("Preview")) + self.window.set_application(self.main_window.get_application()) + self.window.set_default_size( + self.main_window.get_allocated_width(), self.main_window.get_allocated_height()) + self.window.set_transient_for(self.main_window) + self.window.set_modal(False) + self.window.add(self.preview) + self.window.connect("delete-event", self.on_window_closed) + self.window.show() + + else: + raise ValueError("Unknown preview mode {}".format(self.mode)) + + web_view.show() + + def hide(self, web_view): + if self.mode == self.FULL_WIDTH: + self.content.remove(self.preview) + self.content.add(self.editor) + + elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT: + self.content.remove(self.preview) + self.content.set_size_request(-1, -1) + + elif self.mode == self.WINDOWED: + self.window.remove(self.preview) + self.window.destroy() + self.window = None + + else: + raise ValueError("Unknown preview mode {}".format(self.mode)) + + self.preview.remove(web_view) + + def update_mode(self, web_view): + mode = self.settings.get_enum("preview-mode") + if mode != self.mode: + if web_view: + self.hide(web_view) + self.mode = mode + self.show(web_view) + text = self.get_text_for_preview_mode(self.mode) + self.mode_button.set_label(text) + if self.popover: + self.popover.popdown() + + def show_mode_popover(self, _button): + self.mode_button.set_state_flags(Gtk.StateFlags.CHECKED, False) + + menu = Gio.Menu() + modes = self.settings.props.settings_schema.get_key("preview-mode").get_range()[1] + for i, mode in enumerate(modes): + menu_item = Gio.MenuItem.new(self.get_text_for_preview_mode(i), None) + menu_item.set_action_and_target_value("app.preview_mode", GLib.Variant.new_string(mode)) + menu.append_item(menu_item) + self.popover = Gtk.Popover.new_from_model(self.mode_button, menu) + self.popover.connect('closed', self.on_popover_closed) + self.popover.popup() + + def on_popover_closed(self, _popover): + self.mode_button.unset_state_flags(Gtk.StateFlags.CHECKED) + + self.popover = None + self.text_view.grab_focus() + + def on_window_closed(self, window, _event): + preview_action = window.get_application().lookup_action("preview") + preview_action.change_state(GLib.Variant.new_boolean(False)) + + def get_text_for_preview_mode(self, mode): + if mode == self.FULL_WIDTH: + return _("Full-Width") + elif mode == self.HALF_WIDTH: + return _("Half-Width") + elif mode == self.HALF_HEIGHT: + return _("Half-Height") + elif mode == self.WINDOWED: + return _("Windowed") + else: + raise ValueError("Unknown preview mode {}".format(mode)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py index 3129d85..1709366 100644 --- a/uberwriter/stats_handler.py +++ b/uberwriter/stats_handler.py @@ -9,6 +9,13 @@ from uberwriter.stats_counter import StatsCounter class StatsHandler: """Shows a default statistic on the stats button, and allows the user to toggle which one.""" + # Must match the order/index defined in gschema.xml + CHARACTERS = 0 + WORDS = 1 + SENTENCES = 2 + PARAGRAPHS = 3 + READ_TIME = 4 + def __init__(self, stats_button, text_view): super().__init__() @@ -28,7 +35,6 @@ class StatsHandler: self.read_time = (0, 0, 0) self.settings = Settings.new() - self.default_stat = self.settings.get_enum("stat-default") self.stats_counter = StatsCounter() @@ -59,15 +65,15 @@ class StatsHandler: self.update_stats) def get_text_for_stat(self, stat): - if stat == 0: + if stat == self.CHARACTERS: return _("{:n} Characters").format(self.characters) - elif stat == 1: + elif stat == self.WORDS: return _("{:n} Words").format(self.words) - elif stat == 2: + elif stat == self.SENTENCES: return _("{:n} Sentences").format(self.sentences) - elif stat == 3: + elif stat == self.PARAGRAPHS: return _("{:n} Paragraphs").format(self.paragraphs) - elif stat == 4: + elif stat == self.READ_TIME: return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time) else: raise ValueError("Unknown stat {}".format(stat)) diff --git a/uberwriter/web_view.py b/uberwriter/web_view.py index ca69c7a..cdfb9d5 100644 --- a/uberwriter/web_view.py +++ b/uberwriter/web_view.py @@ -16,7 +16,7 @@ class WebView(WebKit2.WebView): GET_SCROLL_SCALE_JS = """ e = document.documentElement; -e.scrollHeight > e.clientHeight ? e.scrollTop / (e.scrollHeight - e.clientHeight) : 0; +e.scrollHeight > e.clientHeight ? e.scrollTop / (e.scrollHeight - e.clientHeight) : -1; """ SET_SCROLL_SCALE_JS = """ @@ -37,10 +37,10 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; 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_dirty = False self.state_waiting = False self.timeout_id = None @@ -49,13 +49,13 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; return self.scroll_scale def set_scroll_scale(self, scale): - self.pending_scroll_scale = scale + self.state_dirty = True + self.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): @@ -72,9 +72,10 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.run_javascript( self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale) - def write_scroll_scale(self, scroll_scale): + def write_scroll_scale(self): + self.state_dirty = False self.run_javascript( - self.SET_SCROLL_SCALE_JS.format(scroll_scale), None, None) + self.SET_SCROLL_SCALE_JS.format(self.scroll_scale), None, None) def sync_scroll_scale(self, _web_view, result): self.state_waiting = False @@ -88,16 +89,15 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.timeout_id = None # Set scroll scale if specified, and the state is not dirty - if scroll_scale not in (None, self.scroll_scale): + if scroll_scale not in (None, -1, 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 self.state_dirty: + self.write_scroll_scale() if delay > 0: self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0) else: diff --git a/uberwriter/window.py b/uberwriter/window.py index fbdfb40..0f44e68 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -15,7 +15,6 @@ # END LICENSE import codecs -import locale import logging import os import urllib @@ -24,7 +23,7 @@ from gettext import gettext as _ import gi from uberwriter.export_dialog import Export -from uberwriter.previewer import Previewer +from uberwriter.preview_handler import PreviewHandler from uberwriter.stats_handler import StatsHandler from uberwriter.text_view import TextView @@ -112,13 +111,12 @@ class Window(Gtk.ApplicationWindow): # 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 content = builder.get_object('content') editor = builder.get_object('editor') - self.previewer = Previewer(content, editor, self.text_view) + self.preview_handler = PreviewHandler(self, content, editor, self.text_view) # Setup header/stats bar hide after 3 seconds self.top_bottom_bars_visible = True @@ -419,6 +417,9 @@ class Window(Gtk.ApplicationWindow): def update_default_stat(self): self.stats_handler.update_default_stat() + def update_preview_mode(self): + self.preview_handler.update_preview_mode() + def menu_toggle_sidebar(self, _widget=None): """WIP """ @@ -455,14 +456,14 @@ class Window(Gtk.ApplicationWindow): """ if state.get_boolean(): - self.previewer.show() + self.preview_handler.show() else: - self.previewer.hide() + self.preview_handler.hide() return True def reload_preview(self): - self.previewer.reload() + self.preview_handler.reload() def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" From dc0652e3edd809a2aeca42a563c5035ec6598012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 25 Apr 2019 23:50:23 +0100 Subject: [PATCH 11/27] Improve documentation for the new preview --- uberwriter/preview_handler.py | 7 ++++++- uberwriter/preview_renderer.py | 23 ++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/uberwriter/preview_handler.py b/uberwriter/preview_handler.py index cf9afa3..1bc9d54 100644 --- a/uberwriter/preview_handler.py +++ b/uberwriter/preview_handler.py @@ -21,6 +21,11 @@ class Step(IntEnum): class PreviewHandler: + """Handles showing/hiding the preview, and allows the user to toggle between modes. + + The rendering itself is handled by `PreviewRendered`. This class handles conversion/loading and + connects it all together (including synchronization, ie. text changes, scroll).""" + def __init__(self, window, content, editor, text_view): self.text_view = text_view @@ -73,7 +78,7 @@ class PreviewHandler: self.web_view.load_html(html, 'file://localhost/') elif step == Step.RENDER: - # Last and one-time step: show the preview. + # Last step: show the preview. This is a one-time step. if self.shown: return self.shown = True diff --git a/uberwriter/preview_renderer.py b/uberwriter/preview_renderer.py index 5283cac..5e91d23 100644 --- a/uberwriter/preview_renderer.py +++ b/uberwriter/preview_renderer.py @@ -28,22 +28,30 @@ class PreviewRenderer: self.mode = self.settings.get_enum("preview-mode") def show(self, web_view): + """Show the preview, depending on the currently selected mode.""" + self.preview.pack_start(web_view, True, True, 0) + # Full-width preview: swap editor with preview. if self.mode == self.FULL_WIDTH: self.content.remove(self.editor) self.content.add(self.preview) + # Half-width preview: set horizontal orientation and add the preview. + # Ask for a minimum width that respects the editor's minimum requirements. elif self.mode == self.HALF_WIDTH: self.content.set_orientation(Gtk.Orientation.HORIZONTAL) self.content.set_size_request(self.text_view.get_min_width() * 2, -1) self.content.add(self.preview) + # Half-height preview: set vertical orientation and add the preview. + # Ask for a minimum height that provides a comfortable experience. elif self.mode == self.HALF_HEIGHT: self.content.set_orientation(Gtk.Orientation.VERTICAL) - self.content.set_size_request(-1, 800) + self.content.set_size_request(-1, 768) self.content.add(self.preview) + # Windowed preview: create a window and show the preview in it. elif self.mode == self.WINDOWED: self.window = Gtk.Window(title=_("Preview")) self.window.set_application(self.main_window.get_application()) @@ -61,14 +69,21 @@ class PreviewRenderer: web_view.show() def hide(self, web_view): + """Hide the preview, depending on the currently selected mode.""" + + self.preview.remove(web_view) + + # Full-width preview: swap preview with editor. if self.mode == self.FULL_WIDTH: self.content.remove(self.preview) self.content.add(self.editor) + # Half-width/height previews: remove preview and reset size requirements. elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT: self.content.remove(self.preview) self.content.set_size_request(-1, -1) + # Windowed preview: remove preview and destroy window. elif self.mode == self.WINDOWED: self.window.remove(self.preview) self.window.destroy() @@ -77,9 +92,9 @@ class PreviewRenderer: else: raise ValueError("Unknown preview mode {}".format(self.mode)) - self.preview.remove(web_view) - def update_mode(self, web_view): + """Update preview mode, adjusting the mode button and the preview itself.""" + mode = self.settings.get_enum("preview-mode") if mode != self.mode: if web_view: @@ -92,6 +107,8 @@ class PreviewRenderer: self.popover.popdown() def show_mode_popover(self, _button): + """Show preview mode popover.""" + self.mode_button.set_state_flags(Gtk.StateFlags.CHECKED, False) menu = Gio.Menu() From 86cffc40ecc30de48b2b149ede7a0528bfb22a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 26 Apr 2019 03:12:54 +0100 Subject: [PATCH 12/27] Improve full-width preview workflow --- data/media/css/gtk/base.css | 57 +++++---- data/ui/Preview.ui | 2 +- data/ui/Window.ui | 2 +- uberwriter/__init__.py | 2 +- uberwriter/application.py | 6 +- uberwriter/headerbars.py | 37 +++++- uberwriter/{window.py => main_window.py} | 74 ++++-------- uberwriter/preview_handler.py | 40 ++++--- uberwriter/preview_renderer.py | 141 ++++++++++++++--------- uberwriter/styled_window.py | 37 ++++++ uberwriter/text_view_scroller.py | 3 - uberwriter/web_view.py | 19 +-- 12 files changed, 241 insertions(+), 179 deletions(-) rename uberwriter/{window.py => main_window.py} (89%) create mode 100644 uberwriter/styled_window.py diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css index 5d2711a..f92c69f 100644 --- a/data/media/css/gtk/base.css +++ b/data/media/css/gtk/base.css @@ -22,37 +22,11 @@ /* Main window and text colors */ .uberwriter-window { - /*border-radius: 7px 7px 3px 3px;*/ background: @theme_base_color; color: @theme_fg_color; caret-color: @theme_fg_color; } -.uberwriter-window .uberwriter-editor { - font-family: 'Fira Mono', monospace; - font-size: 16px; -} - -.uberwriter-window .uberwriter-editor.size14 { - font-size: 14px; -} - -.uberwriter-window .uberwriter-editor.size15 { - font-size: 15px; -} - -.uberwriter-window .uberwriter-editor.size16 { - font-size: 16px; -} - -.uberwriter-window .uberwriter-editor.size17 { - font-size: 17px; -} - -.uberwriter-window .uberwriter-editor.size18 { - font-size: 18px; -} - #titlebar-revealer { padding: 0; } @@ -67,10 +41,32 @@ } .uberwriter-editor { + -gtk-key-bindings: editor-bindings; border: none; background-color: transparent; text-decoration-color: @error_color; - -gtk-key-bindings: editor-bindings; + font-family: 'Fira Mono', monospace; + font-size: 16px; +} + +.uberwriter-editor.size14 { + font-size: 14px; +} + +.uberwriter-editor.size15 { + font-size: 15px; +} + +.uberwriter-editor.size16 { + font-size: 16px; +} + +.uberwriter-editor.size17 { + font-size: 17px; +} + +.uberwriter-editor.size18 { + font-size: 18px; } .uberwriter-editor text { @@ -96,9 +92,8 @@ padding: 0; } -.toggle-button { +.inline-button { color: alpha(@theme_fg_color, 0.6); - background-color: @theme_base_color; text-shadow: inherit; box-shadow: initial; background-clip: initial; @@ -116,8 +111,8 @@ transition: 100ms ease-in; } -.toggle-button:hover, -.toggle-button:checked { +.inline-button:hover, +.inline-button:checked { color: @theme_fg_color; background-color: mix(@theme_base_color, @theme_bg_color, 0.5); } diff --git a/data/ui/Preview.ui b/data/ui/Preview.ui index a084329..ddc354b 100644 --- a/data/ui/Preview.ui +++ b/data/ui/Preview.ui @@ -35,7 +35,7 @@ right True diff --git a/data/ui/Window.ui b/data/ui/Window.ui index 7f6e72c..c5e33dd 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -116,7 +116,7 @@ right True diff --git a/uberwriter/__init__.py b/uberwriter/__init__.py index 8632281..1bdceb7 100644 --- a/uberwriter/__init__.py +++ b/uberwriter/__init__.py @@ -19,7 +19,7 @@ import gi gi.require_version('Gtk', '3.0') -from uberwriter import window +from uberwriter import main_window from uberwriter import application from uberwriter.helpers import set_up_logging from uberwriter.config import get_version diff --git a/uberwriter/application.py b/uberwriter/application.py index 62e38f1..fdabda9 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -15,10 +15,12 @@ from gettext import gettext as _ import gi +from uberwriter.main_window import MainWindow + gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position from gi.repository import GLib, Gio, Gtk, GdkPixbuf -from uberwriter import window +from uberwriter import main_window from uberwriter.settings import Settings from uberwriter.helpers import set_up_logging from uberwriter.preferences_dialog import PreferencesDialog @@ -155,7 +157,7 @@ class Application(Gtk.Application): # Windows are associated with the application # when the last one is closed the application shuts down # self.window = Window(application=self, title="UberWriter") - self.window = window.Window(self) + self.window = MainWindow(self) if self.args: self.window.load_file(self.args[0]) diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index 110ba52..5b9ad75 100644 --- a/uberwriter/headerbars.py +++ b/uberwriter/headerbars.py @@ -48,8 +48,32 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods self.hb_container.add(self.hb_revealer) self.hb_container.show() - self.btns = buttons(app) - pack_buttons(self.hb, self.btns) + self.btns = main_buttons(app) + pack_main_buttons(self.hb, self.btns) + + self.hb.show_all() + + +class PreviewHeaderbar: + """Sets up the preview headerbar + """ + + def __init__(self): + self.hb = Gtk.HeaderBar().new() + self.hb.props.show_close_button = True + self.hb.get_style_context().add_class("titlebar") + + self.hb_revealer = Gtk.Revealer(name="titlebar-revealer") + self.hb_revealer.add(self.hb) + self.hb_revealer.props.transition_duration = 750 + self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE + self.hb_revealer.show() + self.hb_revealer.set_reveal_child(True) + + self.hb_container = Gtk.Frame(name="titlebar-container") + self.hb_container.set_shadow_type(Gtk.ShadowType.NONE) + self.hb_container.add(self.hb_revealer) + self.hb_container.show() self.hb.show_all() @@ -70,14 +94,14 @@ class FullscreenHeaderbar: self.hb.show() self.events.hide() - self.btns = buttons(app) + self.btns = main_buttons(app) fs_btn_exit = Gtk.Button().new_from_icon_name("view-restore-symbolic", Gtk.IconSize.BUTTON) fs_btn_exit.set_tooltip_text(_("Exit Fullscreen")) fs_btn_exit.set_action_name("app.fullscreen") - pack_buttons(self.hb, self.btns, fs_btn_exit) + pack_main_buttons(self.hb, self.btns, fs_btn_exit) self.hb.show_all() @@ -101,7 +125,8 @@ class FullscreenHeaderbar: else: self.revealer.set_reveal_child(False) -def buttons(app): + +def main_buttons(app): """constructor for the headerbar buttons Returns: @@ -154,7 +179,7 @@ def buttons(app): return btn -def pack_buttons(headerbar, btn, btn_exit=None): +def pack_main_buttons(headerbar, btn, btn_exit=None): """Pack the given buttons in the given headerbar Arguments: diff --git a/uberwriter/window.py b/uberwriter/main_window.py similarity index 89% rename from uberwriter/window.py rename to uberwriter/main_window.py index 0f44e68..7f9599d 100644 --- a/uberwriter/window.py +++ b/uberwriter/main_window.py @@ -15,6 +15,7 @@ # END LICENSE import codecs +import locale import logging import os import urllib @@ -25,6 +26,7 @@ import gi from uberwriter.export_dialog import Export from uberwriter.preview_handler import PreviewHandler from uberwriter.stats_handler import StatsHandler +from uberwriter.styled_window import StyledWindow from uberwriter.text_view import TextView gi.require_version('Gtk', '3.0') @@ -51,7 +53,7 @@ LOGGER = logging.getLogger('uberwriter') CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/") -class Window(Gtk.ApplicationWindow): +class MainWindow(StyledWindow): __gsignals__ = { 'save-file': (GObject.SIGNAL_ACTION, None, ()), 'open-file': (GObject.SIGNAL_ACTION, None, ()), @@ -65,14 +67,13 @@ class Window(Gtk.ApplicationWindow): def __init__(self, app): """Set up the main window""" - Gtk.ApplicationWindow.__init__(self, - application=Gio.Application.get_default(), - title="Uberwriter") + super().__init__(application=Gio.Application.get_default(), title="Uberwriter") + + self.get_style_context().add_class('uberwriter-window') # Set UI 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) @@ -150,34 +151,6 @@ class Window(Gtk.ApplicationWindow): ### self.searchreplace = SearchAndReplace(self, self.text_view, builder) - # Set current theme - self.apply_current_theme() - self.get_style_context().add_class('uberwriter-window') - - def apply_current_theme(self, *_): - """Adjusts the window, CSD and preview for the current theme. - """ - # Get current theme - theme, changed = Theme.get_current_changed() - if changed: - # Set theme variant (dark/light) - Gtk.Settings.get_default().set_property( - "gtk-application-prefer-dark-theme", - GLib.Variant("b", theme.is_dark)) - - # Set theme css - style_provider = Gtk.CssProvider() - style_provider.load_from_path(helpers.get_css_path("gtk/base.css")) - Gtk.StyleContext.add_provider_for_screen( - self.get_screen(), style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - # Reload preview if it exists - self.reload_preview() - - # Redraw contents of window - self.queue_draw() - def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and updates the title and the counters to reflect that @@ -534,15 +507,10 @@ class Window(Gtk.ApplicationWindow): True -- Gtk things """ - if (self.was_motion is False - and self.top_bottom_bars_visible + if (not self.was_motion and self.buffer_modified_for_status_bar - and self.text_view.props.has_focus): # pylint: disable=no-member - # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) - self.stats_revealer.set_reveal_child(False) - self.headerbar.hb_revealer.set_reveal_child(False) - self.top_bottom_bars_visible = False - self.buffer_modified_for_status_bar = False + and self.text_view.props.has_focus): + self.reveal_top_bottom_bars(False) self.was_motion = False return True @@ -560,24 +528,22 @@ class Window(Gtk.ApplicationWindow): return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar - if self.top_bottom_bars_visible is False: - self.stats_revealer.set_reveal_child(True) - self.headerbar.hb_revealer.set_reveal_child(True) - self.headerbar.hb.props.opacity = 1 - self.top_bottom_bars_visible = True - self.buffer_modified_for_status_bar = False - # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) + self.reveal_top_bottom_bars(True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ - if self.top_bottom_bars_visible is False: - self.stats_revealer.set_reveal_child(True) - self.headerbar.hb_revealer.set_reveal_child(True) - self.headerbar.hb.props.opacity = 1 - self.top_bottom_bars_visible = True - self.buffer_modified_for_status_bar = False + self.reveal_top_bottom_bars(True) + + def reveal_top_bottom_bars(self, reveal): + if self.top_bottom_bars_visible != reveal: + self.headerbar.hb_revealer.set_reveal_child(reveal) + self.stats_revealer.set_reveal_child(reveal) + for revealer in self.preview_handler.get_top_bottom_bar_revealers(): + revealer.set_reveal_child(reveal) + self.top_bottom_bars_visible = reveal + self.buffer_modified_for_status_bar = reveal def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the diff --git a/uberwriter/preview_handler.py b/uberwriter/preview_handler.py index 1bc9d54..cb00cd4 100644 --- a/uberwriter/preview_handler.py +++ b/uberwriter/preview_handler.py @@ -8,7 +8,7 @@ from uberwriter.helpers import get_builder from uberwriter.preview_renderer import PreviewRenderer gi.require_version('WebKit2', '4.0') -from gi.repository import WebKit2 +from gi.repository import WebKit2, GLib from uberwriter.preview_converter import PreviewConverter from uberwriter.web_view import WebView @@ -35,10 +35,13 @@ class PreviewHandler: builder = get_builder("Preview") preview = builder.get_object("preview") mode_button = builder.get_object("preview_mode_button") + self.mode_revealer = builder.get_object("preview_mode_revealer") self.preview_converter = PreviewConverter() self.preview_renderer = PreviewRenderer( - window, content, editor, text_view, preview, mode_button) + window, content, editor, text_view, preview, self.mode_revealer, mode_button) + + window.connect("style-updated", self.reload) self.web_scroll_handler_id = None self.text_scroll_handler_id = None @@ -83,18 +86,18 @@ class PreviewHandler: return self.shown = True - self.web_view.set_scroll_scale(self.text_view.get_scroll_scale()) - - self.text_changed_handler_id =\ - self.text_view.get_buffer().connect("changed", self.__show) - 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) + GLib.idle_add(self.web_view.set_scroll_scale, self.text_view.get_scroll_scale()) self.preview_renderer.show(self.web_view) - def reload(self): + self.text_changed_handler_id = \ + self.text_view.get_buffer().connect("changed", self.__show) + 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) + + def reload(self, *_): if self.shown: self.show() @@ -102,14 +105,14 @@ class PreviewHandler: if self.shown: self.shown = False - self.text_view.set_scroll_scale(self.web_view.get_scroll_scale()) + GLib.idle_add(self.text_view.set_scroll_scale, self.web_view.get_scroll_scale()) + + self.preview_renderer.hide(self.web_view) self.text_view.get_buffer().disconnect(self.text_changed_handler_id) self.text_view.disconnect(self.text_scroll_handler_id) self.web_view.disconnect(self.web_scroll_handler_id) - self.preview_renderer.hide(self.web_view) - if self.loading: self.loading = False @@ -119,6 +122,12 @@ class PreviewHandler: def update_preview_mode(self): self.preview_renderer.update_mode(self.web_view) + def get_top_bottom_bar_revealers(self): + if self.shown and not self.preview_renderer.window: + return [self.mode_revealer] + else: + return [] + def on_load_changed(self, _web_view, event): if event == WebKit2.LoadEvent.FINISHED: self.loading = False @@ -133,7 +142,8 @@ class PreviewHandler: self.web_view.set_scroll_scale(scale) def on_web_view_scrolled(self, _web_view, scale): - if self.shown and not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5): + if self.shown and self.text_view.get_mapped() and \ + not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5): self.text_view.set_scroll_scale(scale) @staticmethod diff --git a/uberwriter/preview_renderer.py b/uberwriter/preview_renderer.py index 5e91d23..99aba49 100644 --- a/uberwriter/preview_renderer.py +++ b/uberwriter/preview_renderer.py @@ -2,7 +2,9 @@ from gettext import gettext as _ from gi.repository import Gtk, Gio, GLib +from uberwriter import headerbars from uberwriter.settings import Settings +from uberwriter.styled_window import StyledWindow class PreviewRenderer: @@ -14,99 +16,126 @@ class PreviewRenderer: HALF_HEIGHT = 2 WINDOWED = 3 - def __init__(self, main_window, content, editor, text_view, preview, mode_button): + def __init__( + self, main_window, content, editor, text_view, preview, mode_revealer, mode_button): self.content = content self.editor = editor self.text_view = text_view self.preview = preview + self.mode_revealer = mode_revealer self.mode_button = mode_button self.mode_button.connect("clicked", self.show_mode_popover) self.popover = None self.settings = Settings.new() self.main_window = main_window + self.main_window.connect("delete-event", self.on_window_closed) self.window = None + self.headerbar = None self.mode = self.settings.get_enum("preview-mode") + self.update_mode() + def show(self, web_view): """Show the preview, depending on the currently selected mode.""" - self.preview.pack_start(web_view, True, True, 0) - - # Full-width preview: swap editor with preview. - if self.mode == self.FULL_WIDTH: - self.content.remove(self.editor) - self.content.add(self.preview) - - # Half-width preview: set horizontal orientation and add the preview. - # Ask for a minimum width that respects the editor's minimum requirements. - elif self.mode == self.HALF_WIDTH: - self.content.set_orientation(Gtk.Orientation.HORIZONTAL) - self.content.set_size_request(self.text_view.get_min_width() * 2, -1) - self.content.add(self.preview) - - # Half-height preview: set vertical orientation and add the preview. - # Ask for a minimum height that provides a comfortable experience. - elif self.mode == self.HALF_HEIGHT: - self.content.set_orientation(Gtk.Orientation.VERTICAL) - self.content.set_size_request(-1, 768) - self.content.add(self.preview) - # Windowed preview: create a window and show the preview in it. - elif self.mode == self.WINDOWED: - self.window = Gtk.Window(title=_("Preview")) - self.window.set_application(self.main_window.get_application()) - self.window.set_default_size( - self.main_window.get_allocated_width(), self.main_window.get_allocated_height()) - self.window.set_transient_for(self.main_window) - self.window.set_modal(False) - self.window.add(self.preview) + if self.mode == self.WINDOWED: + # Create transient window of the main window. + self.window = StyledWindow(application=self.main_window.get_application()) self.window.connect("delete-event", self.on_window_closed) + + # Create a custom header bar and move the mode button there. + headerbar = headerbars.PreviewHeaderbar() + self.headerbar = headerbar.hb + self.headerbar.set_title(_("Preview")) + self.mode_button.get_style_context().remove_class("inline-button") + self.mode_revealer.remove(self.mode_button) + self.headerbar.pack_end(self.mode_button) + self.window.set_titlebar(headerbar.hb_container) + + # Position it next to the main window. + width, height = self.main_window.get_size() + self.window.resize(width, height) + x, y = self.main_window.get_position() + if x is not None and y is not None: + self.main_window.move(x, y) + self.window.move(x + width + 16, y) + + # Add webview and show. + self.window.add(web_view) self.window.show() else: - raise ValueError("Unknown preview mode {}".format(self.mode)) + self.preview.pack_start(web_view, True, True, 0) + self.content.add(self.preview) + + # Full-width preview: swap editor with preview. + if self.mode == self.FULL_WIDTH: + self.content.remove(self.editor) + + # Half-width preview: set horizontal orientation and add the preview. + # Ask for a minimum width that respects the editor's minimum requirements. + elif self.mode == self.HALF_WIDTH: + self.content.set_orientation(Gtk.Orientation.HORIZONTAL) + self.content.set_size_request(self.text_view.get_min_width() * 2, -1) + + # Half-height preview: set vertical orientation and add the preview. + # Ask for a minimum height that provides a comfortable experience. + elif self.mode == self.HALF_HEIGHT: + self.content.set_orientation(Gtk.Orientation.VERTICAL) + self.content.set_size_request(-1, 768) + + else: + raise ValueError("Unknown preview mode {}".format(self.mode)) web_view.show() def hide(self, web_view): """Hide the preview, depending on the currently selected mode.""" - self.preview.remove(web_view) - - # Full-width preview: swap preview with editor. - if self.mode == self.FULL_WIDTH: - self.content.remove(self.preview) - self.content.add(self.editor) - - # Half-width/height previews: remove preview and reset size requirements. - elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT: - self.content.remove(self.preview) - self.content.set_size_request(-1, -1) - # Windowed preview: remove preview and destroy window. - elif self.mode == self.WINDOWED: - self.window.remove(self.preview) + if self.mode == self.WINDOWED: + self.main_window.present() + self.headerbar.remove(self.mode_button) + self.mode_button.get_style_context().add_class("inline-button") + self.mode_revealer.add(self.mode_button) + self.headerbar = None + self.window.remove(web_view) self.window.destroy() self.window = None else: - raise ValueError("Unknown preview mode {}".format(self.mode)) + self.preview.remove(web_view) + self.content.remove(self.preview) - def update_mode(self, web_view): + # Full-width preview: swap preview with editor. + if self.mode == self.FULL_WIDTH: + self.content.add(self.editor) + + # Half-width/height previews: remove preview and reset size requirements. + elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT: + self.content.set_size_request(-1, -1) + + else: + raise ValueError("Unknown preview mode {}".format(self.mode)) + + def update_mode(self, web_view=None): """Update preview mode, adjusting the mode button and the preview itself.""" mode = self.settings.get_enum("preview-mode") - if mode != self.mode: - if web_view: - self.hide(web_view) - self.mode = mode - self.show(web_view) + if web_view and mode != self.mode: + self.hide(web_view) + self.mode = mode + self.show(web_view) + else: + self.mode = mode + if self.mode_button: text = self.get_text_for_preview_mode(self.mode) self.mode_button.set_label(text) - if self.popover: - self.popover.popdown() + if self.popover: + self.popover.popdown() - def show_mode_popover(self, _button): + def show_mode_popover(self, button): """Show preview mode popover.""" self.mode_button.set_state_flags(Gtk.StateFlags.CHECKED, False) @@ -117,7 +146,7 @@ class PreviewRenderer: menu_item = Gio.MenuItem.new(self.get_text_for_preview_mode(i), None) menu_item.set_action_and_target_value("app.preview_mode", GLib.Variant.new_string(mode)) menu.append_item(menu_item) - self.popover = Gtk.Popover.new_from_model(self.mode_button, menu) + self.popover = Gtk.Popover.new_from_model(button, menu) self.popover.connect('closed', self.on_popover_closed) self.popover.popup() diff --git a/uberwriter/styled_window.py b/uberwriter/styled_window.py new file mode 100644 index 0000000..e88101e --- /dev/null +++ b/uberwriter/styled_window.py @@ -0,0 +1,37 @@ +import gi + +from uberwriter import helpers +from uberwriter.theme import Theme + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, GLib + + +class StyledWindow(Gtk.ApplicationWindow): + """A window that will redraw itself upon theme changes.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.connect("style-updated", self.apply_current_theme) + self.apply_current_theme() + + def apply_current_theme(self, *_): + """Adjusts the window, CSD and preview for the current theme.""" + # Get current theme + theme, changed = Theme.get_current_changed() + if changed: + # Set theme variant (dark/light) + Gtk.Settings.get_default().set_property( + "gtk-application-prefer-dark-theme", + GLib.Variant("b", theme.is_dark)) + + # Set theme css + style_provider = Gtk.CssProvider() + style_provider.load_from_path(helpers.get_css_path("gtk/base.css")) + Gtk.StyleContext.add_provider_for_screen( + self.get_screen(), style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + # Redraw contents of window + self.queue_draw() \ No newline at end of file diff --git a/uberwriter/text_view_scroller.py b/uberwriter/text_view_scroller.py index 70f4c95..b483359 100644 --- a/uberwriter/text_view_scroller.py +++ b/uberwriter/text_view_scroller.py @@ -1,6 +1,3 @@ -from gi.repository import Gtk - - class TextViewScroller: def __init__(self, text_view, scrolled_window): super().__init__() diff --git a/uberwriter/web_view.py b/uberwriter/web_view.py index cdfb9d5..412c3db 100644 --- a/uberwriter/web_view.py +++ b/uberwriter/web_view.py @@ -37,10 +37,10 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; 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_dirty = False self.state_waiting = False self.timeout_id = None @@ -49,13 +49,13 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; return self.scroll_scale def set_scroll_scale(self, scale): - self.state_dirty = True - self.scroll_scale = 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): @@ -72,10 +72,9 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.run_javascript( self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale) - def write_scroll_scale(self): - self.state_dirty = False + def write_scroll_scale(self, scroll_scale): self.run_javascript( - self.SET_SCROLL_SCALE_JS.format(self.scroll_scale), None, None) + self.SET_SCROLL_SCALE_JS.format(scroll_scale), None, None) def sync_scroll_scale(self, _web_view, result): self.state_waiting = False @@ -96,9 +95,11 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; # Handle the current state if not self.state_loaded or self.state_load_failed or self.state_waiting: return - if self.state_dirty: - self.write_scroll_scale() - if delay > 0: + if self.pending_scroll_scale: + self.write_scroll_scale(self.pending_scroll_scale) + self.pending_scroll_scale = None + self.read_scroll_scale() + elif delay > 0: self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0) else: self.read_scroll_scale() From 8ae2dfcb0b3a4804637c9cfe4c449c0c4c206e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 26 Apr 2019 03:13:14 +0100 Subject: [PATCH 13/27] Fix preview CSS --- data/media/css/web/adwaita_dark.css | 2 +- data/media/css/web/arc.css | 2 +- data/media/css/web/arc_dark.css | 2 +- data/media/css/web/arc_darker.css | 2 +- data/media/css/web/highcontrast.css | 2 +- data/media/css/web/highcontrast_inverse.css | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/media/css/web/adwaita_dark.css b/data/media/css/web/adwaita_dark.css index df71e44..afedc5c 100644 --- a/data/media/css/web/adwaita_dark.css +++ b/data/media/css/web/adwaita_dark.css @@ -1,4 +1,4 @@ -@import url("web/web__base.css"); +@import url("base.css"); :root { --text-color: #eeeeec; diff --git a/data/media/css/web/arc.css b/data/media/css/web/arc.css index 688fb87..7ee62ac 100644 --- a/data/media/css/web/arc.css +++ b/data/media/css/web/arc.css @@ -1,4 +1,4 @@ -@import url("web/web__base.css"); +@import url("base.css"); :root { --text-color: #3b3e45; diff --git a/data/media/css/web/arc_dark.css b/data/media/css/web/arc_dark.css index aeba9ac..f421845 100644 --- a/data/media/css/web/arc_dark.css +++ b/data/media/css/web/arc_dark.css @@ -1,4 +1,4 @@ -@import url("web/web__base.css"); +@import url("base.css"); :root { --text-color: #d3dae3; diff --git a/data/media/css/web/arc_darker.css b/data/media/css/web/arc_darker.css index 3eeede8..a2b855f 100644 --- a/data/media/css/web/arc_darker.css +++ b/data/media/css/web/arc_darker.css @@ -1 +1 @@ -@import url("web/web_arc.css"); \ No newline at end of file +@import url("arc.css"); diff --git a/data/media/css/web/highcontrast.css b/data/media/css/web/highcontrast.css index 8e12c4f..3a33d1f 100644 --- a/data/media/css/web/highcontrast.css +++ b/data/media/css/web/highcontrast.css @@ -1,4 +1,4 @@ -@import url("web/web__base.css"); +@import url("base.css"); a { text-decoration: underline; diff --git a/data/media/css/web/highcontrast_inverse.css b/data/media/css/web/highcontrast_inverse.css index e4ed265..3e5ac25 100644 --- a/data/media/css/web/highcontrast_inverse.css +++ b/data/media/css/web/highcontrast_inverse.css @@ -1,4 +1,4 @@ -@import url("web/web__base.css"); +@import url("base.css"); a { text-decoration: underline; From ebbbd730563f5a39d0e2cae5e51467fd4c68a7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 26 Apr 2019 03:17:44 +0100 Subject: [PATCH 14/27] Disable search during full-width preview --- uberwriter/search_and_replace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 462db2e..87920eb 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -96,7 +96,8 @@ class SearchAndReplace: """ show search box """ - if self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False: + if self.textview.get_mapped() and ( + self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False): self.searchbar.set_search_mode(True) self.box.set_reveal_child(True) self.searchentry.grab_focus() From e533bf190d96170d5104adf71d46857f35111065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 26 Apr 2019 06:06:51 +0100 Subject: [PATCH 15/27] Remove unused vadjustment property --- data/ui/Window.ui | 6 ------ 1 file changed, 6 deletions(-) diff --git a/data/ui/Window.ui b/data/ui/Window.ui index c5e33dd..79e1035 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -3,11 +3,6 @@ - - 100 - 1 - 10 - True False @@ -86,7 +81,6 @@ True True True - adjustment1 From 7ff1df43719cfd55804e5daacbcb3615f901f759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 00:43:06 +0100 Subject: [PATCH 16/27] Optimize markup update on margin/indent change Updating text tags is expensive, so avoid it unless necessary. A place where this was felt was when pasting large text. --- uberwriter/text_view_markup_handler.py | 34 +++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py index fd3a9a2..40a3da5 100644 --- a/uberwriter/text_view_markup_handler.py +++ b/uberwriter/text_view_markup_handler.py @@ -79,10 +79,10 @@ class MarkupHandler: strikethrough=False, justification=Gtk.Justification.LEFT) - self.table = buffer.create_tag('table') - self.table.set_property('wrap-mode', Gtk.WrapMode.NONE) - self.table.set_property('pixels-above-lines', 0) - self.table.set_property('pixels-below-lines', 0) + self.table = buffer.create_tag('table', + wrap_mode=Gtk.WrapMode.NONE, + pixels_above_lines=0, + pixels_below_lines=0) self.mathtext = buffer.create_tag('mathtext') @@ -94,6 +94,8 @@ class MarkupHandler: # Margin and indents # A baseline margin is set to allow negative offsets for formatting headers, lists, etc self.margins_indents = {} + self.baseline_margin = 0 + self.char_width = 0 self.update_margins_indents() # Style @@ -272,11 +274,10 @@ class MarkupHandler: def get_margin_indent_tag(self, margin_level, indent_level): level = (margin_level, indent_level) if level not in self.margins_indents: - tag = self.text_buffer.create_tag( - "margin_indent_" + str(margin_level) + "_" + str(indent_level)) margin, indent = self.get_margin_indent(margin_level, indent_level) - tag.set_property("left-margin", margin) - tag.set_property("indent", indent) + tag = self.text_buffer.create_tag( + "margin_indent_{}_{}".format(margin_level, indent_level), + left_margin=margin, indent=indent) self.margins_indents[level] = tag return tag else: @@ -284,7 +285,7 @@ class MarkupHandler: def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None): if baseline_margin is None: - baseline_margin = self.text_view.get_left_margin() + baseline_margin = self.text_view.props.left_margin if char_width is None: char_width = helpers.get_char_width(self.text_view) margin = max(baseline_margin + char_width * margin_level, 0) @@ -292,16 +293,21 @@ class MarkupHandler: return margin, indent def update_margins_indents(self): - baseline_margin = self.text_view.get_left_margin() + baseline_margin = self.text_view.props.left_margin char_width = helpers.get_char_width(self.text_view) - # Adjust tab size, as character width can change + # Bail out if neither the baseline margin nor character width change + if baseline_margin == self.baseline_margin and char_width == self.char_width: + return + self.baseline_margin = baseline_margin + self.char_width = char_width + + # Adjust tab size tab_array = Pango.TabArray.new(1, True) tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width) self.text_view.set_tabs(tab_array) - # Adjust margins and indents, as character width can change + # Adjust margins and indents for level, tag in self.margins_indents.items(): margin, indent = self.get_margin_indent(*level, baseline_margin, char_width) - tag.set_property("left-margin", margin) - tag.set_property("indent", indent) + tag.set_properties(left_margin=margin, indent=indent) From 7a2e6d5d8f1040bb0a658dfa3e50b04935fe5c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:03:46 +0100 Subject: [PATCH 17/27] Fix unwanted scroll while resizing the preview --- uberwriter/web_view.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uberwriter/web_view.py b/uberwriter/web_view.py index 412c3db..46130e8 100644 --- a/uberwriter/web_view.py +++ b/uberwriter/web_view.py @@ -34,6 +34,7 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.connect("load-changed", self.on_load_changed) self.connect("load-failed", self.on_load_failed) + self.connect("size-allocate", self.on_size_allocate) self.connect("destroy", self.on_destroy) self.scroll_scale = 0.0 @@ -63,6 +64,9 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.state_load_failed = True self.state_loop() + def on_size_allocate(self, *_): + self.set_scroll_scale(self.scroll_scale) + def on_destroy(self, _widget): self.state_loaded = False self.state_loop() From e80b61cf9df54ae80666cc35d7db06d386fd5910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:03:57 +0100 Subject: [PATCH 18/27] Work-around for unwanted scroll while resizing the editor The problem: When a TextView *with vertical margins set* is resized, it scrolls upwards automatically. It's not entirely clear why this happens, but removing the top/bottom margins fixes the issue entirely. The work-around: enforcing the scroll scale between a resize starting and the UI becoming idle again. This is a hack, and the experience is not great (the scroll is visibly unstable for a few ms), but it patches and old bug in UberWriter. The better solution: Figuring out how to prevent it from happening, either by somehow ensuring the TextView does not do this, or by approaching the layout differently where the margin is not set on the TextView itself. --- uberwriter/text_view.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index abbd6ec..51ad6af 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -1,4 +1,5 @@ import gi +from gi.repository.GObject import SignalMatchType from uberwriter.inline_preview import InlinePreview from uberwriter.text_view_format_inserter import FormatInserter @@ -106,6 +107,12 @@ class TextView(Gtk.TextView): self.hemingway_mode = False self.connect('key-press-event', self.on_key_press_event) + # While resizing the TextView, there is unwanted scroll upwards if a top margin is present. + # When a size allocation is detected, this variable will hold the scroll to re-set until the + # UI is idle again. + # TODO: Find a better way to handle unwanted scroll. + self.frozen_scroll_scale = None + def get_text(self): text_buffer = self.get_buffer() start_iter = text_buffer.get_start_iter() @@ -127,6 +134,11 @@ class TextView(Gtk.TextView): self.update_horizontal_margin() self.update_vertical_margin() self.markup.update_margins_indents() + self.queue_draw() + + # TODO: Find a better way to handle unwanted scroll on resize. + self.frozen_scroll_scale = self.get_scroll_scale() + GLib.idle_add(self.unfreeze_scroll_scale) def on_text_changed(self, *_): self.markup.apply() @@ -156,7 +168,14 @@ class TextView(Gtk.TextView): return False def on_scroll_scale_changed(self, *_): - self.emit("scroll-scale-changed", self.get_scroll_scale()) + if self.frozen_scroll_scale is not None: + self.set_scroll_scale(self.frozen_scroll_scale) + else: + self.emit("scroll-scale-changed", self.get_scroll_scale()) + + def unfreeze_scroll_scale(self): + self.frozen_scroll_scale = None + self.queue_draw() def set_focus_mode(self, focus_mode): """Toggle focus mode. From c2a43c374a0bbbcbd682537f264ae59f52ea789b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:11:25 +0100 Subject: [PATCH 19/27] Remove unused class --- uberwriter/main_window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uberwriter/main_window.py b/uberwriter/main_window.py index 7f9599d..794033c 100644 --- a/uberwriter/main_window.py +++ b/uberwriter/main_window.py @@ -99,7 +99,6 @@ class MainWindow(StyledWindow): self.add_accel_group(self.accel_group) self.scrolled_window = builder.get_object('editor_scrolledwindow') - self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window') # Setup text editor self.text_view = TextView(self.settings.get_int("characters-per-line")) From a0a19ffbe737e8af10b2f3b26c6cb0f1f7cef096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:13:48 +0100 Subject: [PATCH 20/27] Replace get_value(X).get_Y() with get_Y(X) for Settings --- uberwriter/inline_preview.py | 3 +-- uberwriter/preferences_dialog.py | 2 +- uberwriter/theme.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/uberwriter/inline_preview.py b/uberwriter/inline_preview.py index 75dbaee..02bf115 100644 --- a/uberwriter/inline_preview.py +++ b/uberwriter/inline_preview.py @@ -457,8 +457,7 @@ class InlinePreview: path = path[7:] elif not path.startswith("/"): # then the path is relative - base_path = self.settings.get_value( - "open-file-path").get_string() + base_path = self.settings.get_string("open-file-path") path = base_path + "/" + path LOGGER.info(path) diff --git a/uberwriter/preferences_dialog.py b/uberwriter/preferences_dialog.py index fffda4b..190d5a1 100644 --- a/uberwriter/preferences_dialog.py +++ b/uberwriter/preferences_dialog.py @@ -83,7 +83,7 @@ class PreferencesDialog: self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay) input_format_store = Gtk.ListStore(int, str) - input_format = self.settings.get_value("input-format").get_string() + input_format = self.settings.get_string("input-format") input_format_active = 0 for i, fmt in enumerate(self.formats): input_format_store.append([i, fmt["name"]]) diff --git a/uberwriter/theme.py b/uberwriter/theme.py index db47156..2c11e2f 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -31,8 +31,8 @@ class Theme: @classmethod def get_current_changed(cls): theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name') - dark_mode_auto = cls.settings.get_value('dark-mode-auto').get_boolean() - dark_mode = cls.settings.get_value('dark-mode').get_boolean() + dark_mode_auto = cls.settings.get_boolean('dark-mode-auto') + dark_mode = cls.settings.get_boolean('dark-mode') current_theme = cls.get_for_name(theme_name) if not dark_mode_auto and dark_mode != current_theme.is_dark and current_theme.inverse_name: current_theme = cls.get_for_name(current_theme.inverse_name, current_theme.name) From db652ef84f4d2f9fa1aed51af42fc8d4bdfd674f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:17:45 +0100 Subject: [PATCH 21/27] Ensure text view is focused when toggling preview Consistent with other menu options. --- uberwriter/main_window.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/uberwriter/main_window.py b/uberwriter/main_window.py index 794033c..460374c 100644 --- a/uberwriter/main_window.py +++ b/uberwriter/main_window.py @@ -173,19 +173,16 @@ class MainWindow(StyledWindow): if state.get_boolean(): self.fullscreen() self.fs_headerbar.events.show() - else: self.unfullscreen() self.fs_headerbar.events.hide() - self.text_view.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ - focus_mode = state.get_boolean() - self.text_view.set_focus_mode(focus_mode) + self.text_view.set_focus_mode(state.get_boolean()) self.text_view.grab_focus() def set_hemingway_mode(self, state): @@ -195,6 +192,22 @@ class MainWindow(StyledWindow): self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() + def toggle_preview(self, state): + """Toggle the preview mode + + Arguments: + state {gtk bool} -- Desired state of the preview mode (enabled/disabled) + """ + + if state.get_boolean(): + self.text_view.grab_focus() + self.preview_handler.show() + else: + self.preview_handler.hide() + self.text_view.grab_focus() + + return True + # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document @@ -420,20 +433,6 @@ class MainWindow(StyledWindow): elif self.overlay_id: self.scrolled_window.disconnect(self.overlay_id) - def toggle_preview(self, state): - """Toggle the preview mode - - Arguments: - state {gtk bool} -- Desired state of the preview mode (enabled/disabled) - """ - - if state.get_boolean(): - self.preview_handler.show() - else: - self.preview_handler.hide() - - return True - def reload_preview(self): self.preview_handler.reload() From 939edcc762a2261387c862e564a5f13ca7ac4228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:56:26 +0100 Subject: [PATCH 22/27] Allow disabling scroll sync --- data/de.wolfvollprecht.UberWriter.gschema.xml | 7 ++++ data/ui/Preferences.ui | 41 +++++++++++++++---- uberwriter/application.py | 2 + uberwriter/main_window.py | 4 +- uberwriter/preferences_dialog.py | 8 ++++ uberwriter/preview_handler.py | 36 ++++++++++------ uberwriter/preview_renderer.py | 9 ++-- 7 files changed, 82 insertions(+), 25 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index c9e45ec..664870c 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -48,6 +48,13 @@ It can cause performance problems to some users. + + true + Synchronize editor/preview scrolling + + Keep the editor and preview scroll positions in sync. + + "markdown" Input format diff --git a/data/ui/Preferences.ui b/data/ui/Preferences.ui index b057517..d02cdd7 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -138,11 +138,11 @@ - + True False start - Input format + Synchronize editor/preview scrolling right @@ -151,17 +151,29 @@ - + True True - False - help + end - 1 + 2 4 + + + True + False + start + Input format + right + + + 0 + 5 + + True @@ -172,9 +184,24 @@ 2 - 4 + 5 + + + True + True + False + help + + + 1 + 5 + + + + + diff --git a/uberwriter/application.py b/uberwriter/application.py index fdabda9..449deda 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -188,6 +188,8 @@ class Application(Gtk.Application): self.window.toggle_gradient_overlay(settings.get_value(key)) elif key == "input-format": self.window.reload_preview() + elif key == "sync-scroll": + self.window.reload_preview(reshow=True) elif key == "stat-default": self.window.update_default_stat() elif key == "preview-mode": diff --git a/uberwriter/main_window.py b/uberwriter/main_window.py index 460374c..b8db702 100644 --- a/uberwriter/main_window.py +++ b/uberwriter/main_window.py @@ -433,8 +433,8 @@ class MainWindow(StyledWindow): elif self.overlay_id: self.scrolled_window.disconnect(self.overlay_id) - def reload_preview(self): - self.preview_handler.reload() + def reload_preview(self, reshow=False): + self.preview_handler.reload(reshow=reshow) def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" diff --git a/uberwriter/preferences_dialog.py b/uberwriter/preferences_dialog.py index 190d5a1..b875b1e 100644 --- a/uberwriter/preferences_dialog.py +++ b/uberwriter/preferences_dialog.py @@ -82,6 +82,10 @@ class PreferencesDialog: self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay")) self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay) + self.sync_scroll_switch = self.builder.get_object("sync_scroll_switch") + self.sync_scroll_switch.set_active(self.settings.get_value("sync-scroll")) + self.sync_scroll_switch.connect("state-set", self.on_sync_scroll) + input_format_store = Gtk.ListStore(int, str) input_format = self.settings.get_string("input-format") input_format_active = 0 @@ -126,6 +130,10 @@ class PreferencesDialog: self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(state)) return False + def on_sync_scroll(self, _, state): + self.settings.set_value("sync-scroll", GLib.Variant.new_boolean(state)) + return False + def on_input_format(self, combobox): fmt = self.formats[combobox.get_active()] self.settings.set_value("input-format", GLib.Variant.new_string(fmt["format"])) diff --git a/uberwriter/preview_handler.py b/uberwriter/preview_handler.py index cb00cd4..7110e44 100644 --- a/uberwriter/preview_handler.py +++ b/uberwriter/preview_handler.py @@ -6,6 +6,7 @@ import gi from uberwriter.helpers import get_builder from uberwriter.preview_renderer import PreviewRenderer +from uberwriter.settings import Settings gi.require_version('WebKit2', '4.0') from gi.repository import WebKit2, GLib @@ -43,9 +44,11 @@ class PreviewHandler: window.connect("style-updated", self.reload) + self.text_changed_handler_id = None + + self.settings = Settings.new() self.web_scroll_handler_id = None self.text_scroll_handler_id = None - self.text_changed_handler_id = None self.loading = False self.shown = False @@ -78,7 +81,7 @@ class PreviewHandler: if self.web_view.is_loading(): self.web_view_pending_html = html else: - self.web_view.load_html(html, 'file://localhost/') + self.web_view.load_html(html, "file://localhost/") elif step == Step.RENDER: # Last step: show the preview. This is a one-time step. @@ -86,32 +89,41 @@ class PreviewHandler: return self.shown = True + self.text_changed_handler_id = \ + self.text_view.get_buffer().connect("changed", self.__show) + GLib.idle_add(self.web_view.set_scroll_scale, self.text_view.get_scroll_scale()) self.preview_renderer.show(self.web_view) - self.text_changed_handler_id = \ - self.text_view.get_buffer().connect("changed", self.__show) - 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) + if self.settings.get_boolean("sync-scroll"): + 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) - def reload(self, *_): + def reload(self, *_widget, reshow=False): if self.shown: + if reshow: + self.hide() self.show() def hide(self): if self.shown: self.shown = False + self.text_view.get_buffer().disconnect(self.text_changed_handler_id) + GLib.idle_add(self.text_view.set_scroll_scale, self.web_view.get_scroll_scale()) self.preview_renderer.hide(self.web_view) - self.text_view.get_buffer().disconnect(self.text_changed_handler_id) - self.text_view.disconnect(self.text_scroll_handler_id) - self.web_view.disconnect(self.web_scroll_handler_id) + if self.text_scroll_handler_id: + self.text_view.disconnect(self.text_scroll_handler_id) + self.text_scroll_handler_id = None + if self.web_scroll_handler_id: + self.web_view.disconnect(self.web_scroll_handler_id) + self.web_scroll_handler_id = None if self.loading: self.loading = False diff --git a/uberwriter/preview_renderer.py b/uberwriter/preview_renderer.py index 99aba49..2185486 100644 --- a/uberwriter/preview_renderer.py +++ b/uberwriter/preview_renderer.py @@ -18,6 +18,8 @@ class PreviewRenderer: def __init__( self, main_window, content, editor, text_view, preview, mode_revealer, mode_button): + self.main_window = main_window + self.main_window.connect("delete-event", self.on_window_closed) self.content = content self.editor = editor self.text_view = text_view @@ -25,14 +27,13 @@ class PreviewRenderer: self.mode_revealer = mode_revealer self.mode_button = mode_button self.mode_button.connect("clicked", self.show_mode_popover) - self.popover = None + self.settings = Settings.new() - self.main_window = main_window - self.main_window.connect("delete-event", self.on_window_closed) + self.popover = None self.window = None self.headerbar = None - self.mode = self.settings.get_enum("preview-mode") + self.mode = self.settings.get_enum("preview-mode") self.update_mode() def show(self, web_view): From c2d0bde9f87956da4bc91ec3df41f982ac2601cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 04:58:54 +0100 Subject: [PATCH 23/27] Replace set_value(X, type(Y)) with set_type(X, Y) for Settings --- uberwriter/main_window.py | 2 +- uberwriter/preferences_dialog.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/uberwriter/main_window.py b/uberwriter/main_window.py index b8db702..11bfc96 100644 --- a/uberwriter/main_window.py +++ b/uberwriter/main_window.py @@ -605,4 +605,4 @@ class MainWindow(StyledWindow): else: self.filename = None base_path = "/" - self.settings.set_value("open-file-path", GLib.Variant("s", base_path)) + self.settings.set_string("open-file-path", base_path) diff --git a/uberwriter/preferences_dialog.py b/uberwriter/preferences_dialog.py index b875b1e..921c09d 100644 --- a/uberwriter/preferences_dialog.py +++ b/uberwriter/preferences_dialog.py @@ -111,32 +111,32 @@ class PreferencesDialog: preferences_window.show() def on_dark_mode_auto(self, _, state): - self.settings.set_value("dark-mode-auto", GLib.Variant.new_boolean(state)) + self.settings.set_boolean("dark-mode-auto", state) if state and self.dark_mode_switch.get_active(): self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False)) return False def on_dark_mode(self, _, state): - self.settings.set_value("dark-mode", GLib.Variant.new_boolean(state)) + self.settings.set_boolean("dark-mode", state) if state and self.dark_mode_auto_switch.get_active(): self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False)) return False def on_spellcheck(self, _, state): - self.settings.set_value("spellcheck", GLib.Variant.new_boolean(state)) + self.settings.set_boolean("spellcheck", state) return False def on_gradient_overlay(self, _, state): - self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(state)) + self.settings.set_boolean("gradient-overlay", state) return False def on_sync_scroll(self, _, state): - self.settings.set_value("sync-scroll", GLib.Variant.new_boolean(state)) + self.settings.set_boolean("sync-scroll", state) return False def on_input_format(self, combobox): fmt = self.formats[combobox.get_active()] - self.settings.set_value("input-format", GLib.Variant.new_string(fmt["format"])) + self.settings.set_string("input-format", fmt["format"]) def on_input_format_help(self, _): fmt = self.formats[self.input_format_combobox.get_active()] From 250519f3193bc940f15b171aeefa419c74fd6678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 27 Apr 2019 05:11:08 +0100 Subject: [PATCH 24/27] Improve gspell_view encapsulation inside TextView --- uberwriter/main_window.py | 4 +--- uberwriter/text_view.py | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/uberwriter/main_window.py b/uberwriter/main_window.py index 11bfc96..1d2884d 100644 --- a/uberwriter/main_window.py +++ b/uberwriter/main_window.py @@ -417,9 +417,7 @@ class MainWindow(StyledWindow): status {gtk bool} -- Desired status of the spellchecking """ - self.text_view.gspell_view\ - .set_inline_spell_checking(state.get_boolean() - and not self.text_view.focus_mode) + self.text_view.set_spellcheck(state.get_boolean()) def toggle_gradient_overlay(self, state): """Toggle the gradient overlay diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 51ad6af..ea16989 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -65,6 +65,7 @@ class TextView(Gtk.TextView): self.get_buffer().connect('changed', self.on_text_changed) # Spell checking + self.spellcheck = True self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self) self.gspell_view.basic_setup() @@ -184,10 +185,14 @@ class TextView(Gtk.TextView): and the surrounding text is greyed out.""" self.focus_mode = focus_mode - self.gspell_view.set_inline_spell_checking(not focus_mode) self.update_vertical_margin() self.markup.apply() self.smooth_scroll_to() + self.set_spellcheck(self.spellcheck) + + def set_spellcheck(self, spellcheck): + self.spellcheck = spellcheck + self.gspell_view.set_inline_spell_checking(self.spellcheck and not self.focus_mode) def update_horizontal_margin(self): width = self.get_allocation().width From 16382d9574ab380bfab449ab8477ac61a864465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 4 May 2019 16:29:51 +0100 Subject: [PATCH 25/27] Remove non-Python requirements from requirements.txt --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0a7f6c0..10e2313 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ regex -enchant -pypandoc==1.4 -pyenchant -pygtkspellcheck +pypandoc==1.4 \ No newline at end of file From e39694571d4feac14796377db19262988d36a7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 8 May 2019 03:12:59 +0100 Subject: [PATCH 26/27] Fix file paths `path_to_file`'s argument is an absolute path, which already contains a leading `/`. Having an additional slash (ie. `file:////some/path`) actually breaks things, eg. the export flow using local assets. --- uberwriter/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py index e7f2c03..d7fa007 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -55,7 +55,7 @@ def get_builder(builder_file_name): def path_to_file(path): """Return a file path (file:///) for the given path""" - return "file:///" + path + return "file://" + path def get_media_file(media_file_path): From c5abc531e097c45c4ec4d97bf5aaafc9686ef937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 8 May 2019 04:11:35 +0100 Subject: [PATCH 27/27] Fix scrolling glitch during preview Typing while in preview mode would occasionally lead to a scrolling glitch, where scroll would briefly be at 0, before jumping to the location it was supposed to be in in the first case. This happened due to the async nature of JS calls, in the following scenario: 1. Load 2. Read started 3. Read finished 4. Read started 5. Load 6. Read finished The results from op 4 would be invalid due to loading in-between, and handling the result in 6 would set the wrong scroll value. This change ensures results are discarded whenever we are waiting for them and a new load starts. --- uberwriter/web_view.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/uberwriter/web_view.py b/uberwriter/web_view.py index 46130e8..d4bc6ae 100644 --- a/uberwriter/web_view.py +++ b/uberwriter/web_view.py @@ -42,6 +42,7 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; self.state_loaded = False self.state_load_failed = False + self.state_discard_result = False self.state_waiting = False self.timeout_id = None @@ -56,6 +57,7 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; 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_result = event == WebKit2.LoadEvent.STARTED and self.state_waiting self.pending_scroll_scale = self.scroll_scale self.state_loop() @@ -74,16 +76,20 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale; def read_scroll_scale(self): self.state_waiting = True self.run_javascript( - self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale) + self.GET_SCROLL_SCALE_JS, None, self.finish_read_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): + def finish_read_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()) + if not self.state_discard_result: + result = self.run_javascript_finish(result) + 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 # Remove any pending callbacks