Add toggle between various preview modes

Including:
* Full-width (original)
* Half-width
* Half-height
* Windowed
github/fork/yochananmarqos/patch-1
Gonçalo Silva 2019-04-25 23:41:43 +01:00
parent f72f61ae7d
commit 65e7028843
10 changed files with 230 additions and 76 deletions

View File

@ -10,6 +10,13 @@
<value nick='read_time' value='4' /> <value nick='read_time' value='4' />
</enum> </enum>
<enum id='de.wolfvollprecht.UberWriter.PreviewMode'>
<value nick='full-width' value='0' />
<value nick='half-width' value='1' />
<value nick='half-height' value='2' />
<value nick='windowed' value='3' />
</enum>
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter"> <schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
<key name='dark-mode-auto' type='b'> <key name='dark-mode-auto' type='b'>
@ -76,11 +83,11 @@
Maximum number of characters per line within the editor. Maximum number of characters per line within the editor.
</description> </description>
</key> </key>
<key name='preview-side-by-side' type='b'> <key name='preview-mode' enum='de.wolfvollprecht.UberWriter.PreviewMode'>
<default>false</default> <default>"full-width"</default>
<summary>Side-by-side preview</summary> <summary>Preview mode</summary>
<description> <description>
Show the preview side by side, instead of full-width. How to display the preview.
</description> </description>
</key> </key>

View File

@ -96,7 +96,7 @@
padding: 0; padding: 0;
} }
.stats-button { .toggle-button {
color: alpha(@theme_fg_color, 0.6); color: alpha(@theme_fg_color, 0.6);
background-color: @theme_base_color; background-color: @theme_base_color;
text-shadow: inherit; text-shadow: inherit;
@ -116,8 +116,8 @@
transition: 100ms ease-in; transition: 100ms ease-in;
} }
.stats-button :hover, .toggle-button:hover,
.stats-button :checked { .toggle-button:checked {
color: @theme_fg_color; color: @theme_fg_color;
background-color: mix(@theme_base_color, @theme_bg_color, 0.5); background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
} }

View File

@ -11,6 +11,7 @@
<object class="GtkBox" id="preview"> <object class="GtkBox" id="preview">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>
<placeholder/> <placeholder/>
@ -33,6 +34,9 @@
<property name="image">pan-down</property> <property name="image">pan-down</property>
<property name="image_position">right</property> <property name="image_position">right</property>
<property name="always_show_image">True</property> <property name="always_show_image">True</property>
<style>
<class name="toggle-button"/>
</style>
</object> </object>
</child> </child>
</object> </object>

View File

@ -115,6 +115,9 @@
<property name="image">pan-down</property> <property name="image">pan-down</property>
<property name="image_position">right</property> <property name="image_position">right</property>
<property name="always_show_image">True</property> <property name="always_show_image">True</property>
<style>
<class name="toggle-button"/>
</style>
</object> </object>
</child> </child>
</object> </object>

View File

@ -120,10 +120,18 @@ class Application(Gtk.Application):
stat_default = self.settings.get_string("stat-default") stat_default = self.settings.get_string("stat-default")
action = Gio.SimpleAction.new_stateful( 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) action.connect("activate", self.on_stat_default)
self.add_action(action) 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 # Shortcuts
# TODO: be aware that a couple of shortcuts are defined in base.css # 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() self.window.reload_preview()
elif key == "stat-default": elif key == "stat-default":
self.window.update_default_stat() self.window.update_default_stat()
elif key == "preview-mode":
self.window.update_preview_mode()
def on_new(self, _action, _value): def on_new(self, _action, _value):
self.window.new_document() self.window.new_document()
@ -250,6 +260,10 @@ class Application(Gtk.Application):
action.set_state(value) action.set_state(value)
self.settings.set_string("stat-default", value.get_string()) 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__": # ~ if __name__ == "__main__":
# ~ app = Application() # ~ app = Application()
# ~ app.run(sys.argv) # ~ app.run(sys.argv)

View File

