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' />
</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">
<key name='dark-mode-auto' type='b'>
@ -76,11 +83,11 @@
Maximum number of characters per line within the editor.
</description>
</key>
<key name='preview-side-by-side' type='b'>
<default>false</default>
<summary>Side-by-side preview</summary>
<key name='preview-mode' enum='de.wolfvollprecht.UberWriter.PreviewMode'>
<default>"full-width"</default>
<summary>Preview mode</summary>
<description>
Show the preview side by side, instead of full-width.
How to display the preview.
</description>
</key>

View File

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

View File

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

View File

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

View File

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

View File

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

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:
"""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))

View File

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

View File

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