Improve full-width preview workflow

github/fork/yochananmarqos/patch-1
Gonçalo Silva 2019-04-26 03:12:54 +01:00
parent dc0652e3ed
commit 86cffc40ec
12 changed files with 241 additions and 179 deletions

View File

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

View File

@ -35,7 +35,7 @@
<property name="image_position">right</property>
<property name="always_show_image">True</property>
<style>
<class name="toggle-button"/>
<class name="inline-button"/>
</style>
</object>
</child>

View File

@ -116,7 +116,7 @@
<property name="image_position">right</property>
<property name="always_show_image">True</property>
<style>
<class name="toggle-button"/>
<class name="inline-button"/>
</style>
</object>
</child>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,3 @@
from gi.repository import Gtk
class TextViewScroller:
def __init__(self, text_view, scrolled_window):
super().__init__()

View File

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