@ -5,12 +5,12 @@ from enum import auto, IntEnum
import gi import gi
from uberwriter.helpers import get_builder from uberwriter.helpers import get_builder
from uberwriter.preview_renderer import PreviewRenderer
gi.require_version('WebKit2', '4.0') gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2 from gi.repository import WebKit2
from uberwriter.preview_converter import PreviewConverter from uberwriter.preview_converter import PreviewConverter
from uberwriter.settings import Settings
from uberwriter.web_view import WebView from uberwriter.web_view import WebView
@ -20,38 +20,31 @@ class Step(IntEnum):
RENDER = auto() RENDER = auto()
class Previewer: class PreviewHandler:
def __init__(self, content, editor, text_view): def __init__(self, window, content, editor, text_view):
self.content = content
self.editor = editor
self.text_view = text_view self.text_view = text_view
self.web_view = None self.web_view = None
self.web_view_pending_html = None self.web_view_pending_html = None
builder = get_builder("Preview") builder = get_builder("Preview")
self.preview = builder.get_object("preview") preview = builder.get_object("preview")
self.preview_mode_button = builder.get_object("preview_mode_button") mode_button = builder.get_object("preview_mode_button")
self.preview_mode_button.get_style_context().add_class('toggle-button')
self.preview_converter = PreviewConverter() self.preview_converter = PreviewConverter()
self.preview_renderer = PreviewRenderer(
window, content, editor, text_view, preview, mode_button)
self.web_scroll_handler_id = None self.web_scroll_handler_id = None
self.text_scroll_handler_id = None self.text_scroll_handler_id = None
self.text_changed_handler_id = None self.text_changed_handler_id = None
self.settings = Settings.new()
self.loading = False self.loading = False
self.showing = False self.shown = False
def show(self): def show(self):
self.__show() self.__show()
def reload(self):
if self.showing:
self.show()
def __show(self, html=None, step=Step.CONVERT_HTML): def __show(self, html=None, step=Step.CONVERT_HTML):
if step == Step.CONVERT_HTML: if step == Step.CONVERT_HTML:
# First step: convert text to HTML. # First step: convert text to HTML.
@ -80,41 +73,37 @@ class Previewer:
self.web_view.load_html(html, 'file://localhost/') self.web_view.load_html(html, 'file://localhost/')
elif step == Step.RENDER: elif step == Step.RENDER:
# Last and one-time step: show the web view. # Last and one-time step: show the preview.
if self.showing: if self.shown:
return return
self.showing = True self.shown = True
if self.settings.get_boolean("preview-side-by-side"): self.web_view.set_scroll_scale(self.text_view.get_scroll_scale())
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.text_changed_handler_id =\
self.content.add(self.preview) self.text_view.get_buffer().connect("changed", self.__show)
self.web_view.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): def hide(self):
if self.showing: if self.shown:
self.showing = False self.shown = False
if self.settings.get_boolean("preview-side-by-side"): self.text_view.set_scroll_scale(self.web_view.get_scroll_scale())
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.text_view.get_buffer().disconnect(self.text_changed_handler_id)
self.preview.remove(self.web_view) 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: if self.loading:
self.loading = False self.loading = False
@ -122,6 +111,9 @@ class Previewer:
self.web_view.destroy() self.web_view.destroy()
self.web_view = None 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): def on_load_changed(self, _web_view, event):
if event == WebKit2.LoadEvent.FINISHED: if event == WebKit2.LoadEvent.FINISHED:
self.loading = False self.loading = False
@ -132,11 +124,11 @@ class Previewer:
self.__show(step=Step.RENDER) self.__show(step=Step.RENDER)
def on_text_view_scrolled(self, _text_view, scale): 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) self.web_view.set_scroll_scale(scale)
def on_web_view_scrolled(self, _web_view, 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) self.text_view.set_scroll_scale(scale)
@staticmethod @staticmethod

View File

@ -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))

View File

