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] 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."""