Merge branch 'master'

ft.librem5
Manuel Genovés 2019-05-12 21:24:30 +02:00
commit 7e34e9cc62
34 changed files with 1175 additions and 464 deletions

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ flatpak/*
data/ui/shortcut_handlers
*.ui~
.vscode/
.idea/
*.glade~
dist/uberwriter-2.0b0-py3.7.egg
builddir/*

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'>
@ -41,6 +48,13 @@
It can cause performance problems to some users.
</description>
</key>
<key name='sync-scroll' type='b'>
<default>true</default>
<summary>Synchronize editor/preview scrolling</summary>
<description>
Keep the editor and preview scroll positions in sync.
</description>
</key>
<key name='input-format' type='s'>
<default>"markdown"</default>
<summary>Input format</summary>
@ -69,6 +83,20 @@
Which statistic is shown on the main window.
</description>
</key>
<key name='characters-per-line' type='i'>
<default>66</default>
<summary>Characters per line</summary>
<description>
Maximum number of characters per line within the editor.
</description>
</key>
<key name='preview-mode' enum='de.wolfvollprecht.UberWriter.PreviewMode'>
<default>"full-width"</default>
<summary>Preview mode</summary>
<description>
How to display the preview.
</description>
</key>
</schema>

View File

@ -22,25 +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.small .uberwriter-editor {
font-size: 14px;
}
.uberwriter-window.large .uberwriter-editor {
font-size: 18px;
}
#titlebar-revealer {
padding: 0;
}
@ -55,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 {
@ -84,9 +92,8 @@
padding: 0;
}
.stats-counter {
.inline-button {
color: alpha(@theme_fg_color, 0.6);
background-color: @theme_base_color;
text-shadow: inherit;
box-shadow: initial;
background-clip: initial;
@ -104,8 +111,8 @@
transition: 100ms ease-in;
}
.stats-counter:hover,
.stats-counter:checked {
.inline-button:hover,
.inline-button:checked {
color: @theme_fg_color;
background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
}

View File

@ -1,4 +1,4 @@
@import url("web/web__base.css");
@import url("base.css");
:root {
--text-color: #eeeeec;

View File

@ -1,4 +1,4 @@
@import url("web/web__base.css");
@import url("base.css");
:root {
--text-color: #3b3e45;

View File

@ -1,4 +1,4 @@
@import url("web/web__base.css");
@import url("base.css");
:root {
--text-color: #d3dae3;

View File

@ -1 +1 @@
@import url("web/web_arc.css");
@import url("arc.css");

View File

@ -45,7 +45,7 @@ html {
}
}
@media screen and (min-width: 1000px) {
@media screen and (min-width: 1280px) {
html {
font-size: 18px;
}
@ -57,9 +57,9 @@ 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;
padding: 4em;
}
a {

View File

@ -1,4 +1,4 @@
@import url("web/web__base.css");
@import url("base.css");
a {
text-decoration: underline;

View File

@ -1,4 +1,4 @@
@import url("web/web__base.css");
@import url("base.css");
a {
text-decoration: underline;

View File

@ -138,11 +138,11 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="format_label">
<object class="GtkLabel" id="sync_scroll_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Input format</property>
<property name="label" translatable="yes">Synchronize editor/preview scrolling</property>
<property name="justify">right</property>
</object>
<packing>
@ -151,17 +151,29 @@
</packing>
</child>
<child>
<object class="GtkButton" id="input_format_help_button">
<object class="GtkSwitch" id="sync_scroll_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">help</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="left_attach">2</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="input_format_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Input format</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="input_format_combobox">
<property name="visible">True</property>
@ -172,9 +184,24 @@
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">4</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkButton" id="input_format_help_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="image">help</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>

51
data/ui/Preview.ui 100644
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="pan-down">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">pan-down-symbolic</property>
<property name="icon_size">2</property>
</object>
<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/>
</child>
<child>
<object class="GtkRevealer" id="preview_mode_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="transition_duration">750</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkButton" id="preview_mode_button">
<property name="label" translatable="yes">Full-Width</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Switch Preview Mode</property>
<property name="halign">end</property>
<property name="image">pan-down</property>
<property name="image_position">right</property>
<property name="always_show_image">True</property>
<style>
<class name="inline-button"/>
</style>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@ -3,37 +3,32 @@
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-local-resource-path ../media -->
<object class="GtkAdjustment" id="adjustment1">
<property name="upper">100</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
<object class="GtkImage" id="edit-find-replace">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</property>
</object>
<object class="GtkImage" id="amunt">
<object class="GtkImage" id="go-up">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-up-symbolic</property>
</object>
<object class="GtkImage" id="avall">
<object class="GtkImage" id="go_down">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-down-symbolic</property>
</object>
<object class="GtkImage" id="ortografia1">
<object class="GtkImage" id="pan-down">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">pan-down-symbolic</property>
<property name="icon_size">2</property>
</object>
<object class="GtkImage" id="spell-check">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-spell-check</property>
</object>
<object class="GtkRecentFilter" id="recentfilter1">
<mime-types>
<mime-type>text/plain</mime-type>
<mime-type>text/x-markdown</mime-type>
</mime-types>
</object>
<object class="GtkImage" id="reemplaza">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</property>
</object>
<object class="GtkOverlay" id="FullscreenOverlay">
<property name="name">FullscreenOverlay</property>
<property name="visible">True</property>
@ -43,10 +38,10 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkPaned" id="main_pained">
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="app_paintable">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="sidebar_box">
<property name="width_request">200</property>
@ -70,65 +65,75 @@
</packing>
</child>
<child>
<object class="GtkGrid" id="grid2">
<object class="GtkBox" id="content">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkRevealer" id="stats_counter_revealer">
<object class="GtkBox" id="editor">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="transition_duration">750</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkButton" id="stats_counter">
<property name="label" translatable="yes">0 Words</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Show Statistics</property>
<property name="halign">end</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkViewport" id="editor_viewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscroll_policy">natural</property>
<property name="vscroll_policy">natural</property>
<property name="shadow_type">none</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="vadjustment">adjustment1</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRevealer" id="editor_stats_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="transition_duration">750</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkButton" id="editor_stats_button">
<property name="label" translatable="yes">0 Words</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Show Statistics</property>
<property name="halign">end</property>
<property name="image">pan-down</property>
<property name="image_position">right</property>
<property name="always_show_image">True</property>
<style>
<class name="inline-button"/>
</style>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="resize">True</property>
<property name="shrink">False</property>
</packing>
</child>
@ -191,7 +196,7 @@
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Previous Match</property>
<property name="image">amunt</property>
<property name="image">go-up</property>
</object>
<packing>
<property name="expand">False</property>
@ -205,7 +210,7 @@
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Next Match</property>
<property name="image">avall</property>
<property name="image">go_down</property>
</object>
<packing>
<property name="expand">False</property>
@ -264,7 +269,7 @@
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Open Replace</property>
<property name="image">reemplaza</property>
<property name="image">edit-find-replace</property>
</object>
<packing>
<property name="expand">False</property>
@ -345,7 +350,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">ortografia1</property>
<property name="image">spell-check</property>
</object>
<packing>
<property name="expand">False</property>

View File

@ -1,5 +1,2 @@
regex
enchant
pypandoc==1.4
pyenchant
pygtkspellcheck
pypandoc==1.4

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
@ -120,10 +122,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
@ -147,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])
@ -178,8 +188,12 @@ 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":
self.window.update_preview_mode()
def on_new(self, _action, _value):
self.window.new_document()
@ -250,6 +264,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

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

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

View File

@ -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"!\[(.*?)\]\((.+?)\)")
@ -455,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)

View File

@ -19,19 +19,18 @@ 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.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')
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
@ -54,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, ()),
@ -68,17 +67,17 @@ 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
self.builder = get_builder('Window')
root = self.builder.get_object("FullscreenOverlay")
root.connect('style-updated', self.apply_current_theme)
builder = get_builder('Window')
root = builder.get_object("FullscreenOverlay")
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()
@ -86,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)
@ -99,28 +98,26 @@ class Window(Gtk.ApplicationWindow):
self.accel_group = Gtk.AccelGroup()
self.add_accel_group(self.accel_group)
self.scrolled_window = builder.get_object('editor_scrolledwindow')
# 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()
# Setup preview webview
self.preview_webview = 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')
# Setup stats counter
self.stats_revealer = builder.get_object('editor_stats_revealer')
self.stats_button = builder.get_object('editor_stats_button')
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
# Setup preview
content = builder.get_object('content')
editor = builder.get_object('editor')
self.preview_handler = PreviewHandler(self, content, editor, self.text_view)
# Setup header/stats bar hide after 3 seconds
self.top_bottom_bars_visible = True
self.was_motion = True
@ -142,8 +139,8 @@ class Window(Gtk.ApplicationWindow):
###
# Sidebar initialization test
###
self.paned_window = self.builder.get_object("main_pained")
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()
@ -151,40 +148,7 @@ class Window(Gtk.ApplicationWindow):
# Search and replace initialization
# Same interface as Sidebar ;)
###
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')
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()
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
def on_text_changed(self, *_args):
"""called when the text changes, sets the self.did_change to true and
@ -209,19 +173,16 @@ class Window(Gtk.ApplicationWindow):
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):
@ -231,47 +192,21 @@ 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
def toggle_preview(self, state):
"""Toggle the preview mode
Arguments:
state {gtk bool} -- Desired state of the preview mode (enabled/disabled)
"""
# 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")
if state.get_boolean():
self.text_view.grab_focus()
self.preview_handler.show()
else:
font_size = 18
self.get_style_context().remove_class("small")
self.get_style_context().add_class("large")
self.preview_handler.hide()
self.text_view.grab_focus()
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
return True
# TODO: refactorizable
def save_document(self, _widget=None, _data=None):
@ -467,6 +402,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
"""
@ -479,9 +417,7 @@ class Window(Gtk.ApplicationWindow):
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
@ -495,58 +431,8 @@ class Window(Gtk.ApplicationWindow):
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.show_preview()
else:
self.show_text_editor()
return True
def show_text_editor(self):
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):
if loaded:
self.scrolled_window.remove(self.scrolled_window.get_child())
self.scrolled_window.add(self.preview_webview)
self.preview_webview.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.preview_webview is None:
self.preview_webview = WebKit.WebView()
self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True)
# Show preview once the load is finished
self.preview_webview.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.preview_webview.load_html(output, 'file://localhost/')
def reload_preview(self):
if self.preview_webview:
self.show_preview()
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."""
@ -617,15 +503,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_counter_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
@ -643,24 +524,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_counter_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_counter_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
@ -726,18 +605,4 @@ class Window(Gtk.ApplicationWindow):
else:
self.filename = None
base_path = "/"
self.settings.set_value("open-file-path", GLib.Variant("s", base_path))
def on_preview_load_change(self, webview, event):
"""swaps text editor with preview once the load is complete
"""
if event == WebKit.LoadEvent.FINISHED:
self.show_preview(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"
self.settings.set_string("open-file-path", base_path)

View File

@ -82,8 +82,12 @@ 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_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"]])
@ -107,28 +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_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()]

View File

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

View File

@ -0,0 +1,166 @@
import math
import webbrowser
from enum import auto, IntEnum
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
from uberwriter.preview_converter import PreviewConverter
from uberwriter.web_view import WebView
class Step(IntEnum):
CONVERT_HTML = auto()
LOAD_WEBVIEW = auto()
RENDER = auto()
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
self.web_view = None
self.web_view_pending_html = None
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, self.mode_revealer, mode_button)
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.loading = False
self.shown = False
def show(self):
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 step: show the preview. This is a one-time step.
if self.shown:
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)
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, *_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)
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
self.web_view.destroy()
self.web_view = None
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
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 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 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
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

View File

@ -0,0 +1,174 @@
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:
"""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_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
self.preview = preview
self.mode_revealer = mode_revealer
self.mode_button = mode_button
self.mode_button.connect("clicked", self.show_mode_popover)
self.settings = Settings.new()
self.popover = None
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."""
# Windowed preview: create a window and show the preview in it.
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:
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."""
# Windowed preview: remove preview and destroy window.
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:
self.preview.remove(web_view)
self.content.remove(self.preview)
# 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 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()
def show_mode_popover(self, button):
"""Show preview mode popover."""
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(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

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

View File

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

View File

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

View File

@ -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
@ -15,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__()
@ -34,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()
@ -65,16 +65,16 @@ class StatsHandler:
self.update_stats)
def get_text_for_stat(self, stat):
if stat == 0:
return _("{:n} Characters".format(self.characters))
elif stat == 1:
return _("{:n} Words".format(self.words))
elif stat == 2:
return _("{:n} Sentences".format(self.sentences))
elif stat == 3:
return _("{:n} Paragraphs".format(self.paragraphs))
elif stat == 4:
return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time))
if stat == self.CHARACTERS:
return _("{:n} Characters").format(self.characters)
elif stat == self.WORDS:
return _("{:n} Words").format(self.words)
elif stat == self.SENTENCES:
return _("{:n} Sentences").format(self.sentences)
elif stat == self.PARAGRAPHS:
return _("{:n} Paragraphs").format(self.paragraphs)
elif stat == self.READ_TIME:
return _("{:d}:{:02d}:{:02d} Read Time").format(*self.read_time)
else:
raise ValueError("Unknown stat {}".format(stat))

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,17 +1,19 @@
import gi
from gi.repository.GObject import SignalMatchType
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')
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
import logging
LOGGER = logging.getLogger('uberwriter')
@ -36,10 +38,13 @@ 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,)),
}
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,11 +54,18 @@ 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.line_chars = line_chars
self.font_size = 16
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.spellcheck = True
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
self.gspell_view.basic_setup()
@ -85,6 +97,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
@ -95,6 +108,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()
@ -105,13 +124,59 @@ class TextView(Gtk.TextView):
text_buffer = self.get_buffer()
text_buffer.set_text(text)
def on_text_changed(self, *_):
self.markup.apply()
GLib.idle_add(self.scroll_to)
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_size_allocate(self, *_):
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()
self.smooth_scroll_to()
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)
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
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 on_scroll_scale_changed(self, *_):
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.
@ -120,10 +185,33 @@ 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.scroll_to()
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
# Ensure the appropriate font size is being used
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.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
def update_vertical_margin(self):
if self.focus_mode:
@ -134,11 +222,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 +241,34 @@ 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:
return
va = scrolled_window.get_vadjustment()
if va.props.page_size < margin * 2:
if self.scroller is None:
return
if mark is None:
mark = self.get_buffer().get_insert()
GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode)
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)
def get_min_width(self, font_size=None):
"""Returns the minimum width of this text view."""
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 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
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 get_pad_chars(self, font_size):
"""Returns the amount of character padding for font_size.
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
Markup can use up to 7 in normal conditions."""
return 8 * (1 + font_size - self.font_sizes[-1])
@staticmethod
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

View File

@ -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}"),
@ -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)

View File

@ -0,0 +1,106 @@
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

View File

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

View File

@ -0,0 +1,115 @@
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) : -1;
"""
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("size-allocate", self.on_size_allocate)
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_discard_result = 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.state_discard_result = event == WebKit2.LoadEvent.STARTED and self.state_waiting
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_size_allocate(self, *_):
self.set_scroll_scale(self.scroll_scale)
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.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 finish_read_scroll_scale(self, _web_view, result):
self.state_waiting = False
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
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, -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
self.read_scroll_scale()
elif delay > 0:
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
else:
self.read_scroll_scale()