@ -9,6 +9,13 @@ from uberwriter.stats_counter import StatsCounter
class StatsHandler: class StatsHandler:
"""Shows a default statistic on the stats button, and allows the user to toggle which one.""" """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): def __init__(self, stats_button, text_view):
super().__init__() super().__init__()
@ -28,7 +35,6 @@ class StatsHandler:
self.read_time = (0, 0, 0) self.read_time = (0, 0, 0)
self.settings = Settings.new() self.settings = Settings.new()
self.default_stat = self.settings.get_enum("stat-default")
self.stats_counter = StatsCounter() self.stats_counter = StatsCounter()
@ -59,15 +65,15 @@ class StatsHandler:
self.update_stats) self.update_stats)
def get_text_for_stat(self, stat): def get_text_for_stat(self, stat):
if stat == 0: if stat == self.CHARACTERS:
return _("{:n} Characters").format(self.characters) return _("{:n} Characters").format(self.characters)
elif stat == 1: elif stat == self.WORDS:
return _("{:n} Words").format(self.words) return _("{:n} Words").format(self.words)
elif stat == 2: elif stat == self.SENTENCES:
return _("{:n} Sentences").format(self.sentences) return _("{:n} Sentences").format(self.sentences)
elif stat == 3: elif stat == self.PARAGRAPHS:
return _("{:n} Paragraphs").format(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) return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time)
else: else:
raise ValueError("Unknown stat {}".format(stat)) raise ValueError("Unknown stat {}".format(stat))

View File

@ -16,7 +16,7 @@ class WebView(WebKit2.WebView):
GET_SCROLL_SCALE_JS = """ GET_SCROLL_SCALE_JS = """
e = document.documentElement; 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 = """ SET_SCROLL_SCALE_JS = """
@ -37,10 +37,10 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.connect("destroy", self.on_destroy) self.connect("destroy", self.on_destroy)
self.scroll_scale = 0.0 self.scroll_scale = 0.0
self.pending_scroll_scale = None
self.state_loaded = False self.state_loaded = False
self.state_load_failed = False self.state_load_failed = False
self.state_dirty = False
self.state_waiting = False self.state_waiting = False
self.timeout_id = None self.timeout_id = None
@ -49,13 +49,13 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
return self.scroll_scale return self.scroll_scale
def set_scroll_scale(self, scale): def set_scroll_scale(self, scale):
self.pending_scroll_scale = scale self.state_dirty = True
self.scroll_scale = scale
self.state_loop() self.state_loop()
def on_load_changed(self, _web_view, event): def on_load_changed(self, _web_view, event):
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
self.state_load_failed = False self.state_load_failed = False
self.pending_scroll_scale = self.scroll_scale
self.state_loop() self.state_loop()
def on_load_failed(self, _web_view, _event): def on_load_failed(self, _web_view, _event):
@ -72,9 +72,10 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.run_javascript( self.run_javascript(
self.GET_SCROLL_SCALE_JS, None, self.sync_scroll_scale) 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.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): def sync_scroll_scale(self, _web_view, result):
self.state_waiting = False self.state_waiting = False
@ -88,16 +89,15 @@ e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
self.timeout_id = None self.timeout_id = None
# Set scroll scale if specified, and the state is not dirty # 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.scroll_scale = scroll_scale
self.emit("scroll-scale-changed", self.scroll_scale) self.emit("scroll-scale-changed", self.scroll_scale)
# Handle the current state # Handle the current state
if not self.state_loaded or self.state_load_failed or self.state_waiting: if not self.state_loaded or self.state_load_failed or self.state_waiting:
return return
if self.pending_scroll_scale: if self.state_dirty:
self.write_scroll_scale(self.pending_scroll_scale) self.write_scroll_scale()
self.pending_scroll_scale = None
if delay > 0: if delay > 0:
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0) self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
else: else:

View File

@ -15,7 +15,6 @@
# END LICENSE # END LICENSE
import codecs import codecs
import locale
import logging import logging
import os import os
import urllib import urllib
@ -24,7 +23,7 @@ from gettext import gettext as _
import gi import gi
from uberwriter.export_dialog import Export 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.stats_handler import StatsHandler
from uberwriter.text_view import TextView from uberwriter.text_view import TextView
@ -112,13 +111,12 @@ class Window(Gtk.ApplicationWindow):
# Setup stats counter # Setup stats counter
self.stats_revealer = builder.get_object('editor_stats_revealer') self.stats_revealer = builder.get_object('editor_stats_revealer')
self.stats_button = builder.get_object('editor_stats_button') 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) self.stats_handler = StatsHandler(self.stats_button, self.text_view)
# Setup preview # Setup preview
content = builder.get_object('content') content = builder.get_object('content')
editor = builder.get_object('editor') 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 # Setup header/stats bar hide after 3 seconds
self.top_bottom_bars_visible = True self.top_bottom_bars_visible = True
@ -419,6 +417,9 @@ class Window(Gtk.ApplicationWindow):
def update_default_stat(self): def update_default_stat(self):
self.stats_handler.update_default_stat() self.stats_handler.update_default_stat()
def update_preview_mode(self):
self.preview_handler.update_preview_mode()
def menu_toggle_sidebar(self, _widget=None): def menu_toggle_sidebar(self, _widget=None):
"""WIP """WIP
""" """
@ -455,14 +456,14 @@ class Window(Gtk.ApplicationWindow):
""" """
if state.get_boolean(): if state.get_boolean():
self.previewer.show() self.preview_handler.show()
else: else:
self.previewer.hide() self.preview_handler.hide()
return True return True
def reload_preview(self): def reload_preview(self):
self.previewer.reload() self.preview_handler.reload()
def load_file(self, filename=None): def load_file(self, filename=None):
"""Open File from command line or open / open recent etc.""" """Open File from command line or open / open recent etc."""