From 7a9b878d0210799bbdce1c7ce72b9823435209aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 28 Mar 2019 17:04:01 +0000 Subject: [PATCH 01/54] Copy improvements Smaller improvements around capitalization, tense and style. --- data/de.wolfvollprecht.UberWriter.gschema.xml | 10 ++++------ data/ui/Preferences.ui | 2 +- uberwriter/window.py | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index e7fdb28..82d891c 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -8,16 +8,14 @@ false Dark mode - If enabled, the window will be dark themed - If disabled, the window will be light themed - asked to install them manually. + Enable or disable the dark mode. true Spellcheck - Enable/disable spellchecking in the application. + Enable or disable spellchecking. @@ -25,14 +23,14 @@ Show gradient overlay Show a gradient overlay over the text at the top anf bottom of the window. - It can cause performance problems to some users + It can cause performance problems to some users. true Allow Uberwriter to poll cursor motion - Used for hidding the headerbar after 3 seconds if the cursor is not moving. + Hide the header and status bars if the cursor is not moving. diff --git a/data/ui/Preferences.ui b/data/ui/Preferences.ui index 3880dc1..fb995dc 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -55,7 +55,7 @@ True False end - Autospellcheck + Check spelling while typing right diff --git a/uberwriter/window.py b/uberwriter/window.py index ba784c7..1ef21be 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -129,7 +129,7 @@ class Window(Gtk.ApplicationWindow): self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1')) self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.text_editor.connect('focus-out-event', self.focus_out) - self.text_editor.get_style_context().connect('changed', self.style_changed) + self.text_editor.get_style_context().connect('changed', self.on_style_changed) self.text_editor.set_top_margin(80) self.text_editor.set_bottom_margin(16) @@ -547,7 +547,7 @@ class Window(Gtk.ApplicationWindow): alloc.width = width_request self.text_editor.size_allocate(alloc) - def style_changed(self, _widget, _data=None): + def on_style_changed(self, _widget, _data=None): pgc = self.text_editor.get_pango_context() mets = pgc.get_metrics() self.markup_buffer.set_multiplier( @@ -574,7 +574,7 @@ class Window(Gtk.ApplicationWindow): filefilter = Gtk.FileFilter.new() filefilter.add_mime_type('text/x-markdown') filefilter.add_mime_type('text/plain') - filefilter.set_name('MarkDown (.md)') + filefilter.set_name('Markdown (.md)') filechooser = Gtk.FileChooserDialog( _("Save your File"), self, From 30df10cab6d33cbbdcfffc0e43bb43c961adb150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 29 Mar 2019 00:46:53 +0000 Subject: [PATCH 02/54] Allow following system theme *and* forcing light / dark theme --- data/de.wolfvollprecht.UberWriter.gschema.xml | 9 ++- data/ui/Preferences.ui | 39 +++++++++--- uberwriter/application.py | 61 ++++++++++++------- uberwriter/theme.py | 5 +- uberwriter/window.py | 8 ++- 5 files changed, 85 insertions(+), 37 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 82d891c..4d3f7ca 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -4,9 +4,16 @@ + + true + Set dark mode automatically + + Whether dark mode depends on the system theme, or is set to what the user specifies. + + false - Dark mode + Force dark mode Enable or disable the dark mode. diff --git a/data/ui/Preferences.ui b/data/ui/Preferences.ui index fb995dc..5b34d2b 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -37,12 +37,11 @@ 10 10 - + True False end - start - Use dark mode + Set dark mode automatically right @@ -50,6 +49,19 @@ 0 + + + True + False + end + Force dark mode + right + + + 0 + 1 + + True @@ -60,7 +72,7 @@ 0 - 1 + 2 @@ -73,7 +85,18 @@ 0 - 2 + 3 + + + + + True + True + app.dark_mode_auto + + + 1 + 0 @@ -84,7 +107,7 @@ 1 - 0 + 1 @@ -95,7 +118,7 @@ 1 - 1 + 2 @@ -107,7 +130,7 @@ 1 - 2 + 3 diff --git a/uberwriter/application.py b/uberwriter/application.py index 304fed1..b700ef3 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -36,6 +36,7 @@ class Application(Gtk.Application): **kwargs) self.window = None self.settings = Settings.new() + self.dark_mode_action = None def do_startup(self, *args, **kwargs): @@ -59,33 +60,35 @@ class Application(Gtk.Application): action.connect("activate", self.on_quit) self.add_action(action) - set_dark_mode = self.settings.get_value("dark-mode") + dark_mode_auto = self.settings.get_value("dark-mode-auto") + action = Gio.SimpleAction.new_stateful("dark_mode_auto", None, + GLib.Variant.new_boolean(dark_mode_auto)) + action.connect("change-state", self.on_dark_mode_auto) + self.add_action(action) + + dark_mode = self.settings.get_value("dark-mode") action = Gio.SimpleAction.new_stateful("dark_mode", None, - GLib.Variant.new_boolean(set_dark_mode)) + GLib.Variant.new_boolean(dark_mode)) action.connect("change-state", self.on_dark_mode) self.add_action(action) - action = Gio.SimpleAction.new_stateful("focus_mode", - None, + action = Gio.SimpleAction.new_stateful("focus_mode", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_focus_mode) self.add_action(action) - action = Gio.SimpleAction.new_stateful("hemingway_mode", - None, + action = Gio.SimpleAction.new_stateful("hemingway_mode", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_hemingway_mode) self.add_action(action) - action = Gio.SimpleAction.new_stateful("fullscreen", - None, + action = Gio.SimpleAction.new_stateful("fullscreen", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_fullscreen) self.add_action(action) - action = Gio.SimpleAction.new_stateful("preview", - None, + action = Gio.SimpleAction.new_stateful("preview", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_preview) self.add_action(action) @@ -94,17 +97,15 @@ class Application(Gtk.Application): action.connect("activate", self.on_search) self.add_action(action) - set_spellcheck = self.settings.get_value("spellcheck") - action = Gio.SimpleAction.new_stateful("spellcheck", - None, - GLib.Variant.new_boolean(set_spellcheck)) + spellcheck = self.settings.get_value("spellcheck") + action = Gio.SimpleAction.new_stateful("spellcheck", None, + GLib.Variant.new_boolean(spellcheck)) action.connect("change-state", self.on_spellcheck) self.add_action(action) - set_gradient_overlay = self.settings.get_value("gradient-overlay") - action = Gio.SimpleAction.new_stateful("draw_gradient", - None, - GLib.Variant.new_boolean(set_gradient_overlay)) + gradient_overlay = self.settings.get_value("gradient-overlay") + action = Gio.SimpleAction.new_stateful("draw_gradient", None, + GLib.Variant.new_boolean(gradient_overlay)) action.connect("change-state", self.on_draw_gradient) self.add_action(action) @@ -163,6 +164,8 @@ class Application(Gtk.Application): self.set_accels_for_action("app.save_as", ["s"]) self.set_accels_for_action("app.quit", ["w", "q"]) + # Theme + self.apply_current_theme() def do_activate(self, *args, **kwargs): @@ -210,8 +213,8 @@ class Application(Gtk.Application): style_provider.load_from_path(theme.gtk_css_path) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ) + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + def on_about(self, _action, _param): builder = get_builder('About') @@ -236,11 +239,25 @@ class Application(Gtk.Application): builder.get_object("shortcuts").set_transient_for(self.window) builder.get_object("shortcuts").show() - def on_dark_mode(self, action, value): + def on_dark_mode_auto(self, action, value, update_dark_mode_auto=True): + action.set_state(value) + self.settings.set_value("dark-mode-auto", GLib.Variant("b", value)) + + if update_dark_mode_auto: + self.on_dark_mode(self.lookup_action("dark_mode"), + GLib.Variant.new_boolean(not value.get_boolean()), + False) + + def on_dark_mode(self, action, value, update_dark_mode_auto=True): action.set_state(value) self.settings.set_value("dark-mode", GLib.Variant("b", value)) - # this changes the headerbar theme accordingly + if update_dark_mode_auto: + self.on_dark_mode_auto(self.lookup_action("dark_mode_auto"), + GLib.Variant.new_boolean(not value.get_boolean()), + False) + + # change the app theme accordingly self.apply_current_theme() # adjust window for theme diff --git a/uberwriter/theme.py b/uberwriter/theme.py index e43ec57..d91d58f 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -31,11 +31,10 @@ class Theme: @classmethod def get_current(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() current_theme = cls.get_for_name(theme_name) - # Technically, we could very easily allow the user to force the light ui on a dark theme. - # However, as there is no inverse of "gtk-application-prefer-dark-theme", we shouldn't do that. - if dark_mode and not current_theme.is_dark and current_theme.inverse_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) return current_theme diff --git a/uberwriter/window.py b/uberwriter/window.py index 1ef21be..5927fae 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -264,13 +264,15 @@ class Window(Gtk.ApplicationWindow): """Adjusts both the window and the CSD for the current theme. """ + # Update markup buffer's style self.markup_buffer.update_style() - # Reload preview if it exists, otherwise redraw contents of window (self) + # Reload preview if it exists if self.preview_webview: self.show_preview() - else: - self.queue_draw() + + # Redraw contents of window + self.queue_draw() def scrolled(self, widget): """if window scrolled + focusmode make font black again""" From 99641125ba5a2a2c56fdac7a29241ddfe829140f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 29 Mar 2019 00:50:07 +0000 Subject: [PATCH 03/54] Uniformize main class <> file naming --- uberwriter/__init__.py | 23 ++++++++--------------- uberwriter/export_dialog.py | 2 +- uberwriter/headerbars.py | 5 +++-- uberwriter/text_editor.py | 4 ++-- uberwriter/window.py | 18 ++++++++---------- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/uberwriter/__init__.py b/uberwriter/__init__.py index f062c1e..ed4348d 100644 --- a/uberwriter/__init__.py +++ b/uberwriter/__init__.py @@ -15,15 +15,9 @@ ### END LICENSE import sys -import locale -import os - -import gettext -from gettext import gettext as _ - import gi + gi.require_version('Gtk', '3.0') -from gi.repository import Gtk # pylint: disable=E0611 from uberwriter import window from uberwriter import application @@ -34,17 +28,16 @@ from uberwriter.uberwriterconfig import get_version def main(): 'constructor for your class instances' # (options, args) = parse_options() - + # Run the application. app = application.Application() - + # ~ if args: - # ~ for arg in args: - # ~ pass + # ~ for arg in args: + # ~ pass # ~ else: - # ~ pass + # ~ pass # ~ if options.experimental_features: - # ~ window.use_experimental_features(True) - + # ~ window.use_experimental_features(True) + app.run(sys.argv) - diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index 7a2362f..d344d0b 100644 --- a/uberwriter/export_dialog.py +++ b/uberwriter/export_dialog.py @@ -35,7 +35,7 @@ from uberwriter.helpers import get_builder LOGGER = logging.getLogger('uberwriter') -class Export: +class UberwriterExportDialog: """ Manages all the export operations and dialogs """ diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index e70b04e..999a4cd 100644 --- a/uberwriter/headerbars.py +++ b/uberwriter/headerbars.py @@ -20,12 +20,12 @@ from collections import namedtuple from gettext import gettext as _ import gi + gi.require_version('Gtk', '3.0') from gi.repository import Gtk from uberwriter.helpers import get_builder from uberwriter.helpers import get_descendant -from uberwriter.application import Application as app class MainHeaderbar: #pylint: disable=too-few-public-methods """Sets up the main application headerbar @@ -54,7 +54,7 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods self.hb.show_all() -class FsHeaderbar: +class FullscreenHeaderbar: """Sets up and manages the fullscreen headerbar and his events """ @@ -153,6 +153,7 @@ def buttons(app): return btn + def pack_buttons(headerbar, btn, btn_exit=None): """Pack the given buttons in the given headerbar diff --git a/uberwriter/text_editor.py b/uberwriter/text_editor.py index a94a1b2..60919e1 100644 --- a/uberwriter/text_editor.py +++ b/uberwriter/text_editor.py @@ -81,7 +81,7 @@ class UndoableDelete: or self.text in ("\r", "\n", " ")) -class TextEditor(Gtk.TextView): +class UberwriterTextEditor(Gtk.TextView): """TextEditor encapsulates management of TextBuffer and TextIter for common functionality, such as cut, copy, paste, undo, redo, and highlighting of text. @@ -442,7 +442,7 @@ class TestWindow(Gtk.Window): windowbox = Gtk.VBox(homogeneous=False, spacing=2) windowbox.show() self.add(windowbox) - self.editor = TextEditor() + self.editor = UberwriterTextEditor() self.editor.show() windowbox.pack_end(self.editor, True, True, 0) self.set_size_request(200, 200) diff --git a/uberwriter/window.py b/uberwriter/window.py index 5927fae..2c45812 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -26,8 +26,9 @@ import webbrowser from gettext import gettext as _ import gi -from gi.repository.GObject import param_spec_string +from uberwriter.export_dialog import UberwriterExportDialog +from uberwriter.text_editor import UberwriterTextEditor gi.require_version('Gtk', '3.0') gi.require_version('WebKit2', '4.0') # pylint: disable=wrong-import-position @@ -36,24 +37,21 @@ from gi.repository import WebKit2 as WebKit from gi.repository import Pango # pylint: disable=E0611 import cairo -# import cairo.Pattern, cairo.SolidPattern -from uberwriter import headerbars from uberwriter import helpers from uberwriter.theme import Theme from uberwriter.helpers import get_builder from uberwriter.gtkspellcheck import SpellChecker from uberwriter.markup_buffer import MarkupBuffer -from uberwriter.text_editor import TextEditor from uberwriter.inline_preview import InlinePreview from uberwriter.sidebar import Sidebar from uberwriter.search_and_replace import SearchAndReplace from uberwriter.settings import Settings -# from .auto_correct import AutoCorrect -from uberwriter.export_dialog import Export -# from .plugins.bibtex import BibTex +from . import headerbars + + # Some Globals # TODO move them somewhere for better # accesibility from other files @@ -89,7 +87,7 @@ class Window(Gtk.ApplicationWindow): # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) - self.fs_headerbar = headerbars.FsHeaderbar(self.builder, app) + self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app) self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) @@ -116,7 +114,7 @@ class Window(Gtk.ApplicationWindow): self.add_accel_group(self.accel_group) # Setup text editor - self.text_editor = TextEditor() + self.text_editor = UberwriterTextEditor() self.text_editor.set_name('UberwriterEditor') self.get_style_context().add_class('uberwriter_window') @@ -964,7 +962,7 @@ class Window(Gtk.ApplicationWindow): """open the export and advanced export dialog """ - self.export = Export(self.filename) + self.export = UberwriterExportDialog(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() From 16a8ac78db83359152439046276ad4d963579cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 29 Mar 2019 01:30:43 +0000 Subject: [PATCH 04/54] Fix changing theme while app is running not applying until restart Overall refactoring of how the theme is set, bounding it to the window instead of the application, which generally makes it easier to listen for the "style-updated" signal. --- uberwriter/application.py | 25 -------------- uberwriter/markup_buffer.py | 65 ++++++++++++++++++----------------- uberwriter/theme.py | 13 ++++++- uberwriter/window.py | 67 ++++++++++++++++++++----------------- 4 files changed, 84 insertions(+), 86 deletions(-) diff --git a/uberwriter/application.py b/uberwriter/application.py index b700ef3..7b199e9 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -164,10 +164,6 @@ class Application(Gtk.Application): self.set_accels_for_action("app.save_as", ["s"]) self.set_accels_for_action("app.quit", ["w", "q"]) - # Theme - - self.apply_current_theme() - def do_activate(self, *args, **kwargs): # We only allow a single window and raise any existing ones if not self.window: @@ -199,23 +195,6 @@ class Application(Gtk.Application): self.activate() return 0 - def apply_current_theme(self): - # get current theme - theme = Theme.get_current() - - # 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(theme.gtk_css_path) - Gtk.StyleContext.add_provider_for_screen( - Gdk.Screen.get_default(), style_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - def on_about(self, _action, _param): builder = get_builder('About') about_dialog = builder.get_object("AboutDialog") @@ -257,10 +236,6 @@ class Application(Gtk.Application): GLib.Variant.new_boolean(not value.get_boolean()), False) - # change the app theme accordingly - self.apply_current_theme() - - # adjust window for theme self.window.apply_current_theme() def on_focus_mode(self, action, value): diff --git a/uberwriter/markup_buffer.py b/uberwriter/markup_buffer.py index c413482..7afc8ae 100644 --- a/uberwriter/markup_buffer.py +++ b/uberwriter/markup_buffer.py @@ -23,11 +23,29 @@ from gi.repository import Pango class MarkupBuffer(): + regex = { + "ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_ + "STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__ + "STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"), + "BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE), + "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"), + "LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE), + "NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE), + "INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE), + "HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE), + "HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE), + "HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE), + "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"), + "HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"), + "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL), + "LINK": re.compile(r"\(http(.+?)\)") + } - def __init__(self, Parent, TextBuffer, base_leftmargin): - self.multiplier = 10 - self.parent = Parent - self.text_buffer = TextBuffer + def __init__(self, window, text_editor, base_leftmargin): + self.margin_multiplier = 10 + self.parent = window + self.text_editor = text_editor + self.text_buffer = text_editor.get_buffer() # Styles self.italic = self.text_buffer.create_tag("italic", @@ -94,37 +112,24 @@ class MarkupBuffer(): self.table_env = self.text_buffer.create_tag('table_env') self.table_env.set_property('wrap-mode', Gtk.WrapMode.NONE) - # self.table_env.set_property('font', 'Ubuntu Mono 13px') self.table_env.set_property('pixels-above-lines', 0) self.table_env.set_property('pixels-below-lines', 0) - self.update_style() + # Theme + self.text_editor.connect('style-updated', self.apply_current_theme) + self.apply_current_theme() - regex = { - "ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_ - "STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__ - "STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"), - "BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE), - "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"), - "LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE), - "NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE), - "INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE), - "HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE), - "HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE), - "HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE), - "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"), - "HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"), - "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL), - "LINK": re.compile(r"\(http(.+?)\)") - } - - def update_style(self): - (found, color) = self.parent.get_style_context().lookup_color('math_text_color') + def apply_current_theme(self, *_): + # Math text color + (found, color) = self.text_editor.get_style_context().lookup_color('math_text_color') if not found: - (_, color) = self.parent.get_style_context().lookup_color('foreground_color') - + (_, color) = self.text_editor.get_style_context().lookup_color('foreground_color') self.math_text.set_property("foreground", color.to_string()) + # Margin + mets = self.text_editor.get_pango_context().get_metrics() + self.set_multiplier(Pango.units_to_double(mets.get_approximate_char_width()) + 1) + def markup_buffer(self, mode=0): buf = self.text_buffer @@ -306,10 +311,10 @@ class MarkupBuffer(): end_sentence, self.text_buffer.get_end_iter()) def set_multiplier(self, multiplier): - self.multiplier = multiplier + self.margin_multiplier = multiplier def recalculate(self, lm): - multiplier = self.multiplier + multiplier = self.margin_multiplier for i in range(0, 6): new_margin = (lm - multiplier) - multiplier * (i + 1) self.rev_leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin) diff --git a/uberwriter/theme.py b/uberwriter/theme.py index d91d58f..c1ab1a0 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -11,6 +11,7 @@ class Theme: The light variant is listed first, followed by the dark variant, if any. """ + previous = None settings = Settings.new() def __init__(self, name, gtk_css_path, web_css_path, is_dark, inverse_name): @@ -36,7 +37,17 @@ class Theme: 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) - return current_theme + changed = current_theme != cls.previous + cls.previous = current_theme + return current_theme, changed + + def __eq__(self, other): + return isinstance(other, self.__class__) and \ + self.name == other.name and \ + self.gtk_css_path == other.gtk_css_path and \ + self.web_css_path == other.web_css_path and \ + self.is_dark == other.is_dark and \ + self.inverse_name == other.inverse_name defaultThemes = [ diff --git a/uberwriter/window.py b/uberwriter/window.py index 2c45812..dcdbcd9 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -64,6 +64,15 @@ CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/") class Window(Gtk.ApplicationWindow): + __gsignals__ = { + 'save-file': (GObject.SIGNAL_ACTION, None, ()), + 'open-file': (GObject.SIGNAL_ACTION, None, ()), + 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), + 'new-file': (GObject.SIGNAL_ACTION, None, ()), + 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), + 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), + 'close-window': (GObject.SIGNAL_ACTION, None, ()) + } WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE) @@ -74,6 +83,7 @@ class Window(Gtk.ApplicationWindow): application=Gio.Application.get_default(), title="Uberwriter") + # Set UI self.builder = get_builder('UberwriterWindow') self.add(self.builder.get_object("FullscreenOverlay")) @@ -127,7 +137,7 @@ class Window(Gtk.ApplicationWindow): self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1')) self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) self.text_editor.connect('focus-out-event', self.focus_out) - self.text_editor.get_style_context().connect('changed', self.on_style_changed) + self.text_editor.connect('style-updated', self.apply_current_theme) self.text_editor.set_top_margin(80) self.text_editor.set_bottom_margin(16) @@ -183,12 +193,9 @@ class Window(Gtk.ApplicationWindow): # Markup and Shortcuts for the TextBuffer self.markup_buffer = MarkupBuffer( - self, self.text_buffer, base_leftmargin) + self, self.text_editor, base_leftmargin) self.markup_buffer.markup_buffer() - # Set current theme - self.apply_current_theme() - # Scrolling -> Dark or not? self.textchange = False self.scroll_count = 0 @@ -248,29 +255,34 @@ class Window(Gtk.ApplicationWindow): self.connect("configure-event", self.window_resize) self.connect("delete-event", self.on_delete_called) - __gsignals__ = { - 'save-file': (GObject.SIGNAL_ACTION, None, ()), - 'open-file': (GObject.SIGNAL_ACTION, None, ()), - 'save-file-as': (GObject.SIGNAL_ACTION, None, ()), - 'new-file': (GObject.SIGNAL_ACTION, None, ()), - 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()), - 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()), - 'close-window': (GObject.SIGNAL_ACTION, None, ()) - } + # Set current theme + self.apply_current_theme() - def apply_current_theme(self): - """Adjusts both the window and the CSD for the 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() - # Update markup buffer's style - self.markup_buffer.update_style() + if changed: + # Set theme variant (dark/light) + Gtk.Settings.get_default().set_property( + "gtk-application-prefer-dark-theme", + GLib.Variant("b", theme.is_dark)) - # Reload preview if it exists - if self.preview_webview: - self.show_preview() + # Set theme css + style_provider = Gtk.CssProvider() + style_provider.load_from_path(theme.gtk_css_path) + Gtk.StyleContext.add_provider_for_screen( + self.get_screen(), style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - # Redraw contents of window - self.queue_draw() + # Reload preview if it exists + if self.preview_webview: + self.show_preview() + + # Redraw contents of window + self.queue_draw() def scrolled(self, widget): """if window scrolled + focusmode make font black again""" @@ -547,12 +559,6 @@ class Window(Gtk.ApplicationWindow): alloc.width = width_request self.text_editor.size_allocate(alloc) - def on_style_changed(self, _widget, _data=None): - pgc = self.text_editor.get_pango_context() - mets = pgc.get_metrics() - self.markup_buffer.set_multiplier( - Pango.units_to_double(mets.get_approximate_char_width()) + 1) - # TODO: refactorizable def save_document(self, _widget=None, _data=None): """provide to the user a filechooser and save the document @@ -876,12 +882,13 @@ class Window(Gtk.ApplicationWindow): base_path = '' os.environ['PANDOC_PREFIX'] = base_path + '/' + theme, _ = Theme.get_current() args = ['pandoc', '-s', '--from=markdown', '--to=html5', '--mathjax', - '--css=' + Theme.get_current().web_css_path, + '--css=' + theme.web_css_path, '--quiet', '--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'), '--lua-filter=' + helpers.get_script_path('task-list.lua')] From 6688eb259ec4b847cd72e20994a8df171ab858d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 31 Mar 2019 03:16:18 +0100 Subject: [PATCH 05/54] Refactor textview / textbuffer into separate modules Changes include: * Much better encapsulation of textview/textbuffer, with each isolated responsibility living independently on its own class/file. * Less code overall * Various small fixes around the components involved, such as: * Indentation of nested lists (fixes #120) * Unwanted scroll on select all (ctrl+a) * Removal of unused code around the components involved * Fixes for scrollbar location, now at the edge of the window --- data/media/css/_gtk_base.css | 62 ++- data/ui/UberwriterWindow.ui | 19 +- help/stump/help.md | 4 +- help/stump/help_backup.html | 4 +- help/stump/help_backup.md | 4 +- uberwriter/__init__.py | 8 - uberwriter/application.py | 2 - uberwriter/export_dialog.py | 3 +- uberwriter/format_shortcuts.py | 4 - uberwriter/headerbars.py | 4 +- uberwriter/helpers.py | 8 + uberwriter/inline_preview.py | 41 +- uberwriter/markup_buffer.py | 326 --------------- uberwriter/search_and_replace.py | 26 +- uberwriter/text_editor.py | 487 ---------------------- uberwriter/text_view.py | 175 ++++++++ uberwriter/text_view_drag_drop_handler.py | 62 +++ uberwriter/text_view_format_inserter.py | 154 +++++++ uberwriter/text_view_markup_handler.py | 246 +++++++++++ uberwriter/text_view_scroller.py | 84 ++++ uberwriter/text_view_undo_redo_handler.py | 204 +++++++++ uberwriter/window.py | 470 +++------------------ 22 files changed, 1074 insertions(+), 1323 deletions(-) delete mode 100644 uberwriter/markup_buffer.py delete mode 100644 uberwriter/text_editor.py create mode 100644 uberwriter/text_view.py create mode 100644 uberwriter/text_view_drag_drop_handler.py create mode 100644 uberwriter/text_view_format_inserter.py create mode 100644 uberwriter/text_view_markup_handler.py create mode 100644 uberwriter/text_view_scroller.py create mode 100644 uberwriter/text_view_undo_redo_handler.py diff --git a/data/media/css/_gtk_base.css b/data/media/css/_gtk_base.css index fdbf0cf..84261ab 100644 --- a/data/media/css/_gtk_base.css +++ b/data/media/css/_gtk_base.css @@ -8,42 +8,39 @@ bind "i" { "insert-italic" () }; bind "b" { "insert-bold" () }; bind "r" { "insert-hrule" () }; - bind "u" { "insert-ulistitem" () }; - bind "h" { "insert-heading" () }; + bind "u" { "insert-listitem" () }; + bind "h" { "insert-header" () }; bind "z" { "undo" () }; bind "y" { "redo" () }; - bind "d" { "insert-strikeout" () }; + bind "d" { "insert-strikethrough" () }; /*bind "t" { "insert-at-cursor" ('[ ] ') };*/ bind "z" { "redo" () }; } /* Main window and text colors */ -.uberwriter_window { +.uberwriter-window { /*border-radius: 7px 7px 3px 3px;*/ background: @background_color; caret-color: @foreground_color; } -.uberwriter_window.small .uberwriter-editor { +.uberwriter-window .uberwriter-editor { font-family: 'Fira Mono', monospace; - font-size: 12px; -} -.uberwriter_window grid { - background-color: @background_color; + font-size: 16px; + padding-top: 80px; + padding-bottom: 16px; } -#UberwriterWindow.medium .uberwriter-editor { - font-family: 'Fira Mono', monospace; - font-size: 15px; +.uberwriter-window.small .uberwriter-editor { + font-size: 14px; } -#UberwriterWindow.large .uberwriter-editor { - font-family: 'Fira Mono', monospace; +.uberwriter-window.large .uberwriter-editor { font-size: 18px; } -#titlebar_revealer { +#titlebar-revealer { padding: 0; } @@ -52,8 +49,8 @@ background: transparent; } -#titlebar_container { - background: @background_color; +#titlebar-container { + background: @background_color; } .uberwriter-editor { @@ -89,11 +86,11 @@ } -.status_bar_box label { +.status-bar-box label { color: #666; } -.status_bar_box button { +.status-bar-box button { /* finding reset */ background-color: @background_color; text-shadow: inherit; @@ -118,26 +115,26 @@ transition: 100ms ease-in; } -.status_bar_box button:hover, -.status_bar_box button:checked { +.status-bar-box button:hover, +.status-bar-box button:checked { transition: 0s ease-in; color: @background_color; background-color: #666; } -.status_bar_box button:hover label, -.status_bar_box button:checked label { +.status-bar-box button:hover label, +.status-bar-box button:checked label { color: @background_color; } -.status_bar_box button:active { +.status-bar-box button:active { color: #EEE; background-color: #EEE; background-image: none; box-shadow: 0 0 2px rgba(0,0,0,0.4) } -.status_bar_box separator { +.status-bar-box separator { border-color: #999; border-right: none; } @@ -150,7 +147,7 @@ background: #FFF; } -#UberwriterWindow treeview { +.uberwriter-window treeview { padding: 3px 3px 3px 3px; } @@ -165,7 +162,7 @@ padding: 5px; } -/* .QuickPreviewPopup { +/* .quick-preview-popup { padding: 5px; margin: 5px; border: 1px solid #333; @@ -183,8 +180,7 @@ border: 5px solid @background_color; } -#LexikonBubble .lexikon_heading { - /*font: serif 12;*/ +#LexikonBubble .lexikon-heading { font-family: serif; font-size: 12px; padding-bottom: 5px; @@ -193,21 +189,21 @@ padding-left: 10px; } -#LexikonBubble .lexikon_num { +#LexikonBubble .lexikon-num { padding-right: 5px; padding-left: 20px; } -.QuickPreviewPopup { +.quick-preview-popup { background-color: @background_color; } -.QuickPreviewPopup grid { +.quick-preview-popup grid { background-color: @background_color; color: @foreground_color; border-color: @background_color; } -.QuickPreviewPopup label { +.quick-preview-popup label { color: @foreground_color; } \ No newline at end of file diff --git a/data/ui/UberwriterWindow.ui b/data/ui/UberwriterWindow.ui index 162d6f9..b30c922 100644 --- a/data/ui/UberwriterWindow.ui +++ b/data/ui/UberwriterWindow.ui @@ -186,23 +186,16 @@ natural none - + + 500 True - False - center + True + True True True + adjustment1 - - 500 - True - True - True - adjustment1 - - - - + diff --git a/help/stump/help.md b/help/stump/help.md index c63bf5b..9b0dc25 100644 --- a/help/stump/help.md +++ b/help/stump/help.md @@ -619,9 +619,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte feas*ible*, not feas*able*. -#### Strikeout +#### Strikethrough -To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example, +To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example, This ~~is deleted text.~~ diff --git a/help/stump/help_backup.html b/help/stump/help_backup.html index 6c05d16..dc82ab5 100644 --- a/help/stump/help_backup.html +++ b/help/stump/help_backup.html @@ -47,8 +47,8 @@ is *emphasized with asterisks*.
This is * not emphasized *, and \*neither is this\*.

Because _ is sometimes used inside words and identifiers, pandoc does not interpret a _ surrounded by alphanumeric characters as an emphasis marker. If you want to emphasize just part of a word, use *:

feas*ible*, not feas*able*.
-

Strikeout

-

To strikeout a section of text with a horizontal line, begin and end it with ~~. Thus, for example,

+

Strikethrough

+

To strikethrough a section of text with a horizontal line, begin and end it with ~~. Thus, for example,

This ~~is deleted text.~~

Block quotations

Markdown uses email conventions for quoting blocks of text. A block quotation is one or more paragraphs or other block elements (such as lists or headers), with each line preceded by a > character and a space.

diff --git a/help/stump/help_backup.md b/help/stump/help_backup.md index 6395e5c..17518d1 100644 --- a/help/stump/help_backup.md +++ b/help/stump/help_backup.md @@ -67,9 +67,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte feas*ible*, not feas*able*. -### Strikeout +### Strikethrough -To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example, +To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example, This ~~is deleted text.~~ diff --git a/uberwriter/__init__.py b/uberwriter/__init__.py index ed4348d..20bd475 100644 --- a/uberwriter/__init__.py +++ b/uberwriter/__init__.py @@ -32,12 +32,4 @@ def main(): # Run the application. app = application.Application() - # ~ if args: - # ~ for arg in args: - # ~ pass - # ~ else: - # ~ pass - # ~ if options.experimental_features: - # ~ window.use_experimental_features(True) - app.run(sys.argv) diff --git a/uberwriter/application.py b/uberwriter/application.py index 7b199e9..3d0c2e6 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -173,8 +173,6 @@ class Application(Gtk.Application): self.window = window.Window(self) if self.args: self.window.load_file(self.args[0]) - if self.options.experimental_features: - self.window.use_experimental_features(True) self.window.present() diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index d344d0b..812e8d5 100644 --- a/uberwriter/export_dialog.py +++ b/uberwriter/export_dialog.py @@ -35,7 +35,8 @@ from uberwriter.helpers import get_builder LOGGER = logging.getLogger('uberwriter') -class UberwriterExportDialog: + +class Export: """ Manages all the export operations and dialogs """ diff --git a/uberwriter/format_shortcuts.py b/uberwriter/format_shortcuts.py index 0d0b030..19e0a91 100644 --- a/uberwriter/format_shortcuts.py +++ b/uberwriter/format_shortcuts.py @@ -17,18 +17,14 @@ from gettext import gettext as _ -from uberwriter.markup_buffer import MarkupBuffer - class FormatShortcuts(): """Manage the insertion of formatting for insert them using shortcuts """ - def __init__(self, textbuffer, texteditor): self.text_buffer = textbuffer self.text_editor = texteditor - self.regex = MarkupBuffer.regex def rule(self): """insert ruler at cursor diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index 999a4cd..d424ab4 100644 --- a/uberwriter/headerbars.py +++ b/uberwriter/headerbars.py @@ -36,14 +36,14 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods 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 = Gtk.Revealer(name='titlebar-revealer') self.hb_revealer.add(self.hb) self.hb_revealer.props.transition_duration = 1000 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 = 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() diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py index efd0ef5..3ae6d9e 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -23,6 +23,8 @@ import shutil import gi +from gi.overrides.Pango import Pango + gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: disable=E0611 @@ -160,6 +162,7 @@ def exist_executable(command): return shutil.which(command) is not None + def get_descendant(widget, child_name, level, doPrint=False): if widget is not None: if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) + @@ -188,3 +191,8 @@ def get_descendant(widget, child_name, level, doPrint=False): if child is not None: found = get_descendant(child, child_name, level+1, doPrint) # //search the child if found: return found + + +def get_char_width(widget): + return Pango.units_to_double( + widget.get_pango_context().get_metrics().get_approximate_char_width()) diff --git a/uberwriter/inline_preview.py b/uberwriter/inline_preview.py index 6396411..86fead3 100644 --- a/uberwriter/inline_preview.py +++ b/uberwriter/inline_preview.py @@ -14,28 +14,27 @@ # with this program. If not, see . # END LICENSE -import re -import urllib -from urllib.error import URLError -import webbrowser -import subprocess -import tempfile import logging -import threading +import re +import subprocess import telnetlib - +import tempfile +import threading +import urllib +import webbrowser from gettext import gettext as _ +from urllib.error import URLError +from urllib.parse import unquote import gi + gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GdkPixbuf, GObject -from uberwriter import latex_to_PNG +from uberwriter import latex_to_PNG, text_view_markup_handler from uberwriter.settings import Settings from uberwriter.fix_table import FixTable -from uberwriter.markup_buffer import MarkupBuffer - LOGGER = logging.getLogger('uberwriter') GObject.threads_init() # Still needed? @@ -240,7 +239,7 @@ def fill_lexikon_bubble(vocab, lexikon_dict): if lexikon_dict: for entry in lexikon_dict: vocab_label = Gtk.Label.new(vocab + ' ~ ' + entry['class']) - vocab_label.get_style_context().add_class('lexikon_heading') + vocab_label.get_style_context().add_class('lexikon-heading') vocab_label.set_halign(Gtk.Align.START) vocab_label.set_justify(Gtk.Justification.LEFT) grid.attach(vocab_label, 0, i, 3, 1) @@ -248,14 +247,14 @@ def fill_lexikon_bubble(vocab, lexikon_dict): for definition in entry['defs']: i = i + 1 num_label = Gtk.Label.new(definition['num']) - num_label.get_style_context().add_class('lexikon_num') + num_label.get_style_context().add_class('lexikon-num') num_label.set_justify(Gtk.Justification.RIGHT) grid.attach(num_label, 0, i, 1, 1) def_label = Gtk.Label.new(' '.join(definition['description'])) def_label.set_halign(Gtk.Align.START) def_label.set_justify(Gtk.Justification.LEFT) - def_label.get_style_context().add_class('lexikon_definition') + def_label.get_style_context().add_class('lexikon-definition') def_label.props.wrap = True grid.attach(def_label, 1, i, 1, 1) i = i + 1 @@ -264,11 +263,11 @@ def fill_lexikon_bubble(vocab, lexikon_dict): return None -class InlinePreview(): +class InlinePreview: - def __init__(self, view, text_buffer): - self.text_view = view - self.text_buffer = text_buffer + def __init__(self, text_view): + self.text_view = text_view + self.text_buffer = text_view.get_buffer() self.latex_converter = latex_to_PNG.LatexToPNG() cursor_mark = self.text_buffer.get_insert() cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark) @@ -307,7 +306,7 @@ class InlinePreview(): # b.show_all() # a.show_all() self.popover = Gtk.Popover.new(lbl) - self.popover.get_style_context().add_class("QuickPreviewPopup") + self.popover.get_style_context().add_class("quick-preview-popup") self.popover.add(alignment) # a.add(alignment) _dismiss, rect = self.popover.get_pointing_to() @@ -363,8 +362,8 @@ class InlinePreview(): text = self.text_buffer.get_text(start_iter, end_iter, False) - math = MarkupBuffer.regex["MATH"] - link = MarkupBuffer.regex["LINK"] + math = text_view_markup_handler.regex["MATH"] + link = text_view_markup_handler.regex["LINK"] footnote = re.compile(r'\[\^([^\s]+?)\]') image = re.compile(r"!\[(.*?)\]\((.+?)\)") diff --git a/uberwriter/markup_buffer.py b/uberwriter/markup_buffer.py deleted file mode 100644 index 7afc8ae..0000000 --- a/uberwriter/markup_buffer.py +++ /dev/null @@ -1,326 +0,0 @@ -# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- -### BEGIN LICENSE -# Copyright (C) 2012, Wolf Vollprecht -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR -# PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . -### END LICENSE - -import re -import gi - -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk -from gi.repository import Pango - - -class MarkupBuffer(): - regex = { - "ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_ - "STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__ - "STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"), - "BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE), - "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"), - "LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE), - "NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE), - "INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE), - "HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE), - "HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE), - "HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE), - "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"), - "HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"), - "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL), - "LINK": re.compile(r"\(http(.+?)\)") - } - - def __init__(self, window, text_editor, base_leftmargin): - self.margin_multiplier = 10 - self.parent = window - self.text_editor = text_editor - self.text_buffer = text_editor.get_buffer() - - # Styles - self.italic = self.text_buffer.create_tag("italic", - style=Pango.Style.ITALIC) - - self.emph = self.text_buffer.create_tag("emph", - weight=Pango.Weight.BOLD, - style=Pango.Style.NORMAL) - - self.bolditalic = self.text_buffer.create_tag("bolditalic", - weight=Pango.Weight.BOLD, - style=Pango.Style.ITALIC) - - self.headline_two = self.text_buffer.create_tag("headline_two", - weight=Pango.Weight.BOLD, - style=Pango.Style.NORMAL) - - self.normal_indent = self.text_buffer.create_tag('normal_indent', indent=100) - - self.math_text = self.text_buffer.create_tag('math_text') - - self.unfocused_text = self.text_buffer.create_tag('graytag', - foreground="gray") - - self.underline = self.text_buffer.create_tag("underline", - underline=Pango.Underline.SINGLE) - - self.underline.set_property('weight', Pango.Weight.BOLD) - - self.strikethrough = self.text_buffer.create_tag("strikethrough", - strikethrough=True) - - self.centertext = self.text_buffer.create_tag("centertext", - justification=Gtk.Justification.CENTER) - - self.text_buffer.apply_tag( - self.normal_indent, - self.text_buffer.get_start_iter(), - self.text_buffer.get_end_iter() - ) - - self.rev_leftmargin = [] - for i in range(0, 6): - name = "rev_marg_indent_left" + str(i) - self.rev_leftmargin.append(self.text_buffer.create_tag(name)) - self.rev_leftmargin[i].set_property("left-margin", 90 - 10 * (i + 1)) - self.rev_leftmargin[i].set_property("indent", - 10 * (i + 1) - 10) - #self.leftmargin[i].set_property("background", "gray") - - self.leftmargin = [] - - for i in range(0, 6): - name = "marg_indent_left" + str(i) - self.leftmargin.append(self.text_buffer.create_tag(name)) - self.leftmargin[i].set_property("left-margin", base_leftmargin + 10 + 10 * (i + 1)) - self.leftmargin[i].set_property("indent", - 10 * (i + 1) - 10) - - self.leftindent = [] - - for i in range(0, 15): - name = "indent_left" + str(i) - self.leftindent.append(self.text_buffer.create_tag(name)) - self.leftindent[i].set_property("indent", - 10 * (i + 1) - 20) - - self.table_env = self.text_buffer.create_tag('table_env') - self.table_env.set_property('wrap-mode', Gtk.WrapMode.NONE) - self.table_env.set_property('pixels-above-lines', 0) - self.table_env.set_property('pixels-below-lines', 0) - - # Theme - self.text_editor.connect('style-updated', self.apply_current_theme) - self.apply_current_theme() - - def apply_current_theme(self, *_): - # Math text color - (found, color) = self.text_editor.get_style_context().lookup_color('math_text_color') - if not found: - (_, color) = self.text_editor.get_style_context().lookup_color('foreground_color') - self.math_text.set_property("foreground", color.to_string()) - - # Margin - mets = self.text_editor.get_pango_context().get_metrics() - self.set_multiplier(Pango.units_to_double(mets.get_approximate_char_width()) + 1) - - def markup_buffer(self, mode=0): - buf = self.text_buffer - - # Test for shifting first line - # bbs = buf.get_start_iter() - # bbb = buf.get_iter_at_offset(3) - - # buf.apply_tag(self.ftag, bbs, bbb) - - # Modes: - # 0 -> start to end - # 1 -> around the cursor - # 2 -> n.d. - - if mode == 0: - context_start = buf.get_start_iter() - context_end = buf.get_end_iter() - context_offset = 0 - elif mode == 1: - cursor_mark = buf.get_insert() - context_start = buf.get_iter_at_mark(cursor_mark) - context_start.backward_lines(3) - context_end = buf.get_iter_at_mark(cursor_mark) - context_end.forward_lines(2) - context_offset = context_start.get_offset() - - text = buf.get_slice(context_start, context_end, False) - - self.text_buffer.remove_tag(self.italic, context_start, context_end) - - matches = re.finditer(self.regex["ITALIC"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.italic, start_iter, end_iter) - - self.text_buffer.remove_tag(self.emph, context_start, context_end) - - matches = re.finditer(self.regex["STRONG"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.emph, start_iter, end_iter) - - matches = re.finditer(self.regex["STRONGITALIC"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.bolditalic, start_iter, end_iter) - - self.text_buffer.remove_tag(self.strikethrough, context_start, context_end) - - matches = re.finditer(self.regex["STRIKETHROUGH"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.strikethrough, start_iter, end_iter) - - self.text_buffer.remove_tag(self.math_text, context_start, context_end) - - matches = re.finditer(self.regex["MATH"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.math_text, start_iter, end_iter) - - for margin in self.rev_leftmargin: - self.text_buffer.remove_tag(margin, context_start, context_end) - - matches = re.finditer(self.regex["LIST"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.rev_leftmargin[0], start_iter, end_iter) - - matches = re.finditer(self.regex["NUMERICLIST"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - index = len(match.group(1)) - 1 - if index < len(self.rev_leftmargin): - margin = self.rev_leftmargin[index] - self.text_buffer.apply_tag(margin, start_iter, end_iter) - - matches = re.finditer(self.regex["BLOCKQUOTE"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - index = len(match.group(1)) - 2 - if index < len(self.leftmargin): - self.text_buffer.apply_tag(self.leftmargin[index], start_iter, end_iter) - - for leftindent in self.leftindent: - self.text_buffer.remove_tag(leftindent, context_start, context_end) - - matches = re.finditer(self.regex["INDENTEDLIST"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - index = (len(match.group(1)) - 1) * 2 + len(match.group(2)) - if index < len(self.leftindent): - self.text_buffer.apply_tag(self.leftindent[index], start_iter, end_iter) - - matches = re.finditer(self.regex["HEADINDICATOR"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - index = len(match.group(1)) - 1 - if index < len(self.rev_leftmargin): - margin = self.rev_leftmargin[index] - self.text_buffer.apply_tag(margin, start_iter, end_iter) - - matches = re.finditer(self.regex["HORIZONTALRULE"], text) - rulecontext = context_start.copy() - rulecontext.forward_lines(3) - self.text_buffer.remove_tag(self.centertext, rulecontext, context_end) - - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - start_iter.forward_chars(2) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.centertext, start_iter, end_iter) - - matches = re.finditer(self.regex["HEADLINE"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.emph, start_iter, end_iter) - - matches = re.finditer(self.regex["HEADLINE_TWO"], text) - self.text_buffer.remove_tag(self.headline_two, rulecontext, context_end) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.headline_two, start_iter, end_iter) - - matches = re.finditer(self.regex["TABLE"], text) - for match in matches: - start_iter = buf.get_iter_at_offset(context_offset + match.start()) - end_iter = buf.get_iter_at_offset(context_offset + match.end()) - self.text_buffer.apply_tag(self.table_env, start_iter, end_iter) - - if self.parent.focusmode: - self.focusmode_highlight() - - def focusmode_highlight(self): - start_document = self.text_buffer.get_start_iter() - end_document = self.text_buffer.get_end_iter() - - self.text_buffer.remove_tag( - self.unfocused_text, - start_document, - end_document) - - cursor = self.text_buffer.get_mark("insert") - cursor_iter = self.text_buffer.get_iter_at_mark(cursor) - - end_sentence = cursor_iter.copy() - end_sentence.forward_sentence_end() - - end_line = cursor_iter.copy() - end_line.forward_to_line_end() - - comp = end_line.compare(end_sentence) - # if comp < 0, end_line is BEFORE end_sentence - if comp <= 0: - end_sentence = end_line - - start_sentence = cursor_iter.copy() - start_sentence.backward_sentence_start() - - # grey out everything before - self.text_buffer.apply_tag( - self.unfocused_text, - self.text_buffer.get_start_iter(), start_sentence) - - self.text_buffer.apply_tag( - self.unfocused_text, - end_sentence, self.text_buffer.get_end_iter()) - - def set_multiplier(self, multiplier): - self.margin_multiplier = multiplier - - def recalculate(self, lm): - multiplier = self.margin_multiplier - for i in range(0, 6): - new_margin = (lm - multiplier) - multiplier * (i + 1) - self.rev_leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin) - self.rev_leftmargin[i].set_property("indent", - multiplier * (i + 1) - multiplier) - - for i in range(0, 6): - new_margin = (lm - multiplier) + multiplier + multiplier * (i + 1) - self.leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin) - self.leftmargin[i].set_property("indent", - (multiplier - 1) * (i + 1) - multiplier) diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 63c40a0..32cbfa5 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -14,21 +14,26 @@ # with this program. If not, see . ### END LICENSE -import re import logging +import re + import gi + gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk +from gi.repository import Gdk + # from plugins import plugins LOGGER = logging.getLogger('uberwriter') -class SearchAndReplace(): + +class SearchAndReplace: """ Adds (regex) search and replace functionality to uberwriter """ - def __init__(self, parentwindow): + + def __init__(self, parentwindow, textview): self.parentwindow = parentwindow self.box = parentwindow.builder.get_object("searchbar_placeholder") self.box.set_reveal_child(False) @@ -41,8 +46,8 @@ class SearchAndReplace(): self.open_replace_button = parentwindow.builder.get_object("replace") self.open_replace_button.connect("toggled", self.toggle_replace) - self.textbuffer = parentwindow.text_buffer - self.texteditor = parentwindow.text_editor + self.textview = textview + self.textbuffer = textview.get_buffer() self.nextbutton = parentwindow.builder.get_object("next_result") self.prevbutton = parentwindow.builder.get_object("previous_result") @@ -66,7 +71,7 @@ class SearchAndReplace(): self.highlight = self.textbuffer.create_tag('search_highlight', background="yellow") - self.texteditor.connect("focus-in-event", self.focused_texteditor) + self.textview.connect("focus-in-event", self.focused_texteditor) def toggle_replace(self, widget, _data=None): """toggle the replace box @@ -100,7 +105,6 @@ class SearchAndReplace(): self.hide() self.open_replace_button.set_active(False) - def search(self, _widget=None, _data=None, scroll=True): searchtext = self.searchentry.get_text() buf = self.textbuffer @@ -147,7 +151,7 @@ class SearchAndReplace(): self.active = 0 matchiter = self.matchiters[self.active] - self.texteditor.get_buffer().select_range(matchiter[0], matchiter[1]) + self.textview.get_buffer().select_range(matchiter[0], matchiter[1]) # self.texteditor.scroll_to_iter(matchiter[0], 0.0, True, 0.0, 0.5) @@ -157,8 +161,7 @@ class SearchAndReplace(): self.textbuffer.remove_tag(self.highlight, self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter()) - self.texteditor.grab_focus() - + self.textview.grab_focus() def replace_clicked(self, _widget, _data=None): self.replace(self.active) @@ -177,5 +180,4 @@ class SearchAndReplace(): active = self.active self.search(scroll=False) self.active = active - self.parentwindow.MarkupBuffer.markup_buffer() self.scrollto(self.active) diff --git a/uberwriter/text_editor.py b/uberwriter/text_editor.py deleted file mode 100644 index 60919e1..0000000 --- a/uberwriter/text_editor.py +++ /dev/null @@ -1,487 +0,0 @@ -### BEGIN LICENSE -# Copyright (C) 2012, Wolf Vollprecht -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR -# PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . -### END LICENSE -"""Module for the TextView widgth wich encapsulates management of TextBuffer -and TextIter for common functionality, such as cut, copy, paste, undo, redo, -and highlighting of text. - -Using -#create the TextEditor and set the text -editor = TextEditor() -editor.text = "Text to add to the editor" - -#use cut, works the same for copy, paste, undo, and redo -def __handle_on_cut(self, widget, data=None): - self.editor.cut() - -#add string to highlight -self.editor.add_highlight("Ubuntu") -self.editor.add_highlight("Quickly") - -#remove highlights -self.editor.clear_highlight("Ubuntu") -self.editor.clear_all_highlight() - -Configuring -#Configure as a TextView -self.editor.set_wrap_mode(Gtk.WRAP_CHAR) - -#Access the Gtk.TextBuffer if needed -buffer = self.editor.get_buffer() - -Extending -A TextEditor is Gtk.TextView - -""" - -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, GObject -from uberwriter.format_shortcuts import FormatShortcuts - -import logging -LOGGER = logging.getLogger('uberwriter') - - -class UndoableInsert: - """something that has been inserted into our textbuffer""" - def __init__(self, text_iter, text, length): - self.offset = text_iter.get_offset() - self.text = text - self.length = length - if self.length > 1 or self.text in ("\r", "\n", " "): - self.mergeable = False - else: - self.mergeable = True - - -class UndoableDelete: - """something that has ben deleted from our textbuffer""" - def __init__(self, text_buffer, start_iter, end_iter): - self.text = text_buffer.get_text(start_iter, end_iter, False) - self.start = start_iter.get_offset() - self.end = end_iter.get_offset() - # need to find out if backspace or delete key has been used - # so we don't mess up during redo - insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert()) - - self.delete_key_used = bool(insert_iter.get_offset() <= self.start) - self.mergeable = not bool(self.end - self.start > 1 - or self.text in ("\r", "\n", " ")) - - -class UberwriterTextEditor(Gtk.TextView): - """TextEditor encapsulates management of TextBuffer and TextIter for - common functionality, such as cut, copy, paste, undo, redo, and - highlighting of text. - """ - - __gsignals__ = { - 'insert-italic': (GObject.SignalFlags.ACTION, None, ()), - 'insert-bold': (GObject.SignalFlags.ACTION, None, ()), - 'insert-hrule': (GObject.SignalFlags.ACTION, None, ()), - 'insert-ulistitem': (GObject.SignalFlags.ACTION, None, ()), - 'insert-heading': (GObject.SignalFlags.ACTION, None, ()), - 'insert-strikeout': (GObject.SignalFlags.ACTION, None, ()), - 'undo': (GObject.SignalFlags.ACTION, None, ()), - 'redo': (GObject.SignalFlags.ACTION, None, ()) - } - - def scroll_to_iter(self, iterable, *args): - self.get_buffer().place_cursor(iterable) - - def __init__(self): - """ - Create a TextEditor - """ - - Gtk.TextView.__init__(self) - self.undo_max = None - - self.insert_event = self.get_buffer().connect("insert-text", - self.on_insert_text) - self.delete_event = self.get_buffer().connect("delete-range", - self.on_delete_range) - display = self.get_display() - self.clipboard = Gtk.Clipboard.get_for_display(display, - Gdk.SELECTION_CLIPBOARD) - - self.undo_stack = [] - self.redo_stack = [] - self.not_undoable_action = False - self.undo_in_progress = False - - self.can_delete = True - self.connect('key-press-event', self.on_key_press_event) - - self.format_shortcuts = FormatShortcuts(self.get_buffer(), self) - - self.connect('insert-italic', self.set_italic) - self.connect('insert-bold', self.set_bold) - self.connect('insert-strikeout', self.set_strikeout) - self.connect('insert-hrule', self.insert_horizontal_rule) - self.connect('insert-ulistitem', self.insert_unordered_list_item) - self.connect('insert-heading', self.insert_heading) - self.connect('redo', self.redo) - self.connect('undo', self.undo) - - self.get_style_context().add_class("uberwriter-editor") - - @property - def text(self): - """ - text - a string specifying all the text currently - in the TextEditor's buffer. - - This property is read/write. - """ - start_iter = self.get_buffer().get_iter_at_offset(0) - end_iter = self.get_buffer().get_iter_at_offset(-1) - return self.get_buffer().get_text(start_iter, end_iter, False) - - @property - def can_undo(self): - return bool(self.undo_stack) - - @property - def can_redo(self): - return bool(self.redo_stack) - - @text.setter - def text(self, text): - self.get_buffer().set_text(text) - - def append(self, text): - """append: appends text to the end of the textbuffer. - - arguments: - text - a string to add to the buffer. The text will be the - last text in the buffer. The insertion cursor will not be moved. - - """ - - end_iter = self.get_buffer().get_iter_at_offset(-1) - self.get_buffer().insert(end_iter, text) - - def prepend(self, text): - """prepend: appends text to the start of the textbuffer. - - arguments: - text - a string to add to the buffer. The text will be the - first text in the buffer. The insertion cursor will not be moved. - - """ - - start_iter = self.get_buffer().get_iter_at_offset(0) - self.get_buffer().insert(start_iter, text) - insert_iter = self.get_buffer().get_iter_at_offset(len(text)-1) - self.get_buffer().place_cursor(insert_iter) - - def cursor_to_end(self): - """cursor_to_end: moves the insertion curson to the last position - in the buffer. - - """ - - end_iter = self.get_buffer().get_iter_at_offset(-1) - self.get_buffer().place_cursor(end_iter) - - def cursor_to_start(self): - """cursor_to_start: moves the insertion curson to the first position - in the buffer. - - """ - - start_iter = self.get_buffer().get_iter_at_offset(0) - self.get_buffer().place_cursor(start_iter) - - def cut(self, _widget=None, _data=None): - """cut: cut currently selected text and put it on the clipboard. - This function can be called as a function, or assigned as a signal - handler. - - """ - - self.get_buffer().cut_clipboard(self.clipboard, True) - - def copy(self, _widget=None, _data=None): - """copy: copy currently selected text to the clipboard. - This function can be called as a function, or assigned as a signal - handler. - """ - self.get_buffer().copy_clipboard(self.clipboard) - - def paste(self, _widget=None, _data=None): - """paste: Insert any text currently on the clipboard into the - buffer. - This function can be called as a function, or assigned as a signal - handler. - - """ - - self.get_buffer().paste_clipboard(self.clipboard, None, True) - - def undo(self, _widget=None, _data=None): - """undo inserts or deletions - undone actions are being moved to redo stack""" - if not self.undo_stack: - return - self.begin_not_undoable_action() - self.undo_in_progress = True - undo_action = self.undo_stack.pop() - self.redo_stack.append(undo_action) - buf = self.get_buffer() - if isinstance(undo_action, UndoableInsert): - offset = undo_action.offset - start = buf.get_iter_at_offset(offset) - stop = buf.get_iter_at_offset( - offset + undo_action.length - ) - buf.place_cursor(start) - buf.delete(start, stop) - else: - start = buf.get_iter_at_offset(undo_action.start) - buf.insert(start, undo_action.text) - if undo_action.delete_key_used: - buf.place_cursor(start) - else: - stop = buf.get_iter_at_offset(undo_action.end) - buf.place_cursor(stop) - self.end_not_undoable_action() - self.undo_in_progress = False - - def redo(self, _widget=None, _data=None): - """redo inserts or deletions - - redone actions are moved to undo stack""" - if not self.redo_stack: - return - self.begin_not_undoable_action() - self.undo_in_progress = True - redo_action = self.redo_stack.pop() - self.undo_stack.append(redo_action) - buf = self.get_buffer() - if isinstance(redo_action, UndoableInsert): - start = buf.get_iter_at_offset(redo_action.offset) - buf.insert(start, redo_action.text) - new_cursor_pos = buf.get_iter_at_offset( - redo_action.offset + redo_action.length - ) - buf.place_cursor(new_cursor_pos) - else: - start = buf.get_iter_at_offset(redo_action.start) - stop = buf.get_iter_at_offset(redo_action.end) - buf.delete(start, stop) - buf.place_cursor(start) - self.end_not_undoable_action() - self.undo_in_progress = False - - def on_insert_text(self, _textbuffer, text_iter, text, _length): - """ - _on_insert: internal function to handle programatically inserted - text. Do not call directly. - """ - def can_be_merged(prev, cur): - """see if we can merge multiple inserts here - - will try to merge words or whitespace - can't merge if prev and cur are not mergeable in the first place - can't merge when user set the input bar somewhere else - can't merge across word boundaries""" - whitespace = (' ', '\t') - if not cur.mergeable or not prev.mergeable: - return False - if cur.offset != (prev.offset + prev.length): - return False - if cur.text in whitespace and not prev.text in whitespace: - return False - if prev.text in whitespace and not cur.text in whitespace: - return False - return True - - if not self.undo_in_progress: - self.redo_stack = [] - if self.not_undoable_action: - return - - undo_action = UndoableInsert(text_iter, text, len(text)) - try: - prev_insert = self.undo_stack.pop() - except IndexError: - self.undo_stack.append(undo_action) - return - if not isinstance(prev_insert, UndoableInsert): - self.undo_stack.append(prev_insert) - self.undo_stack.append(undo_action) - return - if can_be_merged(prev_insert, undo_action): - prev_insert.length += undo_action.length - prev_insert.text += undo_action.text - self.undo_stack.append(prev_insert) - else: - self.undo_stack.append(prev_insert) - self.undo_stack.append(undo_action) - - def on_delete_range(self, text_buffer, start_iter, end_iter): - """On delete - """ - def can_be_merged(prev, cur): - """see if we can merge multiple deletions here - - will try to merge words or whitespace - can't merge if prev and cur are not mergeable in the first place - can't merge if delete and backspace key were both used - can't merge across word boundaries""" - - whitespace = (' ', '\t') - if not cur.mergeable or not prev.mergeable: - return False - if prev.delete_key_used != cur.delete_key_used: - return False - if prev.start != cur.start and prev.start != cur.end: - return False - if cur.text not in whitespace and \ - prev.text in whitespace: - return False - if cur.text in whitespace and \ - prev.text not in whitespace: - return False - return True - - if not self.undo_in_progress: - self.redo_stack = [] - if self.not_undoable_action: - return - undo_action = UndoableDelete(text_buffer, start_iter, end_iter) - try: - prev_delete = self.undo_stack.pop() - except IndexError: - self.undo_stack.append(undo_action) - return - if not isinstance(prev_delete, UndoableDelete): - self.undo_stack.append(prev_delete) - self.undo_stack.append(undo_action) - return - if can_be_merged(prev_delete, undo_action): - if prev_delete.start == undo_action.start: # delete key used - prev_delete.text += undo_action.text - prev_delete.end += (undo_action.end - undo_action.start) - else: # Backspace used - prev_delete.text = "%s%s" % (undo_action.text, - prev_delete.text) - prev_delete.start = undo_action.start - self.undo_stack.append(prev_delete) - else: - self.undo_stack.append(prev_delete) - self.undo_stack.append(undo_action) - - def begin_not_undoable_action(self): - """don't record the next actions - toggles self.not_undoable_action""" - self.not_undoable_action = True - - def end_not_undoable_action(self): - """record next actions - toggles self.not_undoable_action""" - self.not_undoable_action = False - - def on_key_press_event(self, widget, event): - if widget == self and not self.can_delete: - return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete - else: - return False - - def set_italic(self, _widget, _data=None): - """Ctrl + I""" - self.format_shortcuts.italic() - - def set_bold(self, _widget, _data=None): - """Ctrl + Shift + D""" - self.format_shortcuts.bold() - - def set_strikeout(self, _widget, _data=None): - """Ctrl + B""" - self.format_shortcuts.strikeout() - - def insert_horizontal_rule(self, _widget, _data=None): - """Ctrl + R""" - self.format_shortcuts.rule() - - def insert_unordered_list_item(self, _widget, _data=None): - """Ctrl + U""" - self.format_shortcuts.unordered_list_item() - - def insert_ordered_list(self, _widget, _data=None): - """CTRL + O""" - self.format_shortcuts.ordered_list_item() - - def insert_heading(self, _widget, _data=None): - """CTRL + H""" - self.format_shortcuts.heading() - - -class TestWindow(Gtk.Window): - """For testing and demonstrating AsycnTaskProgressBox. - - """ - def __init__(self): - # create a window a VBox to hold the controls - Gtk.Window.__init__(self) - self.set_title("TextEditor Test Window") - windowbox = Gtk.VBox(homogeneous=False, spacing=2) - windowbox.show() - self.add(windowbox) - self.editor = UberwriterTextEditor() - self.editor.show() - windowbox.pack_end(self.editor, True, True, 0) - self.set_size_request(200, 200) - self.show() - self.maximize() - - self.connect("destroy", Gtk.main_quit) - self.editor.text = "this is some inserted text" - self.editor.append("\nLine 3") - self.editor.prepend("Line1\n") - self.editor.cursor_to_end() - self.editor.cursor_to_start() - self.editor.undo_max = 100 - cut_button = Gtk.Button(label="Cut") - cut_button.connect("clicked", self.editor.cut) - cut_button.show() - windowbox.pack_start(cut_button, False, False, 0) - - copy_button = Gtk.Button(label="Copy") - copy_button.connect("clicked", self.editor.copy) - copy_button.show() - windowbox.pack_start(copy_button, False, False, 0) - - paste_button = Gtk.Button(label="Paste") - paste_button.connect("clicked", self.editor.paste) - paste_button.show() - windowbox.pack_start(paste_button, False, False, 0) - - undo_button = Gtk.Button(label="Undo") - undo_button.connect("clicked", self.editor.undo) - undo_button.show() - windowbox.pack_start(undo_button, False, False, 0) - - redo_button = Gtk.Button(label="Redo") - redo_button.connect("clicked", self.editor.redo) - redo_button.show() - windowbox.pack_start(redo_button, False, False, 0) - - -if __name__ == "__main__": - TEST = TestWindow() - Gtk.main() diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py new file mode 100644 index 0000000..d580b78 --- /dev/null +++ b/uberwriter/text_view.py @@ -0,0 +1,175 @@ +import gi + +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_undo_redo_handler import UndoRedoHandler +from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, \ + TARGET_TEXT +from uberwriter.text_view_scroller import Scroller + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, GObject + +import logging +LOGGER = logging.getLogger('uberwriter') + + +class TextView(Gtk.TextView): + """UberwriterTextView encapsulates all the features around the editor. + + It combines the following: + - Undo / redo (via TextBufferUndoRedoHandler) + - Format shortcuts (via TextBufferShortcutInserter) + - Markup (via TextBufferMarkupHandler) + - Preview popover (via TextBufferMarkupHandler) + - Drag and drop (via TextViewDragDropHandler) + - Scrolling (via TextViewScroller) + - The various modes supported by UberWriter (eg. Focus Mode, Hemingway Mode) + """ + + __gsignals__ = { + 'insert-italic': (GObject.SignalFlags.ACTION, None, ()), + 'insert-bold': (GObject.SignalFlags.ACTION, None, ()), + 'insert-hrule': (GObject.SignalFlags.ACTION, None, ()), + 'insert-listitem': (GObject.SignalFlags.ACTION, None, ()), + 'insert-header': (GObject.SignalFlags.ACTION, None, ()), + 'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()), + 'undo': (GObject.SignalFlags.ACTION, None, ()), + 'redo': (GObject.SignalFlags.ACTION, None, ()) + } + + def __init__(self): + super().__init__() + + # Appearance + self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self.set_pixels_above_lines(4) + self.set_pixels_below_lines(4) + self.set_pixels_inside_wrap(8) + self.get_style_context().add_class('uberwriter-editor') + + # General behavior + self.get_buffer().connect('changed', self.on_text_changed) + self.connect('size-allocate', self.on_size_allocate) + + # Undo / redo + self.undo_redo = UndoRedoHandler() + self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text) + self.get_buffer().connect('delete-range', self.undo_redo.on_delete_range) + self.connect('undo', self.undo_redo.undo) + self.connect('redo', self.undo_redo.redo) + + # Format shortcuts + self.shortcut = FormatInserter() + self.connect('insert-italic', self.shortcut.insert_italic) + self.connect('insert-bold', self.shortcut.insert_bold) + self.connect('insert-strikethrough', self.shortcut.insert_strikethrough) + self.connect('insert-hrule', self.shortcut.insert_horizontal_rule) + self.connect('insert-listitem', self.shortcut.insert_list_item) + self.connect('insert-header', self.shortcut.insert_header) + + # Markup + self.markup = MarkupHandler(self) + self.connect('style-updated', self.markup.on_style_updated) + self.get_buffer().connect('paste-done', self.on_paste_done) + + # Preview popover + self.preview_popover = InlinePreview(self) + + # Drag and drop + self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT) + + # Scrolling + self.scroller = Scroller() + self.get_buffer().connect('mark-set', self.on_mark_set) + + # Focus mode + self.focus_mode = False + self.original_top_margin = self.props.top_margin + self.original_bottom_margin = self.props.bottom_margin + self.connect('button-release-event', self.on_button_release_event) + + # Hemingway mode + self.hemingway_mode = False + self.connect('key-press-event', self.on_key_press_event) + + def get_text(self): + text_buffer = self.get_buffer() + start_iter = text_buffer.get_start_iter() + end_iter = text_buffer.get_end_iter() + return text_buffer.get_text(start_iter, end_iter, False) + + def set_text(self, text): + text_buffer = self.get_buffer() + text_buffer.set_text(text) + self.markup.apply() # TODO + + def on_text_changed(self, *_): + self.markup.apply(True) + self.scroll_to() + + def on_paste_done(self, *_): + self.markup.apply() + + def on_size_allocate(self, *_): + self.update_vertical_margin() + self.markup.update_margins() + + def set_focus_mode(self, focus_mode): + """Toggle focus mode. + + When in focus mode, the cursor sits in the middle of the text view, + and the surrounding text is greyed out.""" + + self.focus_mode = focus_mode + self.update_vertical_margin() + self.markup.apply() + self.scroll_to() + + def update_vertical_margin(self): + if self.focus_mode: + height = self.get_allocation().height + self.props.top_margin = height / 2 + self.props.bottom_margin = height / 2 + else: + self.props.top_margin = self.original_top_margin + self.props.bottom_margin = self.original_bottom_margin + + def on_button_release_event(self, _widget, _event): + if self.focus_mode: + self.markup.apply(True) + return False + + def set_hemingway_mode(self, hemingway_mode): + """Toggle hemingway mode. + + When in hemingway mode, the backspace and delete keys are ignored.""" + + self.hemingway_mode = hemingway_mode + + def on_key_press_event(self, _widget, event): + if self.hemingway_mode: + return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete + else: + return False + + def clear(self): + """Clear text and undo history""" + + self.get_buffer().set_text('') + self.undo_redo.clear() + + def scroll_to(self, mark=None): + """Scrolls if needed to ensure mark is visible. + + If mark is unspecified, the cursor is used.""" + + self.scroller.scroll_to(self, mark, self.focus_mode) + + def on_mark_set(self, _text_buffer, _location, mark, _data=None): + if mark.get_name() == 'insert': + self.markup.apply(not self.focus_mode) + elif mark.get_name() == 'gtk_drag_target': + self.scroll_to(mark) + return True diff --git a/uberwriter/text_view_drag_drop_handler.py b/uberwriter/text_view_drag_drop_handler.py new file mode 100644 index 0000000..1b6c593 --- /dev/null +++ b/uberwriter/text_view_drag_drop_handler.py @@ -0,0 +1,62 @@ +import mimetypes +import urllib + +from gi.repository import Gtk + +(TARGET_URI, TARGET_TEXT) = range(2) + + +class DragDropHandler: + TARGET_URI = None + + def __init__(self, text_view, *targets): + super().__init__() + + self.target_list = Gtk.TargetList.new([]) + if TARGET_URI in targets: + self.target_list.add_uri_targets(TARGET_URI) + if TARGET_TEXT in targets: + self.target_list.add_text_targets(TARGET_TEXT) + + text_view.drag_dest_set_target_list(self.target_list) + text_view.connect_after('drag-data-received', self.on_drag_data_received) + + def on_drag_data_received(self, text_view, drag_context, _x, _y, data, info, time): + """Handle drag and drop events""" + + text_buffer = text_view.get_buffer() + if info == TARGET_URI: + uris = data.get_uris() + for uri in uris: + uri = urllib.parse.unquote_plus(uri) + mime = mimetypes.guess_type(uri) + + if mime[0] is not None and mime[0].startswith('image'): + if uri.startswith("file://"): + uri = uri[7:] + text = "![Image caption](%s)" % uri + limit_left = 2 + limit_right = 23 + else: + text = "[Link description](%s)" % uri + limit_left = 1 + limit_right = 22 + text_buffer.place_cursor(text_buffer.get_iter_at_mark( + text_buffer.get_mark('gtk_drag_target'))) + text_buffer.insert_at_cursor(text) + insert_mark = text_buffer.get_insert() + selection_bound = text_buffer.get_selection_bound() + cursor_iter = text_buffer.get_iter_at_mark(insert_mark) + cursor_iter.backward_chars(len(text) - limit_left) + text_buffer.move_mark(insert_mark, cursor_iter) + cursor_iter.forward_chars(limit_right) + text_buffer.move_mark(selection_bound, cursor_iter) + + elif info == TARGET_TEXT: + text_buffer.place_cursor(text_buffer.get_iter_at_mark( + text_buffer.get_mark('gtk_drag_target'))) + text_buffer.insert_at_cursor(data.get_text()) + + Gtk.drag_finish(drag_context, True, True, time) + text_view.get_window().present_with_time(time) + return False diff --git a/uberwriter/text_view_format_inserter.py b/uberwriter/text_view_format_inserter.py new file mode 100644 index 0000000..5cbaf61 --- /dev/null +++ b/uberwriter/text_view_format_inserter.py @@ -0,0 +1,154 @@ +from gettext import gettext as _ + + +class FormatInserter: + """Manages insertion of formatting. + + Methods can be called directly, as well as be used as signal callbacks.""" + + def insert_italic(self, text_view, _data=None): + """Insert italic or mark a selection as bold""" + + self.__wrap(text_view, "_", _("italic text")) + + def insert_bold(self, text_view, _data=None): + """Insert bold or mark a selection as bold""" + + self.__wrap(text_view, "**", _("bold text")) + + def insert_strikethrough(self, text_view, _data=None): + """Insert strikethrough or mark a selection as strikethrough""" + + self.__wrap(text_view, "~~", _("strikethrough text")) + + def insert_horizontal_rule(self, text_view, _data=None): + """Insert horizontal rule""" + + text_buffer = text_view.get_buffer() + text_buffer.insert_at_cursor("\n\n---\n") + text_view.scroll_mark_onscreen(text_buffer.get_insert()) + + def insert_list_item(self, text_view, _data=None): + """Insert list item or mark a selection as list item""" + + text_buffer = text_view.get_buffer() + if text_buffer.get_has_selection(): + (start, end) = text_buffer.get_selection_bounds() + if start.starts_line(): + text = text_buffer.get_text(start, end, False) + if text.startswith(("- ", "* ", "+ ")): + delete_end = start.forward_chars(2) + text_buffer.delete(start, delete_end) + else: + text_buffer.insert(start, "- ") + else: + helptext = _("Item") + text_length = len(helptext) + + cursor_mark = text_buffer.get_insert() + cursor_iter = text_buffer.get_iter_at_mark(cursor_mark) + + start_ext = cursor_iter.copy() + start_ext.backward_lines(3) + text = text_buffer.get_text(cursor_iter, start_ext, False) + lines = text.splitlines() + + for line in reversed(lines): + if line and line.startswith(("- ", "* ", "+ ")): + if cursor_iter.starts_line(): + text_buffer.insert_at_cursor(line[:2] + helptext) + else: + text_buffer.insert_at_cursor( + "\n" + line[:2] + helptext) + break + else: + if not lines[-1] and not lines[-2]: + text_buffer.insert_at_cursor("- " + helptext) + elif not lines[-1]: + if cursor_iter.starts_line(): + text_buffer.insert_at_cursor("- " + helptext) + else: + text_buffer.insert_at_cursor("\n- " + helptext) + else: + text_buffer.insert_at_cursor("\n\n- " + helptext) + break + + self.__select_text(text_view, 0, text_length) + + def insert_ordered_list_item(self, _text_view, _data=None): + # TODO: implement ordered lists + pass + + def insert_header(self, text_view, _data=None): + """Insert header or mark a selection as a list header""" + + text_buffer = text_view.get_buffer() + if text_buffer.get_has_selection(): + (start, end) = text_buffer.get_selection_bounds() + text = text_buffer.get_text(start, end, False) + text_buffer.delete(start, end) + else: + text = _("Header") + + text_buffer.insert_at_cursor("#" + " " + text) + self.__select_text(text_view, 0, len(text)) + + @staticmethod + def __wrap(text_view, wrap, helptext=""): + """Inserts wrap format to the selected text (helper text when nothing selected)""" + text_buffer = text_view.get_buffer() + if text_buffer.get_has_selection(): + # Find current highlighting + (start, end) = text_buffer.get_selection_bounds() + moved = False + if (start.get_offset() >= len(wrap) and + end.get_offset() <= text_buffer.get_char_count() - len(wrap)): + moved = True + ext_start = start.copy() + ext_start.backward_chars(len(wrap)) + ext_end = end.copy() + ext_end.forward_chars(len(wrap)) + text = text_buffer.get_text(ext_start, ext_end, True) + else: + text = text_buffer.get_text(start, end, True) + + if moved and text.startswith(wrap) and text.endswith(wrap): + text = text[len(wrap):-len(wrap)] + new_text = text + text_buffer.delete(ext_start, ext_end) + move_back = 0 + else: + if moved: + text = text[len(wrap):-len(wrap)] + new_text = text.lstrip().rstrip() + text = text.replace(new_text, wrap + new_text + wrap) + + text_buffer.delete(start, end) + move_back = len(wrap) + + text_buffer.insert_at_cursor(text) + text_length = len(new_text) + + else: + text_buffer.insert_at_cursor(wrap + helptext + wrap) + text_length = len(helptext) + move_back = len(wrap) + + cursor_mark = text_buffer.get_insert() + cursor_iter = text_buffer.get_iter_at_mark(cursor_mark) + cursor_iter.backward_chars(move_back) + text_buffer.move_mark_by_name('selection_bound', cursor_iter) + cursor_iter.backward_chars(text_length) + text_buffer.move_mark_by_name('insert', cursor_iter) + + @staticmethod + def __select_text(text_view, offset, length): + """Selects text starting at the current cursor minus offset, length characters.""" + + text_buffer = text_view.get_buffer() + cursor_mark = text_buffer.get_insert() + cursor_iter = text_buffer.get_iter_at_mark(cursor_mark) + cursor_iter.backward_chars(offset) + text_buffer.move_mark_by_name('selection_bound', cursor_iter) + cursor_iter.backward_chars(length) + text_buffer.move_mark_by_name('insert', cursor_iter) diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py new file mode 100644 index 0000000..ed1d9da --- /dev/null +++ b/uberwriter/text_view_markup_handler.py @@ -0,0 +1,246 @@ +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# Copyright (C) 2012, Wolf Vollprecht +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +### END LICENSE + +import re +import gi + +from uberwriter import helpers + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +from gi.repository import Pango + + +class MarkupHandler: + regex = { + "ITALIC": re.compile(r"(\*|_)(.*?)\1"), + "BOLD": re.compile(r"(\*\*|__)(.*?)\1"), + "BOLDITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"), + "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"), + "LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-\*\+][ ].+", 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}) .+", re.MULTILINE), + "HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[\=\-]{3,}", re.MULTILINE), + "HORIZONTALRULE": re.compile(r"^\n([ ]{0,3}[\*\-_]{3,}[ ]*)\n", re.MULTILINE), + "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL), + "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"), + } + + def __init__(self, text_view): + self.text_view = text_view + self.text_buffer = text_view.get_buffer() + + # Styles + buffer = self.text_buffer + self.italic = buffer.create_tag('italic', + weight=Pango.Weight.NORMAL, + style=Pango.Style.ITALIC) + + self.bold = buffer.create_tag('bold', + weight=Pango.Weight.BOLD, + style=Pango.Style.NORMAL) + + self.bolditalic = buffer.create_tag('bolditalic', + weight=Pango.Weight.BOLD, + style=Pango.Style.ITALIC) + + self.graytext = buffer.create_tag('graytext', foreground='gray') + + self.strikethrough = buffer.create_tag('strikethrough', strikethrough=True) + + self.centertext = buffer.create_tag('centertext', justification=Gtk.Justification.CENTER) + + self.invisible = buffer.create_tag('invisible', invisible=True) + + 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.mathtext = buffer.create_tag('mathtext') + + # Margins + # A default margin is set to allow negative indents for formatting headers, lists, etc + self.baseline_margin = 0 + self.margins = {} + self.update_margins() + + # Style + self.on_style_updated() + + def on_style_updated(self, *_): + (found, color) = self.text_view.get_style_context().lookup_color('math_text_color') + if not found: + (_, color) = self.text_view.get_style_context().lookup_color('foreground_color') + self.mathtext.set_property("foreground", color.to_string()) + + def apply(self, around_cursor=False): + buffer = self.text_buffer + if around_cursor: + cursor_mark = buffer.get_insert() + start = buffer.get_iter_at_mark(cursor_mark) + start.backward_lines(3) + end = buffer.get_iter_at_mark(cursor_mark) + end.forward_lines(2) + offset = start.get_offset() + else: + start = buffer.get_start_iter() + end = buffer.get_end_iter() + offset = 0 + + text = buffer.get_slice(start, end, False) + + # Remove tags + buffer.remove_tag(self.italic, start, end) + buffer.remove_tag(self.bold, start, end) + buffer.remove_tag(self.bolditalic, start, end) + buffer.remove_tag(self.strikethrough, start, end) + buffer.remove_tag(self.mathtext, start, end) + buffer.remove_tag(self.centertext, start, end) + for tag in self.margins.values(): + buffer.remove_tag(tag, start, end) + buffer.remove_tag(self.graytext, start, end) + + # Apply "_italic_" tag (italic) + matches = re.finditer(self.regex["ITALIC"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.italic, start_iter, end_iter) + + # Apply "**bold**" tag (bold) + matches = re.finditer(self.regex["BOLD"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.bold, start_iter, end_iter) + + # Apply "***bolditalic***" tag (bold/italic) + matches = re.finditer(self.regex["BOLDITALIC"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.bolditalic, start_iter, end_iter) + + # Apply "~~strikethrough~~" tag (strikethrough) + matches = re.finditer(self.regex["STRIKETHROUGH"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.strikethrough, start_iter, end_iter) + + # Apply "* list" tag (offset) + matches = re.finditer(self.regex["LIST"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + # Lists use character+space (eg. "* ") + indent = 2 + nest = len(match.group(1).replace(" ", "\t")) + buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter) + + # Apply "1. numbered list" tag (offset) + matches = re.finditer(self.regex["NUMBEREDLIST"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + # Numeric lists use numbers/letters+dot/parens+space (eg. "123. ") + indent = len(match.group(2)) + 1 + nest = len(match.group(1).replace(" ", "\t")) + buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter) + + # Apply "> blockquote" tag (offset) + matches = re.finditer(self.regex["BLOCKQUOTE"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.get_margin(2), start_iter, end_iter) + + # Apply "#" tag (offset + bold) + matches = re.finditer(self.regex["HEADER"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + indent = -len(match.group(1)) - 1 + buffer.apply_tag(self.get_margin(indent), start_iter, end_iter) + buffer.apply_tag(self.bold, start_iter, end_iter) + + # Apply "======" header underline tag (bold) + matches = re.finditer(self.regex["HEADER_UNDER"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.bold, start_iter, end_iter) + + # Apply "---" horizontal rule tag (center) + matches = re.finditer(self.regex["HORIZONTALRULE"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start(1)) + end_iter = buffer.get_iter_at_offset(offset + match.end(1)) + buffer.apply_tag(self.centertext, start_iter, end_iter) + + # Apply "---" table tag (wrap/pixels) + matches = re.finditer(self.regex["TABLE"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.table, start_iter, end_iter) + + # Apply "$math$" tag (colorize) + matches = re.finditer(self.regex["MATH"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start()) + end_iter = buffer.get_iter_at_offset(offset + match.end()) + buffer.apply_tag(self.mathtext, start_iter, end_iter) + + # Apply focus mode tag (grey out before/after current sentence) + if self.text_view.focus_mode: + cursor_iter = buffer.get_iter_at_mark(buffer.get_insert()) + start_sentence = cursor_iter.copy() + start_sentence.backward_sentence_start() + end_sentence = cursor_iter.copy() + end_sentence.forward_sentence_end() + if start.compare(start_sentence) <= 0: + buffer.apply_tag(self.graytext, start, start_sentence) + if end.compare(end_sentence) >= 0: + buffer.apply_tag(self.graytext, end_sentence, end) + + def get_margin(self, level): + if level not in self.margins: + char_width = helpers.get_char_width(self.text_view) + tag = self.text_buffer.create_tag("indent_" + str(level)) + tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0)) + self.margins[level] = tag + return self.margins[level] + + def update_margins(self): + char_width = helpers.get_char_width(self.text_view) + + # Adjust tab size, as character width can change + tab_array = Pango.TabArray.new(1, True) + tab_array.set_tab(0, Pango.TabAlign.LEFT, 2 * char_width) + self.text_view.set_tabs(tab_array) + + # Adjust baseline margin, as character width can change + # Baseline needs to account for + self.baseline_margin = char_width * 10 + self.text_view.set_left_margin(self.baseline_margin) + self.text_view.set_right_margin(self.baseline_margin) + + # Adjust left margins, as character width can change + for level, tag in self.margins.items(): + tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0)) diff --git a/uberwriter/text_view_scroller.py b/uberwriter/text_view_scroller.py new file mode 100644 index 0000000..9679d12 --- /dev/null +++ b/uberwriter/text_view_scroller.py @@ -0,0 +1,84 @@ +from gi.repository import Gtk + + +class Scroller: + def __init__(self): + super().__init__() + + self.smooth_scroll_starttime = 0 + self.smooth_scroll_endtime = 0 + self.smooth_scroll_acttarget = 0 + self.smooth_scroll_data = { + 'target_pos': -1, + 'source_pos': -1, + 'duration': 0 + } + self.smooth_scroll_tickid = -1 + + def scroll_to(self, text_view, mark=None, center=False): + """Scrolls if needed to ensure mark is visible. + + If mark is unspecified, the cursor is used.""" + + margin = 80 + scrolled_window = text_view.get_ancestor(Gtk.ScrolledWindow.__gtype__) + va = scrolled_window.get_vadjustment() + if va.props.page_size < margin * 2: + return + + text_buffer = text_view.get_buffer() + if mark: + ins_it = text_buffer.get_iter_at_mark(mark) + else: + ins_it = text_buffer.get_iter_at_mark(text_buffer.get_insert()) + loc_rect = text_view.get_iter_location(ins_it) + + pos_y = loc_rect.y + loc_rect.height + text_view.props.top_margin + pos = pos_y - va.props.value + target_pos = -1 + if center: + 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 + self.smooth_scroll_data = { + 'target_pos': target_pos, + 'source_pos': va.props.value, + 'duration': 2000 + } + if self.smooth_scroll_tickid == -1: + self.smooth_scroll_tickid = scrolled_window.add_tick_callback(self.on_tick) + + def on_tick(self, widget, frame_clock, _data=None): + if self.smooth_scroll_data['target_pos'] == -1: + return True + + def ease_out_cubic(value): + return pow(value - 1, 3) + 1 + + now = frame_clock.get_frame_time() + if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']: + self.smooth_scroll_starttime = now + self.smooth_scroll_endtime = now + self.smooth_scroll_data['duration'] * 100 + self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos'] + + if now < self.smooth_scroll_endtime: + time = float(now - self.smooth_scroll_starttime) / float( + self.smooth_scroll_endtime - self.smooth_scroll_starttime) + else: + time = 1 + pos = self.smooth_scroll_data['source_pos'] \ + + (time * (self.smooth_scroll_data['target_pos'] + - self.smooth_scroll_data['source_pos'])) + widget.get_vadjustment().props.value = pos + self.smooth_scroll_data['target_pos'] = -1 + return True + + time = ease_out_cubic(time) + pos = self.smooth_scroll_data['source_pos'] \ + + (time * (self.smooth_scroll_data['target_pos'] + - self.smooth_scroll_data['source_pos'])) + widget.get_vadjustment().props.value = pos + return True diff --git a/uberwriter/text_view_undo_redo_handler.py b/uberwriter/text_view_undo_redo_handler.py new file mode 100644 index 0000000..7ccbd1c --- /dev/null +++ b/uberwriter/text_view_undo_redo_handler.py @@ -0,0 +1,204 @@ +class UndoableInsert: + """Something has been inserted into text_buffer""" + + def __init__(self, text_iter, text, length): + self.offset = text_iter.get_offset() + self.text = text + self.length = length + self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " ")) + + +class UndoableDelete: + """Something has been deleted from text_buffer""" + + def __init__(self, text_buffer, start_iter, end_iter): + self.text = text_buffer.get_text(start_iter, end_iter, False) + self.start = start_iter.get_offset() + self.end = end_iter.get_offset() + # Find out if backspace or delete were used to not mess up redo + insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert()) + self.delete_key_used = bool(insert_iter.get_offset() <= self.start) + self.mergeable = not bool(self.end - self.start > 1 or self.text in ("\r", "\n", " ")) + + +class UndoRedoHandler: + """Manages undo/redo for a given text_buffer. + + Methods can be called directly, as well as be used as signal callbacks.""" + + def __init__(self): + self.undo_stack = [] + self.redo_stack = [] + self.not_undoable_action = False + self.undo_in_progress = False + + def undo(self, text_view, _data=None): + """Undo insertions or deletions. Undone actions are moved to redo stack. + + This method can be registered to a custom undo signal, or used independently.""" + + if not self.undo_stack: + return + self.__begin_not_undoable_action() + self.undo_in_progress = True + undo_action = self.undo_stack.pop() + self.redo_stack.append(undo_action) + text_buffer = text_view.get_buffer() + if isinstance(undo_action, UndoableInsert): + offset = undo_action.offset + start = text_buffer.get_iter_at_offset(offset) + stop = text_buffer.get_iter_at_offset( + offset + undo_action.length + ) + text_buffer.place_cursor(start) + text_buffer.delete(start, stop) + else: + start = text_buffer.get_iter_at_offset(undo_action.start) + text_buffer.insert(start, undo_action.text) + if undo_action.delete_key_used: + text_buffer.place_cursor(start) + else: + stop = text_buffer.get_iter_at_offset(undo_action.end) + text_buffer.place_cursor(stop) + self.__end_not_undoable_action() + self.undo_in_progress = False + + def redo(self, text_view, _data): + """Redo insertions or deletions. Redone actions are moved to undo stack + + This method can be registered to a custom redo signal, or used independently.""" + + if not self.redo_stack: + return + self.__begin_not_undoable_action() + self.undo_in_progress = True + redo_action = self.redo_stack.pop() + self.undo_stack.append(redo_action) + text_buffer = text_view.get_buffer() + if isinstance(redo_action, UndoableInsert): + start = text_buffer.get_iter_at_offset(redo_action.offset) + text_buffer.insert(start, redo_action.text) + new_cursor_pos = text_buffer.get_iter_at_offset( + redo_action.offset + redo_action.length) + text_buffer.place_cursor(new_cursor_pos) + else: + start = text_buffer.get_iter_at_offset(redo_action.start) + stop = text_buffer.get_iter_at_offset(redo_action.end) + text_buffer.delete(start, stop) + text_buffer.place_cursor(start) + self.__end_not_undoable_action() + self.undo_in_progress = False + + def clear(self): + self.undo_stack = [] + self.redo_stack = [] + + def on_insert_text(self, _text_buffer, text_iter, text, _length): + """Registers a text insert. Refer to TextBuffer's "insert-text" signal. + + This method must be registered to TextBuffer's "insert-text" signal, or called manually.""" + + def can_be_merged(prev, cur): + """Check if multiple insertions can be merged + + can't merge if prev and cur are not mergeable in the first place + can't merge when user set the input bar somewhere else + can't merge across word boundaries""" + + whitespace = (' ', '\t') + if not cur.mergeable or not prev.mergeable: + return False + if cur.offset != (prev.offset + prev.length): + return False + if cur.text in whitespace and prev.text not in whitespace: + return False + if prev.text in whitespace and cur.text not in whitespace: + return False + return True + + if not self.undo_in_progress: + self.redo_stack = [] + if self.not_undoable_action: + return + + undo_action = UndoableInsert(text_iter, text, len(text)) + try: + prev_insert = self.undo_stack.pop() + except IndexError: + self.undo_stack.append(undo_action) + return + if not isinstance(prev_insert, UndoableInsert): + self.undo_stack.append(prev_insert) + self.undo_stack.append(undo_action) + return + if can_be_merged(prev_insert, undo_action): + prev_insert.length += undo_action.length + prev_insert.text += undo_action.text + self.undo_stack.append(prev_insert) + else: + self.undo_stack.append(prev_insert) + self.undo_stack.append(undo_action) + + def on_delete_range(self, text_buffer, start_iter, end_iter): + """Registers a range deletion. Refer to TextBuffer's "delete-range" signal. + + This method must be registered to TextBuffer's "delete-range" signal, or called manually.""" + + def can_be_merged(prev, cur): + """Check if multiple deletions can be merged + + can't merge if prev and cur are not mergeable in the first place + can't merge if delete and backspace key were both used + can't merge across word boundaries""" + + whitespace = (' ', '\t') + if not cur.mergeable or not prev.mergeable: + return False + if prev.delete_key_used != cur.delete_key_used: + return False + if prev.start != cur.start and prev.start != cur.end: + return False + if cur.text not in whitespace and \ + prev.text in whitespace: + return False + if cur.text in whitespace and \ + prev.text not in whitespace: + return False + return True + + if not self.undo_in_progress: + self.redo_stack = [] + if self.not_undoable_action: + return + undo_action = UndoableDelete(text_buffer, start_iter, end_iter) + try: + prev_delete = self.undo_stack.pop() + except IndexError: + self.undo_stack.append(undo_action) + return + if not isinstance(prev_delete, UndoableDelete): + self.undo_stack.append(prev_delete) + self.undo_stack.append(undo_action) + return + if can_be_merged(prev_delete, undo_action): + if prev_delete.start == undo_action.start: # delete key used + prev_delete.text += undo_action.text + prev_delete.end += (undo_action.end - undo_action.start) + else: # Backspace used + prev_delete.text = "%s%s" % (undo_action.text, + prev_delete.text) + prev_delete.start = undo_action.start + self.undo_stack.append(prev_delete) + else: + self.undo_stack.append(prev_delete) + self.undo_stack.append(undo_action) + + def __begin_not_undoable_action(self): + """Toggle to stop recording actions""" + + self.not_undoable_action = True + + def __end_not_undoable_action(self): + """Toggle to start recording actions""" + + self.not_undoable_action = False diff --git a/uberwriter/window.py b/uberwriter/window.py index dcdbcd9..432f13d 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -17,7 +17,6 @@ import codecs import locale import logging -import mimetypes import os import re import subprocess @@ -27,14 +26,13 @@ from gettext import gettext as _ import gi -from uberwriter.export_dialog import UberwriterExportDialog -from uberwriter.text_editor import UberwriterTextEditor +from uberwriter.export_dialog import Export +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 -from gi.repository import Pango # pylint: disable=E0611 import cairo @@ -43,15 +41,12 @@ from uberwriter.theme import Theme from uberwriter.helpers import get_builder from uberwriter.gtkspellcheck import SpellChecker -from uberwriter.markup_buffer import MarkupBuffer -from uberwriter.inline_preview import InlinePreview from uberwriter.sidebar import Sidebar from uberwriter.search_and_replace import SearchAndReplace from uberwriter.settings import Settings from . import headerbars - # Some Globals # TODO move them somewhere for better # accesibility from other files @@ -60,8 +55,6 @@ LOGGER = logging.getLogger('uberwriter') CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/") -# See texteditor_lib.Window.py for more details about how this class works - class Window(Gtk.ApplicationWindow): __gsignals__ = { @@ -85,15 +78,15 @@ class Window(Gtk.ApplicationWindow): # Set UI self.builder = get_builder('UberwriterWindow') - self.add(self.builder.get_object("FullscreenOverlay")) + root = self.builder.get_object("FullscreenOverlay") + root.connect('style-updated', self.apply_current_theme) + self.add(root) - self.set_default_size(850, 500) + self.set_default_size(900, 500) - # preferences + # Preferences self.settings = Settings.new() - self.set_name('UberwriterWindow') - # Headerbars self.headerbar = headerbars.MainHeaderbar(app) self.set_titlebar(self.headerbar.hb_container) @@ -102,20 +95,18 @@ class Window(Gtk.ApplicationWindow): self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) - self.focusmode = False - self.word_count = self.builder.get_object('word_count') self.char_count = self.builder.get_object('char_count') # Setup status bar hide after 3 seconds - self.status_bar = self.builder.get_object('status_bar_box') self.statusbar_revealer = self.builder.get_object('status_bar_revealer') - self.status_bar.get_style_context().add_class('status_bar_box') + self.status_bar.get_style_context().add_class('status-bar-box') self.status_bar_visible = True self.was_motion = True self.buffer_modified_for_status_bar = False + self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) GObject.timeout_add(3000, self.poll_for_motion) @@ -124,43 +115,20 @@ class Window(Gtk.ApplicationWindow): self.add_accel_group(self.accel_group) # Setup text editor - self.text_editor = UberwriterTextEditor() - self.text_editor.set_name('UberwriterEditor') - self.get_style_context().add_class('uberwriter_window') + self.text_view = TextView() + self.text_view.props.halign = Gtk.Align.CENTER + self.text_view.connect('focus-out-event', self.focus_out) + self.text_view.show() + self.text_view.grab_focus() - base_leftmargin = 100 - self.text_editor.set_left_margin(base_leftmargin) - self.text_editor.set_left_margin(40) - self.text_editor.set_top_margin(80) - self.text_editor.props.width_request = 600 - self.text_editor.props.halign = Gtk.Align.CENTER - self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1')) - self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) - self.text_editor.connect('focus-out-event', self.focus_out) - self.text_editor.connect('style-updated', self.apply_current_theme) - - self.text_editor.set_top_margin(80) - self.text_editor.set_bottom_margin(16) - - self.text_editor.set_pixels_above_lines(4) - self.text_editor.set_pixels_below_lines(4) - self.text_editor.set_pixels_inside_wrap(8) - - tab_array = Pango.TabArray.new(1, True) - tab_array.set_tab(0, Pango.TabAlign.LEFT, 20) - self.text_editor.set_tabs(tab_array) - - self.text_editor.show() - self.text_editor.grab_focus() + self.text_view.get_buffer().connect('changed', self.on_text_changed) # Setup preview webview self.preview_webview = None - self.editor_alignment = self.builder.get_object('editor_alignment') self.scrolled_window = self.builder.get_object('editor_scrolledwindow') - self.scrolled_window.props.width_request = 600 - self.scrolled_window.add(self.text_editor) - self.alignment_padding = 40 + 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') # some people seems to have performance problems with the overlay. @@ -169,68 +137,9 @@ class Window(Gtk.ApplicationWindow): if self.settings.get_value("gradient-overlay"): self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient) - self.smooth_scroll_starttime = 0 - self.smooth_scroll_endtime = 0 - self.smooth_scroll_acttarget = 0 - self.smooth_scroll_data = { - 'target_pos': -1, - 'source_pos': -1, - 'duration': 0 - } - self.smooth_scroll_tickid = -1 - - self.text_buffer = self.text_editor.get_buffer() - self.text_buffer.set_text('') - - # Init Window height for top/bottom padding - self.window_height = self.get_size()[1] - - self.text_change_event = self.text_buffer.connect( - 'changed', self.text_changed) - # Init file name with None self.set_filename() - # Markup and Shortcuts for the TextBuffer - self.markup_buffer = MarkupBuffer( - self, self.text_editor, base_leftmargin) - self.markup_buffer.markup_buffer() - - # Scrolling -> Dark or not? - self.textchange = False - self.scroll_count = 0 - self.timestamp_last_mouse_motion = 0 - self.text_buffer.connect_after('mark-set', self.mark_set) - - # Drag and drop - - # self.TextEditor.drag_dest_unset() - # self.TextEditor.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) - self.target_list = Gtk.TargetList.new([]) - self.target_list.add_uri_targets(1) - self.target_list.add_text_targets(2) - - self.text_editor.drag_dest_set_target_list(self.target_list) - self.text_editor.connect_after( - 'drag-data-received', self.on_drag_data_received) - - def on_drop(_widget, *_args): - print("drop") - self.text_editor.connect('drag-drop', on_drop) - - self.text_buffer.connect('paste-done', self.paste_done) - # self.connect('key-press-event', self.alt_mod) - - # Events for Typewriter mode - - # Setting up inline preview - self.inline_preview = InlinePreview( - self.text_editor, self.text_buffer) - - # Vertical scrolling - self.vadjustment = self.scrolled_window.get_vadjustment() - self.vadjustment.connect('value-changed', self.scrolled) - # Setting up spellcheck self.auto_correct = None self.toggle_spellcheck(self.settings.get_value("spellcheck")) @@ -248,7 +157,7 @@ class Window(Gtk.ApplicationWindow): # Search and replace initialization # Same interface as Sidebar ;) ### - self.searchreplace = SearchAndReplace(self) + self.searchreplace = SearchAndReplace(self, self.text_view) # Window resize self.window_resize(self) @@ -257,6 +166,7 @@ class Window(Gtk.ApplicationWindow): # 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. @@ -284,58 +194,14 @@ class Window(Gtk.ApplicationWindow): # Redraw contents of window self.queue_draw() - def scrolled(self, widget): - """if window scrolled + focusmode make font black again""" - # if self.focusmode: - # if self.textchange == False: - # if self.scroll_count >= 4: - # self.TextBuffer.apply_tag( - # self.MarkupBuffer.blackfont, - # self.TextBuffer.get_start_iter(), - # self.TextBuffer.get_end_iter()) - # else: - # self.scroll_count += 1 - # else: - # self.scroll_count = 0 - # self.textchange = False - - def paste_done(self, *_): - self.markup_buffer.markup_buffer(0) - - def init_typewriter(self): - """put the cursor at the center of the screen by setting top and - bottom margins to height/2 - """ - - editor_height = self.text_editor.get_allocation().height - self.text_editor.props.top_margin = editor_height / 2 - self.text_editor.props.bottom_margin = editor_height / 2 - - def remove_typewriter(self): - """set margins to default values - """ - - self.text_editor.props.top_margin = 80 - self.text_editor.props.bottom_margin = 16 - self.text_change_event = self.text_buffer.connect( - 'changed', self.text_changed) - - def get_text(self): - """get text from self.text_buffer - """ - - start_iter = self.text_buffer.get_start_iter() - end_iter = self.text_buffer.get_end_iter() - return self.text_buffer.get_text(start_iter, end_iter, False) - def update_line_and_char_count(self): """it... it updates line and characters count """ if self.status_bar_visible is False: return - self.char_count.set_text(str(self.text_buffer.get_char_count())) - text = self.get_text() + text = self.text_view.get_text() + self.char_count.set_text(str(len(text))) words = re.split(self.WORDCOUNT, text) length = len(words) # Last word a "space" @@ -348,12 +214,7 @@ class Window(Gtk.ApplicationWindow): length = 0 self.word_count.set_text(str(length)) - def mark_set(self, _buffer, _location, mark, _data=None): - if mark.get_name() in ['insert', 'gtk_drag_target']: - self.check_scroll(mark) - return True - - def text_changed(self, *_args): + 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 """ @@ -363,12 +224,8 @@ class Window(Gtk.ApplicationWindow): title = self.get_title() self.set_headerbar_title("* " + title) - self.markup_buffer.markup_buffer(1) - self.textchange = True - self.buffer_modified_for_status_bar = True self.update_line_and_char_count() - self.check_scroll(self.text_buffer.get_insert()) def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides @@ -386,178 +243,52 @@ class Window(Gtk.ApplicationWindow): self.unfullscreen() self.fs_headerbar.events.hide() - self.text_editor.grab_focus() + self.text_view.grab_focus() def set_focus_mode(self, state): """toggle focusmode """ - if state.get_boolean(): - self.init_typewriter() - self.markup_buffer.focusmode_highlight() - self.focusmode = True - self.text_editor.grab_focus() - self.check_scroll(self.text_buffer.get_insert()) - if self.spell_checker: - self.spell_checker._misspelled.set_property('underline', 0) - self.click_event = self.text_editor.connect("button-release-event", - self.on_focusmode_click) - else: - self.remove_typewriter() - self.focusmode = False - self.text_buffer.remove_tag(self.markup_buffer.unfocused_text, - self.text_buffer.get_start_iter(), - self.text_buffer.get_end_iter()) - self.text_buffer.remove_tag(self.markup_buffer.blackfont, - self.text_buffer.get_start_iter(), - self.text_buffer.get_end_iter()) - - self.markup_buffer.markup_buffer(1) - self.text_editor.grab_focus() - self.update_line_and_char_count() - self.check_scroll() - if self.spell_checker: - self.spell_checker._misspelled.set_property('underline', 4) - _click_event = self.text_editor.disconnect(self.click_event) + focus_mode = state.get_boolean() + self.text_view.set_focus_mode(focus_mode) + if self.spell_checker: + self.spell_checker._misspelled.set_property('underline', 0 if focus_mode else 4) + self.text_view.grab_focus() def set_hemingway_mode(self, state): """toggle hemingwaymode """ - self.text_editor.can_delete = not state.get_boolean() - self.text_editor.grab_focus() - def on_focusmode_click(self, *_args): - """call MarkupBuffer to mark as bold the line where the cursor is - """ + self.text_view.set_hemingway_mode(state.get_boolean()) + self.text_view.grab_focus() - self.markup_buffer.markup_buffer(1) - - def scroll_smoothly(self, widget, frame_clock, _data=None): - if self.smooth_scroll_data['target_pos'] == -1: - return True - - def ease_out_cubic(time): - time = time - 1 - return pow(time, 3) + 1 - - now = frame_clock.get_frame_time() - if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']: - self.smooth_scroll_starttime = now - self.smooth_scroll_endtime = now + \ - self.smooth_scroll_data['duration'] * 100 - self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos'] - - if now < self.smooth_scroll_endtime: - time = float(now - self.smooth_scroll_starttime) / float( - self.smooth_scroll_endtime - self.smooth_scroll_starttime) - else: - time = 1 - pos = self.smooth_scroll_data['source_pos'] \ - + (time * (self.smooth_scroll_data['target_pos'] - - self.smooth_scroll_data['source_pos'])) - widget.get_vadjustment().props.value = pos - self.smooth_scroll_data['target_pos'] = -1 - return True - - time = ease_out_cubic(time) - pos = self.smooth_scroll_data['source_pos'] \ - + (time * (self.smooth_scroll_data['target_pos'] - - self.smooth_scroll_data['source_pos'])) - widget.get_vadjustment().props.value = pos - return True # continue ticking - - def check_scroll(self, mark=None): - gradient_offset = 80 - buf = self.text_editor.get_buffer() - if mark: - ins_it = buf.get_iter_at_mark(mark) - else: - ins_it = buf.get_iter_at_mark(buf.get_insert()) - loc_rect = self.text_editor.get_iter_location(ins_it) - - # alignment offset added from top - pos_y = loc_rect.y + loc_rect.height + self.text_editor.props.top_margin # pylint: disable=no-member - - ha = self.scrolled_window.get_vadjustment() - if ha.props.page_size < gradient_offset: - return - pos = pos_y - ha.props.value - # print("pos: %i, pos_y %i, page_sz: %i, val: %i" % (pos, pos_y, ha.props.page_size - # - gradient_offset, ha.props.value)) - # global t, amount, initvadjustment - target_pos = -1 - if self.focusmode: - # print("pos: %i > %i" % (pos, ha.props.page_size * 0.5)) - if pos != (ha.props.page_size * 0.5): - target_pos = pos_y - (ha.props.page_size * 0.5) - elif pos > ha.props.page_size - gradient_offset - 60: - target_pos = pos_y - ha.props.page_size + gradient_offset + 40 - elif pos < gradient_offset: - target_pos = pos_y - gradient_offset - self.smooth_scroll_data = { - 'target_pos': target_pos, - 'source_pos': ha.props.value, - 'duration': 2000 - } - if self.smooth_scroll_tickid == -1: - self.smooth_scroll_tickid = self.scrolled_window.add_tick_callback( - self.scroll_smoothly) - - def window_resize(self, widget, _data=None): + def window_resize(self, window, _data=None): """set paddings dependant of the window size """ - # To calc padding top / bottom - self.window_height = widget.get_allocation().height - w_width = widget.get_allocation().width - # Calculate left / right margin + # 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 + w_width = window.get_allocation().width if w_width < 900: - width_request = 600 - self.markup_buffer.set_multiplier(8) - self.current_font_size = 12 - self.alignment_padding = 30 - lm = 7 * 8 - self.get_style_context().remove_class("medium") - self.get_style_context().remove_class("large") + width_request = 700 # ~66 characters self.get_style_context().add_class("small") + self.get_style_context().remove_class("large") - elif w_width < 1400: - width_request = 800 - self.markup_buffer.set_multiplier(10) - self.current_font_size = 15 - self.alignment_padding = 40 - lm = 7 * 10 + elif w_width < 1200: + width_request = 870 # ~66 characters self.get_style_context().remove_class("small") self.get_style_context().remove_class("large") - self.get_style_context().add_class("medium") else: - width_request = 1000 - self.markup_buffer.set_multiplier(13) - self.current_font_size = 17 - self.alignment_padding = 60 - lm = 7 * 13 - self.get_style_context().remove_class("medium") + width_request = 830 # ~66 characters self.get_style_context().remove_class("small") self.get_style_context().add_class("large") - self.editor_alignment.props.margin_bottom = 0 - self.editor_alignment.props.margin_top = 0 - self.text_editor.set_left_margin(lm) - self.text_editor.set_right_margin(lm) - - self.markup_buffer.recalculate(lm) - - if self.focusmode: - self.remove_typewriter() - self.init_typewriter() - - if self.text_editor.props.width_request != width_request: # pylint: disable=no-member - self.text_editor.props.width_request = width_request + if self.text_view.props.width_request != width_request: + self.text_view.props.width_request = width_request self.scrolled_window.props.width_request = width_request - alloc = self.text_editor.get_allocation() - alloc.width = width_request - self.text_editor.size_allocate(alloc) # TODO: refactorizable def save_document(self, _widget=None, _data=None): @@ -569,7 +300,7 @@ class Window(Gtk.ApplicationWindow): LOGGER.info("saving") filename = self.filename file_to_save = codecs.open(filename, encoding="utf-8", mode='w') - file_to_save.write(self.get_text()) + file_to_save.write(self.text_view.get_text()) file_to_save.close() if self.did_change: self.did_change = False @@ -603,7 +334,7 @@ class Window(Gtk.ApplicationWindow): pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') - file_to_save.write(self.get_text()) + file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) @@ -644,7 +375,7 @@ class Window(Gtk.ApplicationWindow): pass file_to_save = codecs.open(filename, encoding="utf-8", mode='w') - file_to_save.write(self.get_text()) + file_to_save.write(self.text_view.get_text()) file_to_save.close() self.set_filename(filename) @@ -671,7 +402,7 @@ class Window(Gtk.ApplicationWindow): proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - text = bytes(self.get_text(), "utf-8") + text = bytes(self.text_view.get_text(), "utf-8") output = proc.communicate(text)[0] clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) @@ -711,14 +442,14 @@ class Window(Gtk.ApplicationWindow): """Show dialog to prevent loss of unsaved changes """ - if self.did_change and self.get_text(): + if self.did_change and self.text_view.get_text(): dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("You have not saved your changes.") ) - dialog.add_button(_("Close without Saving"), Gtk.ResponseType.NO) + dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO) dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(_("Save now"), Gtk.ResponseType.YES) # dialog.set_default_size(200, 60) @@ -745,9 +476,7 @@ class Window(Gtk.ApplicationWindow): if self.check_change() == Gtk.ResponseType.CANCEL: return - self.text_buffer.set_text('') - self.text_editor.undos = [] - self.text_editor.redos = [] + self.text_view.clear() self.did_change = False self.set_filename() @@ -771,7 +500,7 @@ class Window(Gtk.ApplicationWindow): except: try: self.spell_checker = SpellChecker( - self.text_editor, locale.getdefaultlocale()[0], + self.text_view, locale.getdefaultlocale()[0], collapse=False) if self.auto_correct: self.auto_correct.set_language(self.spell_checker.language) @@ -787,7 +516,7 @@ class Window(Gtk.ApplicationWindow): _("You can not enable the Spell Checker.") ) dialog.format_secondary_text( - _("Please install 'hunspell' or 'aspell' dictionarys" + _("Please install 'hunspell' or 'aspell' dictionaries" + " for your language from the software center.")) _response = dialog.run() return @@ -799,46 +528,6 @@ class Window(Gtk.ApplicationWindow): pass return - def on_drag_data_received(self, _widget, drag_context, _x, _y, - data, info, time): - """Handle drag and drop events""" - if info == 1: - # uri target - uris = data.get_uris() - for uri in uris: - uri = urllib.parse.unquote_plus(uri) - mime = mimetypes.guess_type(uri) - - if mime[0] is not None and mime[0].startswith('image'): - if uri.startswith("file://"): - uri = uri[7:] - text = "![Insert image title here](%s)" % uri - limit_left = 2 - limit_right = 23 - else: - text = "[Insert link title here](%s)" % uri - limit_left = 1 - limit_right = 22 - self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark( - self.text_buffer.get_mark('gtk_drag_target'))) - self.text_buffer.insert_at_cursor(text) - insert_mark = self.text_buffer.get_insert() - selection_bound = self.text_buffer.get_selection_bound() - cursor_iter = self.text_buffer.get_iter_at_mark(insert_mark) - cursor_iter.backward_chars(len(text) - limit_left) - self.text_buffer.move_mark(insert_mark, cursor_iter) - cursor_iter.forward_chars(limit_right) - self.text_buffer.move_mark(selection_bound, cursor_iter) - - elif info == 2: - # Text target - self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark( - self.text_buffer.get_mark('gtk_drag_target'))) - self.text_buffer.insert_at_cursor(data.get_text()) - Gtk.drag_finish(drag_context, True, True, time) - self.present() - return False - def toggle_preview(self, state): """Toggle the preview mode @@ -855,8 +544,8 @@ class Window(Gtk.ApplicationWindow): def show_text_editor(self): self.scrolled_window.remove(self.scrolled_window.get_child()) - self.scrolled_window.add(self.text_editor) - self.text_editor.show() + self.scrolled_window.add(self.text_view) + self.text_view.show() self.preview_webview.destroy() self.preview_webview = None self.queue_draw() @@ -868,12 +557,6 @@ class Window(Gtk.ApplicationWindow): self.preview_webview.show() self.queue_draw() else: - # Insert a tag with ID to scroll to - # self.TextBuffer.insert_at_cursor('') - # TODO - # Find a way to find the next header, scroll to the next header. - # TODO: provide a local version of mathjax - # We need to convert relative routes to absolute ones # For that first we need to know if the file is saved: if self.filename: @@ -899,19 +582,13 @@ class Window(Gtk.ApplicationWindow): proc = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - text = bytes(self.get_text(), "utf-8") + text = bytes(self.text_view.get_text(), "utf-8") output = proc.communicate(text)[0] if self.preview_webview is None: self.preview_webview = WebKit.WebView() self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True) - # Delete the cursor-scroll mark again - # cursor_iter = self.TextBuffer.get_iter_at_mark(self.TextBuffer.get_insert()) - # begin_del = cursor_iter.copy() - # begin_del.backward_chars(30) - # self.TextBuffer.delete(begin_del, cursor_iter) - # Show preview once the load is finished self.preview_webview.connect("load-changed", self.on_preview_load_change) @@ -930,19 +607,14 @@ class Window(Gtk.ApplicationWindow): if filename.startswith('file://'): filename = filename[7:] filename = urllib.parse.unquote_plus(filename) + self.text_view.clear() try: - if not os.path.exists(filename): - self.text_buffer.set_text("") - else: + if os.path.exists(filename): current_file = codecs.open(filename, encoding="utf-8", mode='r') - self.text_buffer.set_text(current_file.read()) + self.text_view.set_text(current_file.read()) current_file.close() - self.markup_buffer.markup_buffer(0) - self.set_headerbar_title( - os.path.basename(filename) + self.title_end) - self.text_editor.undo_stack = [] - self.text_editor.redo_stack = [] + self.set_headerbar_title(os.path.basename(filename) + self.title_end) self.set_filename(filename) except Exception: @@ -969,12 +641,12 @@ class Window(Gtk.ApplicationWindow): """open the export and advanced export dialog """ - self.export = UberwriterExportDialog(self.filename) + self.export = Export(self.filename) self.export.dialog.set_transient_for(self) response = self.export.dialog.run() if response == 1: - self.export.export(bytes(self.get_text(), "utf-8")) + self.export.export(bytes(self.text_view.get_text(), "utf-8")) self.export.dialog.destroy() @@ -998,7 +670,7 @@ class Window(Gtk.ApplicationWindow): if (self.was_motion is False and self.status_bar_visible and self.buffer_modified_for_status_bar - and self.text_editor.props.has_focus): # pylint: disable=no-member + and self.text_view.props.has_focus): # pylint: disable=no-member # self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True) self.statusbar_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) @@ -1071,24 +743,6 @@ class Window(Gtk.ApplicationWindow): cr.set_source(lg_btm) cr.fill() - def use_experimental_features(self, _val): - """use experimental features - """ - pass - # try: - # self.auto_correct = AutoCorrect( - # self.text_editor, self.text_buffer) - # except: - # LOGGER.debug("Couldn't install autocorrect.") - - # self.plugins = [BibTex(self)] - - # def alt_mod(self, _widget, event, _data=None): - # # TODO: Click and open when alt is pressed - # if event.state & Gdk.ModifierType.MOD2_MASK: - # LOGGER.info("Alt pressed") - # return - def on_delete_called(self, _widget, _data=None): """Called when the TexteditorWindow is closed. """ From 372d2c8a6536c07aae9fdeb049cc037810d6f589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 31 Mar 2019 03:50:34 +0100 Subject: [PATCH 06/54] Remove tick callback after scrolling is done Adding a tick callback starts updating the frame clock: https://gitlab.gnome.org/GNOME/gtk/blob/d1cdb9b5cfbd68db7f370b0c64195269384d9190/gtk/gtkwidget.c#L3629 Gdk's documentation implies that this is taxing, as it continuously requests a new frame (which makes sense, this API is for animations): https://developer.gnome.org/gdk3/stable/GdkFrameClock.html#gdk-frame-clock-begin-updating Most likely fixes #75 --- uberwriter/scroller.py | 48 ++++++++++++++++++ uberwriter/text_view.py | 37 ++++++++++++-- uberwriter/text_view_scroller.py | 84 -------------------------------- 3 files changed, 80 insertions(+), 89 deletions(-) create mode 100644 uberwriter/scroller.py delete mode 100644 uberwriter/text_view_scroller.py diff --git a/uberwriter/scroller.py b/uberwriter/scroller.py new file mode 100644 index 0000000..c95773e --- /dev/null +++ b/uberwriter/scroller.py @@ -0,0 +1,48 @@ +class Scroller: + def __init__(self, scrolled_window, target_pos, source_pos, duration=2000): + super().__init__() + + self.scrolled_window = scrolled_window + self.target_pos = target_pos + self.source_pos = source_pos + self.duration = duration + + self.is_started = False + self.start_time = 0 + self.end_time = 0 + + self.tick_callback_id = 0 + + def start(self): + self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick, self) + + def end(self): + self.is_started = False + self.scrolled_window.remove_tick_callback(self.tick_callback_id) + + def do_start(self, time): + self.is_started = True + self.start_time = time + self.end_time = time + self.duration * 100 + + @staticmethod + def on_tick(widget, frame_clock, scroller): + def ease_out_cubic(value): + return pow(value - 1, 3) + 1 + + now = frame_clock.get_frame_time() + if not scroller.is_started: + scroller.do_start(now) + + if now < scroller.end_time: + time = float(now - scroller.start_time) / float(scroller.end_time - scroller.start_time) + else: + time = 1 + scroller.end() + + time = ease_out_cubic(time) + pos = scroller.source_pos + (time * (scroller.target_pos - scroller.source_pos)) + widget.get_vadjustment().props.value = pos + return True + + diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index d580b78..f25fa3f 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -4,9 +4,8 @@ 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_undo_redo_handler import UndoRedoHandler -from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, \ - TARGET_TEXT -from uberwriter.text_view_scroller import Scroller +from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT +from uberwriter.scroller import Scroller gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GObject @@ -81,7 +80,7 @@ class TextView(Gtk.TextView): self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT) # Scrolling - self.scroller = Scroller() + self.scroller = None self.get_buffer().connect('mark-set', self.on_mark_set) # Focus mode @@ -165,7 +164,35 @@ class TextView(Gtk.TextView): If mark is unspecified, the cursor is used.""" - self.scroller.scroll_to(self, mark, self.focus_mode) + margin = 80 + scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__) + va = scrolled_window.get_vadjustment() + if va.props.page_size < margin * 2: + return + + 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) + + 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 self.scroller and self.scroller.is_started: + self.scroller.end() + if target_pos: + self.scroller = Scroller(scrolled_window, target_pos, va.props.value) + self.scroller.start() def on_mark_set(self, _text_buffer, _location, mark, _data=None): if mark.get_name() == 'insert': diff --git a/uberwriter/text_view_scroller.py b/uberwriter/text_view_scroller.py deleted file mode 100644 index 9679d12..0000000 --- a/uberwriter/text_view_scroller.py +++ /dev/null @@ -1,84 +0,0 @@ -from gi.repository import Gtk - - -class Scroller: - def __init__(self): - super().__init__() - - self.smooth_scroll_starttime = 0 - self.smooth_scroll_endtime = 0 - self.smooth_scroll_acttarget = 0 - self.smooth_scroll_data = { - 'target_pos': -1, - 'source_pos': -1, - 'duration': 0 - } - self.smooth_scroll_tickid = -1 - - def scroll_to(self, text_view, mark=None, center=False): - """Scrolls if needed to ensure mark is visible. - - If mark is unspecified, the cursor is used.""" - - margin = 80 - scrolled_window = text_view.get_ancestor(Gtk.ScrolledWindow.__gtype__) - va = scrolled_window.get_vadjustment() - if va.props.page_size < margin * 2: - return - - text_buffer = text_view.get_buffer() - if mark: - ins_it = text_buffer.get_iter_at_mark(mark) - else: - ins_it = text_buffer.get_iter_at_mark(text_buffer.get_insert()) - loc_rect = text_view.get_iter_location(ins_it) - - pos_y = loc_rect.y + loc_rect.height + text_view.props.top_margin - pos = pos_y - va.props.value - target_pos = -1 - if center: - 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 - self.smooth_scroll_data = { - 'target_pos': target_pos, - 'source_pos': va.props.value, - 'duration': 2000 - } - if self.smooth_scroll_tickid == -1: - self.smooth_scroll_tickid = scrolled_window.add_tick_callback(self.on_tick) - - def on_tick(self, widget, frame_clock, _data=None): - if self.smooth_scroll_data['target_pos'] == -1: - return True - - def ease_out_cubic(value): - return pow(value - 1, 3) + 1 - - now = frame_clock.get_frame_time() - if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']: - self.smooth_scroll_starttime = now - self.smooth_scroll_endtime = now + self.smooth_scroll_data['duration'] * 100 - self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos'] - - if now < self.smooth_scroll_endtime: - time = float(now - self.smooth_scroll_starttime) / float( - self.smooth_scroll_endtime - self.smooth_scroll_starttime) - else: - time = 1 - pos = self.smooth_scroll_data['source_pos'] \ - + (time * (self.smooth_scroll_data['target_pos'] - - self.smooth_scroll_data['source_pos'])) - widget.get_vadjustment().props.value = pos - self.smooth_scroll_data['target_pos'] = -1 - return True - - time = ease_out_cubic(time) - pos = self.smooth_scroll_data['source_pos'] \ - + (time * (self.smooth_scroll_data['target_pos'] - - self.smooth_scroll_data['source_pos'])) - widget.get_vadjustment().props.value = pos - return True From c5d2322b96d6a9e432563f68d72fefcfe006b034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 31 Mar 2019 03:54:44 +0100 Subject: [PATCH 07/54] Fix cursor position changes not scrolling in focus mode --- uberwriter/text_view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index f25fa3f..6759fd6 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -196,7 +196,11 @@ class TextView(Gtk.TextView): def on_mark_set(self, _text_buffer, _location, mark, _data=None): if mark.get_name() == 'insert': - self.markup.apply(not self.focus_mode) + if self.focus_mode: + self.scroll_to(mark) + self.markup.apply(False) + else: + self.markup.apply(True) elif mark.get_name() == 'gtk_drag_target': self.scroll_to(mark) return True From 86c924972b27ad5885a4ae7946b99048e8069160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 10 Apr 2019 02:59:00 +0100 Subject: [PATCH 08/54] Add support for code blocks, improve overall markup handling This commit adds markup support for code blocks, styling them in a conservative manner, similar to blockquotes, solely indenting them. Partially fixes #90 Code-wise, this means marking up around the cursor becomes exponentially more complex, as a change in one line can affect multiple lines. Solving it is non-trivial, so the whole document is always marked up. Marking up the whole document is irrelevant for small to medium documents, but can incur in a performance penalty for very large documents (empirical testing: 1M characters takes ~0.15s). To alleviate this, GLib.idle_add is used to ensure that markup is only parsed and applied when the UI is idle. Again, small to medium-sized documents see no difference. For very large documents, markup will be slightly delayed to allow for a fluid typing experience. It's important to note that the previous flows frequently used full document markup: paste, focus mode, and search and replace. In some extreme cases, doubly parsing (eg. paste + text change). For very large documents, doing any of these actions would freeze the UI unconditionally, so in more ways than one this is an upgrade. Lastly, it's a little overzealous: with over 1M characters the UI itself struggles more than parsing. In sum: * Markup is always applied to the whole document * The code is simpler * There is never double work * Markup is applied when the UI is idle, for a more smooth experience * Multi-line formatting is now possible to do reliably --- uberwriter/text_view.py | 15 +-- uberwriter/text_view_markup_handler.py | 167 ++++++++++++++++--------- 2 files changed, 115 insertions(+), 67 deletions(-) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 6759fd6..3e59378 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -71,7 +71,6 @@ class TextView(Gtk.TextView): # Markup self.markup = MarkupHandler(self) self.connect('style-updated', self.markup.on_style_updated) - self.get_buffer().connect('paste-done', self.on_paste_done) # Preview popover self.preview_popover = InlinePreview(self) @@ -102,18 +101,14 @@ class TextView(Gtk.TextView): def set_text(self, text): text_buffer = self.get_buffer() text_buffer.set_text(text) - self.markup.apply() # TODO def on_text_changed(self, *_): - self.markup.apply(True) - self.scroll_to() - - def on_paste_done(self, *_): self.markup.apply() + self.scroll_to() def on_size_allocate(self, *_): self.update_vertical_margin() - self.markup.update_margins() + self.markup.update_margins_indents() def set_focus_mode(self, focus_mode): """Toggle focus mode. @@ -137,7 +132,7 @@ class TextView(Gtk.TextView): def on_button_release_event(self, _widget, _event): if self.focus_mode: - self.markup.apply(True) + self.markup.apply() return False def set_hemingway_mode(self, hemingway_mode): @@ -196,11 +191,9 @@ class TextView(Gtk.TextView): 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) - self.markup.apply(False) - else: - self.markup.apply(True) elif mark.get_name() == 'gtk_drag_target': self.scroll_to(mark) return True diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py index ed1d9da..e4d8dcf 100644 --- a/uberwriter/text_view_markup_handler.py +++ b/uberwriter/text_view_markup_handler.py @@ -15,7 +15,9 @@ ### END LICENSE import re + import gi +from gi.overrides import GLib from uberwriter import helpers @@ -25,19 +27,25 @@ from gi.repository import Pango class MarkupHandler: + # Maximum number of characters for which to markup synchronously. + max_char_sync = 100000 + + # Regular expressions for various markdown constructs. regex = { - "ITALIC": re.compile(r"(\*|_)(.*?)\1"), - "BOLD": re.compile(r"(\*\*|__)(.*?)\1"), - "BOLDITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"), - "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"), - "LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-\*\+][ ].+", 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}) .+", re.MULTILINE), - "HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[\=\-]{3,}", re.MULTILINE), - "HORIZONTALRULE": re.compile(r"^\n([ ]{0,3}[\*\-_]{3,}[ ]*)\n", re.MULTILINE), - "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL), - "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"), + "ITALIC": re.compile(r"(\*|_)(.+?)\1"), + "BOLD": re.compile(r"(\*\*|__)(.+?)\1"), + "BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"), + "STRIKETHROUGH": re.compile(r"~~.+?~~"), + "HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\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), + "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}"), } def __init__(self, text_view): @@ -46,6 +54,7 @@ class MarkupHandler: # Styles buffer = self.text_buffer + self.italic = buffer.create_tag('italic', weight=Pango.Weight.NORMAL, style=Pango.Style.ITALIC) @@ -58,13 +67,16 @@ class MarkupHandler: weight=Pango.Weight.BOLD, style=Pango.Style.ITALIC) - self.graytext = buffer.create_tag('graytext', foreground='gray') - self.strikethrough = buffer.create_tag('strikethrough', strikethrough=True) - self.centertext = buffer.create_tag('centertext', justification=Gtk.Justification.CENTER) + self.horizontalrule = buffer.create_tag('centertext', + justification=Gtk.Justification.CENTER) - self.invisible = buffer.create_tag('invisible', invisible=True) + self.plaintext = buffer.create_tag('plaintext', + weight=Pango.Weight.NORMAL, + style=Pango.Style.NORMAL, + strikethrough=False, + justification=Gtk.Justification.LEFT) self.table = buffer.create_tag('table') self.table.set_property('wrap-mode', Gtk.WrapMode.NONE) @@ -73,34 +85,43 @@ class MarkupHandler: self.mathtext = buffer.create_tag('mathtext') - # Margins - # A default margin is set to allow negative indents for formatting headers, lists, etc + self.graytext = buffer.create_tag('graytext', + foreground='gray', + weight=Pango.Weight.NORMAL, + style=Pango.Style.NORMAL) + + # Margin and indents + # A baseline margin is set to allow negative offsets for formatting headers, lists, etc self.baseline_margin = 0 - self.margins = {} - self.update_margins() + self.margins_indents = {} + self.update_margins_indents() # Style self.on_style_updated() + self.version = 0 + def on_style_updated(self, *_): (found, color) = self.text_view.get_style_context().lookup_color('math_text_color') if not found: (_, color) = self.text_view.get_style_context().lookup_color('foreground_color') self.mathtext.set_property("foreground", color.to_string()) - def apply(self, around_cursor=False): - buffer = self.text_buffer - if around_cursor: - cursor_mark = buffer.get_insert() - start = buffer.get_iter_at_mark(cursor_mark) - start.backward_lines(3) - end = buffer.get_iter_at_mark(cursor_mark) - end.forward_lines(2) - offset = start.get_offset() + def apply(self): + self.version = self.version + 1 + if self.text_buffer.get_char_count() < self.max_char_sync: + self.do_apply() else: - start = buffer.get_start_iter() - end = buffer.get_end_iter() - offset = 0 + GLib.idle_add(self.do_apply, self.version) + + def do_apply(self, version=None): + if version is not None and version != self.version: + return + + buffer = self.text_buffer + start = buffer.get_start_iter() + end = buffer.get_end_iter() + offset = 0 text = buffer.get_slice(start, end, False) @@ -109,11 +130,14 @@ class MarkupHandler: buffer.remove_tag(self.bold, start, end) buffer.remove_tag(self.bolditalic, start, end) buffer.remove_tag(self.strikethrough, start, end) + buffer.remove_tag(self.horizontalrule, start, end) + buffer.remove_tag(self.plaintext, start, end) + buffer.remove_tag(self.table, start, end) buffer.remove_tag(self.mathtext, start, end) - buffer.remove_tag(self.centertext, start, end) - for tag in self.margins.values(): + for tag in self.margins_indents.values(): buffer.remove_tag(tag, start, end) buffer.remove_tag(self.graytext, start, end) + buffer.remove_tag(self.graytext, start, end) # Apply "_italic_" tag (italic) matches = re.finditer(self.regex["ITALIC"], text) @@ -143,15 +167,24 @@ class MarkupHandler: end_iter = buffer.get_iter_at_offset(offset + match.end()) buffer.apply_tag(self.strikethrough, start_iter, end_iter) + # Apply "---" horizontal rule tag (center) + matches = re.finditer(self.regex["HORIZONTALRULE"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start(1)) + end_iter = buffer.get_iter_at_offset(offset + match.end(1)) + buffer.apply_tag(self.horizontalrule, start_iter, end_iter) + # Apply "* list" tag (offset) matches = re.finditer(self.regex["LIST"], text) for match in matches: start_iter = buffer.get_iter_at_offset(offset + match.start()) end_iter = buffer.get_iter_at_offset(offset + match.end()) # Lists use character+space (eg. "* ") - indent = 2 + length = 2 nest = len(match.group(1).replace(" ", "\t")) - buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter) + margin = -length - 2 * nest + indent = -length - 2 * length * nest + buffer.apply_tag(self.get_margin_indent_tag(margin, indent), start_iter, end_iter) # Apply "1. numbered list" tag (offset) matches = re.finditer(self.regex["NUMBEREDLIST"], text) @@ -159,24 +192,26 @@ class MarkupHandler: start_iter = buffer.get_iter_at_offset(offset + match.start()) end_iter = buffer.get_iter_at_offset(offset + match.end()) # Numeric lists use numbers/letters+dot/parens+space (eg. "123. ") - indent = len(match.group(2)) + 1 + length = len(match.group(2)) + 1 nest = len(match.group(1).replace(" ", "\t")) - buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter) + margin = -length - 2 * nest + indent = -length - 2 * length * nest + buffer.apply_tag(self.get_margin_indent_tag(margin, indent), start_iter, end_iter) # Apply "> blockquote" tag (offset) matches = re.finditer(self.regex["BLOCKQUOTE"], text) for match in matches: start_iter = buffer.get_iter_at_offset(offset + match.start()) end_iter = buffer.get_iter_at_offset(offset + match.end()) - buffer.apply_tag(self.get_margin(2), start_iter, end_iter) + buffer.apply_tag(self.get_margin_indent_tag(2, -2), start_iter, end_iter) # Apply "#" tag (offset + bold) matches = re.finditer(self.regex["HEADER"], text) for match in matches: start_iter = buffer.get_iter_at_offset(offset + match.start()) end_iter = buffer.get_iter_at_offset(offset + match.end()) - indent = -len(match.group(1)) - 1 - buffer.apply_tag(self.get_margin(indent), start_iter, end_iter) + margin = -len(match.group(1)) - 1 + buffer.apply_tag(self.get_margin_indent_tag(margin, 0), start_iter, end_iter) buffer.apply_tag(self.bold, start_iter, end_iter) # Apply "======" header underline tag (bold) @@ -186,12 +221,13 @@ class MarkupHandler: end_iter = buffer.get_iter_at_offset(offset + match.end()) buffer.apply_tag(self.bold, start_iter, end_iter) - # Apply "---" horizontal rule tag (center) - matches = re.finditer(self.regex["HORIZONTALRULE"], text) + # Apply "```" code tag (offset) + matches = re.finditer(self.regex["CODE"], text) for match in matches: start_iter = buffer.get_iter_at_offset(offset + match.start(1)) end_iter = buffer.get_iter_at_offset(offset + match.end(1)) - buffer.apply_tag(self.centertext, start_iter, end_iter) + buffer.apply_tag(self.get_margin_indent_tag(0, 2), start_iter, end_iter) + buffer.apply_tag(self.plaintext, start_iter, end_iter) # Apply "---" table tag (wrap/pixels) matches = re.finditer(self.regex["TABLE"], text) @@ -219,20 +255,37 @@ class MarkupHandler: if end.compare(end_sentence) >= 0: buffer.apply_tag(self.graytext, end_sentence, end) - def get_margin(self, level): - if level not in self.margins: - char_width = helpers.get_char_width(self.text_view) - tag = self.text_buffer.create_tag("indent_" + str(level)) - tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0)) - self.margins[level] = tag - return self.margins[level] + # Margin and indent are cumulative. They differ in two ways: + # * Margin is always in the beginning, which means it effectively only affects the first line + # of multi-line text. Indent is applied to every line. + # * Margin level can be negative, as a baseline margin exists from which it can be subtracted. + # Indent is always positive, or 0. + 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) + self.margins_indents[level] = tag + return tag + else: + return self.margins_indents[level] - def update_margins(self): + def get_margin_indent(self, margin_level, indent_level, char_width=None): + if char_width is None: + char_width = helpers.get_char_width(self.text_view) + margin = max(self.baseline_margin + char_width * margin_level, 0) + indent = char_width * indent_level + return margin, indent + + def update_margins_indents(self): char_width = helpers.get_char_width(self.text_view) # Adjust tab size, as character width can change tab_array = Pango.TabArray.new(1, True) - tab_array.set_tab(0, Pango.TabAlign.LEFT, 2 * char_width) + tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width) self.text_view.set_tabs(tab_array) # Adjust baseline margin, as character width can change @@ -241,6 +294,8 @@ class MarkupHandler: self.text_view.set_left_margin(self.baseline_margin) self.text_view.set_right_margin(self.baseline_margin) - # Adjust left margins, as character width can change - for level, tag in self.margins.items(): - tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0)) + # Adjust margins and indents, as character width can change + for level, tag in self.margins_indents.items(): + margin, indent = self.get_margin_indent(*level, char_width) + tag.set_property("left-margin", margin) + tag.set_property("indent", indent) From bf657891c66972c84cda7cc2e124b2875d35dd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 11 Apr 2019 02:30:32 +0100 Subject: [PATCH 09/54] Add link markup Fixes #90 --- uberwriter/text_view_markup_handler.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py index e4d8dcf..374445e 100644 --- a/uberwriter/text_view_markup_handler.py +++ b/uberwriter/text_view_markup_handler.py @@ -36,6 +36,7 @@ class MarkupHandler: "BOLD": re.compile(r"(\*\*|__)(.+?)\1"), "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), "LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE), "NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE), @@ -167,6 +168,15 @@ class MarkupHandler: end_iter = buffer.get_iter_at_offset(offset + match.end()) buffer.apply_tag(self.strikethrough, start_iter, end_iter) + matches = re.finditer(self.regex["LINK"], text) + for match in matches: + start_iter = buffer.get_iter_at_offset(offset + match.start(1)) + end_iter = buffer.get_iter_at_offset(offset + match.end(1)) + buffer.apply_tag(self.graytext, start_iter, end_iter) + start_iter = buffer.get_iter_at_offset(offset + match.start(2)) + end_iter = buffer.get_iter_at_offset(offset + match.end(2)) + buffer.apply_tag(self.graytext, start_iter, end_iter) + # Apply "---" horizontal rule tag (center) matches = re.finditer(self.regex["HORIZONTALRULE"], text) for match in matches: From ac7e18b0b3b212ba43b6f410683bb7989574f61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Thu, 11 Apr 2019 03:33:31 +0100 Subject: [PATCH 10/54] Improve auto-scrolling after pasting very large documents There were 2 problems. When pasting very large documents, the height calculations will be temporarily incorrect while the content is rendered over several frames. This is addressed by waiting for the UI to be idle to scroll. Additionally, the scroll time (typically 200ms) needs an adjustment as well. Starting at 200ms, it now scales linearly with distance, amounting to roughly 4 seconds with Pandoc's user guide. --- uberwriter/scroller.py | 34 +++++++++++++++++----------------- uberwriter/text_view.py | 6 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/uberwriter/scroller.py b/uberwriter/scroller.py index c95773e..518167f 100644 --- a/uberwriter/scroller.py +++ b/uberwriter/scroller.py @@ -1,47 +1,47 @@ class Scroller: - def __init__(self, scrolled_window, target_pos, source_pos, duration=2000): + def __init__(self, scrolled_window, source_pos, target_pos): super().__init__() self.scrolled_window = scrolled_window - self.target_pos = target_pos self.source_pos = source_pos - self.duration = duration + 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.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick, self) + self.is_started = True + self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick) def end(self): - self.is_started = False self.scrolled_window.remove_tick_callback(self.tick_callback_id) + self.is_started = False - def do_start(self, time): - self.is_started = True + def setup(self, time): self.start_time = time - self.end_time = time + self.duration * 100 + self.end_time = time + self.duration + self.is_setup = True - @staticmethod - def on_tick(widget, frame_clock, scroller): + 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 scroller.is_started: - scroller.do_start(now) + if not self.is_setup: + self.setup(now) - if now < scroller.end_time: - time = float(now - scroller.start_time) / float(scroller.end_time - scroller.start_time) + if now < self.end_time: + time = float(now - self.start_time) / float(self.end_time - self.start_time) else: time = 1 - scroller.end() + self.end() time = ease_out_cubic(time) - pos = scroller.source_pos + (time * (scroller.target_pos - scroller.source_pos)) + pos = self.source_pos + (time * (self.target_pos - self.source_pos)) widget.get_vadjustment().props.value = pos return True diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 3e59378..3d6ca2e 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -8,7 +8,7 @@ from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, from uberwriter.scroller import Scroller gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, GObject +from gi.repository import Gtk, Gdk, GObject, GLib import logging LOGGER = logging.getLogger('uberwriter') @@ -104,7 +104,7 @@ class TextView(Gtk.TextView): def on_text_changed(self, *_): self.markup.apply() - self.scroll_to() + GLib.idle_add(self.scroll_to) def on_size_allocate(self, *_): self.update_vertical_margin() @@ -186,7 +186,7 @@ class TextView(Gtk.TextView): if self.scroller and self.scroller.is_started: self.scroller.end() if target_pos: - self.scroller = Scroller(scrolled_window, target_pos, va.props.value) + self.scroller = Scroller(scrolled_window, va.props.value, target_pos) self.scroller.start() def on_mark_set(self, _text_buffer, _location, mark, _data=None): From 5be563ccc719d6d4f1383bdd2b07a195d8bba85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 13 Apr 2019 02:35:54 +0100 Subject: [PATCH 11/54] Fix search and replace TextIter must not be reused in between buffer changes. This resulted in unpredictable behavior when using search and replace. For instance, in the following string: This _is_ a _test_ of _search_ and _replace_ Searching for "_" and replacing with "**" sequentially would: [0] This **is_ a _test_ [1] This ****is a _test_ [2] This ****is a **test_ [2] This ****is a ****test Replace had similar results. --- uberwriter/search_and_replace.py | 58 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 32cbfa5..0541fa9 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -35,6 +35,9 @@ class SearchAndReplace: def __init__(self, parentwindow, textview): self.parentwindow = parentwindow + self.textview = textview + self.textbuffer = textview.get_buffer() + self.box = parentwindow.builder.get_object("searchbar_placeholder") self.box.set_reveal_child(False) self.searchbar = parentwindow.builder.get_object("searchbar") @@ -46,9 +49,6 @@ class SearchAndReplace: self.open_replace_button = parentwindow.builder.get_object("replace") self.open_replace_button.connect("toggled", self.toggle_replace) - self.textview = textview - self.textbuffer = textview.get_buffer() - self.nextbutton = parentwindow.builder.get_object("next_result") self.prevbutton = parentwindow.builder.get_object("previous_result") self.regexbutton = parentwindow.builder.get_object("regex") @@ -68,11 +68,13 @@ class SearchAndReplace: self.prevbutton.connect('clicked', self.scrolltoprev) self.regexbutton.connect('toggled', self.search) self.casesensitivebutton.connect('toggled', self.search) - self.highlight = self.textbuffer.create_tag('search_highlight', - background="yellow") + self.highlight = self.textbuffer.create_tag('search_highlight', background="yellow") self.textview.connect("focus-in-event", self.focused_texteditor) + self.matches = [] + self.active = 0 + def toggle_replace(self, widget, _data=None): """toggle the replace box """ @@ -107,10 +109,9 @@ class SearchAndReplace: def search(self, _widget=None, _data=None, scroll=True): searchtext = self.searchentry.get_text() - buf = self.textbuffer - context_start = buf.get_start_iter() - context_end = buf.get_end_iter() - text = buf.get_slice(context_start, context_end, False) + context_start = self.textbuffer.get_start_iter() + context_end = self.textbuffer.get_end_iter() + text = self.textbuffer.get_slice(context_start, context_end, False) self.textbuffer.remove_tag(self.highlight, context_start, context_end) @@ -125,12 +126,12 @@ class SearchAndReplace: matches = re.finditer(searchtext, text, flags) - self.matchiters = [] + self.matches = [] self.active = 0 for match in matches: - start_iter = buf.get_iter_at_offset(match.start()) - end_iter = buf.get_iter_at_offset(match.end()) - self.matchiters.append((start_iter, end_iter)) + self.matches.append((match.start(), match.end())) + start_iter = self.textbuffer.get_iter_at_offset(match.start()) + end_iter = self.textbuffer.get_iter_at_offset(match.end()) self.textbuffer.apply_tag(self.highlight, start_iter, end_iter) if scroll: self.scrollto(self.active) @@ -143,17 +144,17 @@ class SearchAndReplace: self.scrollto(self.active - 1) def scrollto(self, index): - if not self.matchiters: + if not self.matches: return - if index < len(self.matchiters): + if index < len(self.matches): self.active = index else: self.active = 0 - matchiter = self.matchiters[self.active] - self.textview.get_buffer().select_range(matchiter[0], matchiter[1]) - - # self.texteditor.scroll_to_iter(matchiter[0], 0.0, True, 0.0, 0.5) + match = self.matches[self.active] + start_iter = self.textbuffer.get_iter_at_offset(match[0]) + end_iter = self.textbuffer.get_iter_at_offset(match[1]) + self.textbuffer.select_range(start_iter, end_iter) def hide(self): self.replacebox.set_reveal_child(False) @@ -167,17 +168,20 @@ class SearchAndReplace: self.replace(self.active) def replace_all(self, _widget=None, _data=None): - while self.matchiters: - match = self.matchiters[0] - self.textbuffer.delete(match[0], match[1]) - self.textbuffer.insert(match[0], self.replaceentry.get_text()) - self.search(scroll=False) + for match in reversed(self.matches): + self.do_replace(match) + self.search(scroll=False) def replace(self, searchindex, _inloop=False): - match = self.matchiters[searchindex] - self.textbuffer.delete(match[0], match[1]) - self.textbuffer.insert(match[0], self.replaceentry.get_text()) + self.do_replace(self.matches[searchindex]) active = self.active self.search(scroll=False) self.active = active self.scrollto(self.active) + + def do_replace(self, match): + start_iter = self.textbuffer.get_iter_at_offset(match[0]) + end_iter = self.textbuffer.get_iter_at_offset(match[1]) + self.textbuffer.delete(start_iter, end_iter) + start_iter = self.textbuffer.get_iter_at_offset(match[0]) + self.textbuffer.insert(start_iter, self.replaceentry.get_text()) From ef54c752bad1a42eec6ef1803d77fd090adc6bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 13 Apr 2019 02:45:21 +0100 Subject: [PATCH 12/54] Fix replace row missing after hiding The replace row would be consistently missing after losing focus (ie. hiding), and would be inconsistent with the button state. --- uberwriter/search_and_replace.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 0541fa9..f137707 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -78,10 +78,7 @@ class SearchAndReplace: def toggle_replace(self, widget, _data=None): """toggle the replace box """ - if widget.get_active(): - self.replacebox.set_reveal_child(True) - else: - self.replacebox.set_reveal_child(False) + self.replacebox.set_reveal_child(widget.get_active()) # TODO: refactorize! def key_pressed(self, _widget, event, _data=None): @@ -157,7 +154,6 @@ class SearchAndReplace: self.textbuffer.select_range(start_iter, end_iter) def hide(self): - self.replacebox.set_reveal_child(False) self.box.set_reveal_child(False) self.textbuffer.remove_tag(self.highlight, self.textbuffer.get_start_iter(), From 58e2d2e0beb11cc894007293eec8ed1822531198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 13 Apr 2019 02:47:26 +0100 Subject: [PATCH 13/54] Remove uneeded threads_init calls --- uberwriter/inline_preview.py | 2 -- uberwriter/webkit2png/webkit2png.py | 1 - 2 files changed, 3 deletions(-) diff --git a/uberwriter/inline_preview.py b/uberwriter/inline_preview.py index 86fead3..ae5f9d9 100644 --- a/uberwriter/inline_preview.py +++ b/uberwriter/inline_preview.py @@ -37,8 +37,6 @@ from uberwriter.fix_table import FixTable LOGGER = logging.getLogger('uberwriter') -GObject.threads_init() # Still needed? - # TODO: # - Don't insert a span with id, it breaks the text to often # Would be better to search for the nearest title and generate diff --git a/uberwriter/webkit2png/webkit2png.py b/uberwriter/webkit2png/webkit2png.py index ffc2345..ba9f22b 100755 --- a/uberwriter/webkit2png/webkit2png.py +++ b/uberwriter/webkit2png/webkit2png.py @@ -101,7 +101,6 @@ class PyGTKBrowser: if options.delay: print("--delay is only supported on Mac OS X (for now). Sorry!") - gobject.threads_init() window = gtk.Window() window.resize(int(options.initWidth),int(options.initHeight)) self.view = webkit.WebView() From af9d1adf7c88aa09dcd1b08b853fbdf186a701c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 13 Apr 2019 03:45:09 +0100 Subject: [PATCH 14/54] Uniformize header/status bar reveal duration --- uberwriter/headerbars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index d424ab4..6ee4ed8 100644 --- a/uberwriter/headerbars.py +++ b/uberwriter/headerbars.py @@ -38,7 +38,7 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods self.hb_revealer = Gtk.Revealer(name='titlebar-revealer') self.hb_revealer.add(self.hb) - self.hb_revealer.props.transition_duration = 1000 + 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) From 1842a849ad34c75b0e33c980eb17868694089ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 13 Apr 2019 03:45:27 +0100 Subject: [PATCH 15/54] Fix warning due to missing argument --- uberwriter/text_view_undo_redo_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uberwriter/text_view_undo_redo_handler.py b/uberwriter/text_view_undo_redo_handler.py index 7ccbd1c..f91903c 100644 --- a/uberwriter/text_view_undo_redo_handler.py +++ b/uberwriter/text_view_undo_redo_handler.py @@ -63,7 +63,7 @@ class UndoRedoHandler: self.__end_not_undoable_action() self.undo_in_progress = False - def redo(self, text_view, _data): + def redo(self, text_view, _data=None): """Redo insertions or deletions. Redone actions are moved to undo stack This method can be registered to a custom redo signal, or used independently.""" From 50729b0d34ec7282443999563a67262c5798dac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 01:58:45 +0100 Subject: [PATCH 16/54] Introduce pypandoc for pandoc calls --- requirements.txt | 2 +- uberwriter/export_dialog.py | 361 ++++++++++++++++-------------------- uberwriter/helpers.py | 15 +- uberwriter/theme.py | 7 +- uberwriter/window.py | 43 +---- 5 files changed, 186 insertions(+), 242 deletions(-) diff --git a/requirements.txt b/requirements.txt index 340d2e0..971e047 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ regex enchant python-gtkspellcheck -pandoc +pypandoc==1.4 diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index 812e8d5..c652a3f 100644 --- a/uberwriter/export_dialog.py +++ b/uberwriter/export_dialog.py @@ -17,14 +17,11 @@ """ -import os -import subprocess import logging -# import gettext - +import os from gettext import gettext as _ -import gi +import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk @@ -43,6 +40,109 @@ class Export: __gtype_name__ = "export_dialog" + formats = [ + { + "name": "LaTeX Source", + "ext": "tex", + "to": "latex" + }, + { + "name": "LaTeX PDF", + "ext": "pdf", + "to": "pdf" + }, + { + "name": "LaTeX beamer slide show Source .tex", + "ext": "tex", + "to": "beamer" + }, + { + "name": "LaTeX beamer slide show PDF", + "ext": "pdf", + "to": "beamer" + }, + { + "name": "HTML", + "ext": "html", + "to": "html5" + }, + { + "name": "Textile", + "ext": "txt", + "to": "textile" + }, + { + "name": "OpenOffice text document", + "ext": "odt", + "to": "odt" + }, + { + "name": "Word docx", + "ext": "docx", + "to": "docx" + }, + { + "name": "reStructuredText txt", + "ext": "txt", + "to": "rst" + }, + { + "name": "ConTeXt tex", + "ext": "tex", + "to": "context" + }, + { + "name": "groff man", + "ext": "man", + "to": "man" + }, + { + "name": "MediaWiki markup", + "ext": "txt", + "to": "mediawiki" + }, + { + "name": "OpenDocument XML", + "ext": "xml", + "to": "opendocument" + }, + { + "name": "OpenDocument XML", + "ext": "texi", + "to": "texinfo" + }, + { + "name": "Slidy HTML and javascript slide show", + "ext": "html", + "to": "slidy" + }, + { + "name": "Slideous HTML and javascript slide show", + "ext": "html", + "to": "slideous" + }, + { + "name": "HTML5 + javascript slide show", + "ext": "html", + "to": "dzslides" + }, + { + "name": "S5 HTML and javascript slide show", + "ext": "html", + "to": "s5" + }, + { + "name": "EPub electronic publication", + "ext": "epub", + "to": "epub" + }, + { + "name": "RTF Rich Text Format", + "ext": "rtf", + "to": "rtf" + } + ] + def __init__(self, filename): """Set up the about dialog""" self.builder = get_builder('Export') @@ -53,8 +153,8 @@ class Export: stack_pdf_disabled = self.builder.get_object("pdf_disabled") filename = filename or _("Untitled document.md") - self.filechoosers = {export_format:self.stack.get_child_by_name(export_format)\ - for export_format in ["pdf", "html", "odt", "advanced"]} + self.filechoosers = {export_format: self.stack.get_child_by_name(export_format) + for export_format in ["pdf", "html", "odt", "advanced"]} for export_format, filechooser in self.filechoosers.items(): filechooser.set_do_overwrite_confirmation(True) filechooser.set_current_folder(os.path.dirname(filename)) @@ -76,9 +176,12 @@ class Export: self.builder.get_object("highlight_style").set_active(0) + self.builder.get_object("css_filechooser").set_uri( + helpers.path_to_file(Theme.get_current().web_css_path)) + format_store = Gtk.ListStore(int, str) - for fmt_id in self.formats_dict: - format_store.append([fmt_id, self.formats_dict[fmt_id]["name"]]) + for i, fmt in enumerate(self.formats): + format_store.append([i, fmt["name"]]) self.format_field = self.builder.get_object('choose_format') self.format_field.set_model(format_store) @@ -87,171 +190,56 @@ class Export: self.format_field.add_attribute(format_renderer, "text", 1) self.format_field.set_active(0) - formats_dict = { - 1: { - "name": "LaTeX Source", - "ext": "tex", - "to": "latex" - }, - 2: { - "name": "LaTeX PDF", - "ext": "pdf", - "to": "pdf" - }, - 3: { - "name": "LaTeX beamer slide show Source .tex", - "ext": "tex", - "to": "beamer" - }, - 4: { - "name": "LaTeX beamer slide show PDF", - "ext": "pdf", - "to": "beamer" - }, - 5: { - "name": "HTML", - "ext": "html", - "to": "html" - }, - 6: { - "name": "Textile", - "ext": "txt", - "to": "textile" - }, - 7: { - "name": "OpenOffice text document", - "ext": "odt", - "to": "odt" - }, - 8: { - "name": "Word docx", - "ext": "docx", - "to": "docx" - }, - 9: { - "name": "reStructuredText txt", - "ext": "txt", - "to": "rst" - }, - 10: { - "name": "ConTeXt tex", - "ext": "tex", - "to": "context" - }, - 11: { - "name": "groff man", - "ext": "man", - "to": "man" - }, - 12: { - "name": "MediaWiki markup", - "ext": "txt", - "to": "mediawiki" - }, - 13: { - "name": "OpenDocument XML", - "ext": "xml", - "to": "opendocument" - }, - 14: { - "name": "OpenDocument XML", - "ext": "texi", - "to": "texinfo" - }, - 15: { - "name": "Slidy HTML and javascript slide show", - "ext": "html", - "to": "slidy" - }, - 16: { - "name": "Slideous HTML and javascript slide show", - "ext": "html", - "to": "slideous" - }, - 17: { - "name": "HTML5 + javascript slide show", - "ext": "html", - "to": "dzslides" - }, - 18: { - "name": "S5 HTML and javascript slide show", - "ext": "html", - "to": "s5" - }, - 19: { - "name": "EPub electronic publication", - "ext": "epub", - "to": "epub" - }, - 20: { - "name": "RTF Rich Text Format", - "ext": "rtf", - "to": "rtf" - } - - } - def export(self, text=""): - """Export to pdf, html or odt the given text + """Export the given text using the specified format. + For advanced export, this includes special flags for the enabled options. Keyword Arguments: text {str} -- Text to export (default: {""}) """ - export_format = self.stack.get_visible_child_name() + export_type = self.stack.get_visible_child_name() + args = [] + if export_type == "advanced": + filename = self.adv_export_name.get_text() + output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder()) + basename = os.path.basename(filename) + + fmt = self.formats[self.format_field.get_active()] + to = fmt["to"] + ext = fmt["ext"] + + if self.builder.get_object("html5").get_active() and to == "html": + to = "html5" + if self.builder.get_object("smart").get_active(): + to += "+smart" + + args.extend(self.get_advanced_arguments()) - if export_format == "advanced": - self.advanced_export(text) else: - filename = self.filechoosers[export_format].get_filename() - if filename.endswith("." + export_format): - filename = filename[:-len(export_format)-1] - + filename = self.filechoosers[export_type].get_filename() + if filename.endswith("." + export_type): + filename = filename[:-len(export_type)-1] output_dir = os.path.abspath(os.path.join(filename, os.path.pardir)) basename = os.path.basename(filename) - args = ['pandoc', '--from=markdown', '-s'] + to = export_type + ext = export_type - if export_format == "pdf": - args.append("-o%s.pdf" % basename) - - elif export_format == "odt": - args.append("-o%s.odt" % basename) - - elif export_format == "html": - css = Theme.ADWAITA.get_gtk_css_file() - relativize = helpers.get_script_path('relative_to_absolute.lua') - task_list = helpers.get_script_path('task-list.lua') - args.append("-c%s" % css) - args.append("-o%s.html" % basename) + if export_type == "html": + to = "html5" + args.append("--standalone") + args.append("--css=%s" % Theme.get_current().web_css_path) args.append("--mathjax") - args.append("--lua-filter=" + relativize) - args.append("--lua-filter=" + task_list) + args.append("--lua-filter=%s" % helpers.get_script_path('relative_to_absolute.lua')) + args.append("--lua-filter=%s" % helpers.get_script_path('task-list.lua')) - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, cwd=output_dir) - _ = proc.communicate(text)[0] + helpers.pandoc_convert( + text, to=to, args=args, + outputfile="%s/%s.%s" % (output_dir, basename, ext)) - def advanced_export(self, text=""): - """Export the given text to special formats with the enabled flags - - Keyword Arguments: - text {str} -- The text to export (default: {""}) - """ - - filename = self.adv_export_name.get_text() - output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder()) - basename = os.path.basename(filename) - args = self.set_arguments(basename) - - LOGGER.info(args) - - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, cwd=output_dir) - _ = proc.communicate(text)[0] - - def set_arguments(self, basename): - """Retrieve a list of the selected arguments + def get_advanced_arguments(self): + """Retrieve a list of the selected advanced arguments For most of the advanced option checkboxes, returns a list of the related pandoc flags @@ -265,78 +253,49 @@ class Export: highlight_style = self.builder.get_object("highlight_style").get_active_text() - conditions_dict = { - 1: { + conditions = [ + { "condition": self.builder.get_object("toc").get_active(), "yes": "--toc", "no": None }, - 2: { + { "condition": self.builder.get_object("highlight").get_active(), "yes": "--highlight-style=%s" % highlight_style, "no": "--no-highlight" }, - 3: { + { "condition": self.builder.get_object("standalone").get_active(), "yes": "--standalone", "no": None }, - 4: { + { "condition": self.builder.get_object("number_sections").get_active(), "yes": "--number-sections", "no": None }, - 5: { + { "condition": self.builder.get_object("strict").get_active(), "yes": "--strict", "no": None }, - 6: { + { "condition": self.builder.get_object("incremental").get_active(), "yes": "--incremental", "no": None }, - 7: { + { "condition": self.builder.get_object("self_contained").get_active(), "yes": "--self-contained", "no": None } - } + ] - tree_iter = self.format_field.get_active_iter() - if tree_iter is not None: - model = self.format_field.get_model() - row_id, _ = model[tree_iter][:2] + args = [] - fmt = self.formats_dict[row_id] + args.extend([c["yes"] if c["condition"] else c["no"] for c in conditions]) - args = ['pandoc', '--from=markdown'] - - extension = "--to=%s" % fmt["to"] - - if basename.endswith("." + fmt["ext"]): - output_file = "--output=%s" % basename - else: - output_file = "--output=%s.%s" % (basename, fmt["ext"]) - - args.extend([conditions_dict[c_id]["yes"]\ - if conditions_dict[c_id]["condition"]\ - else conditions_dict[c_id]["no"]\ - for c_id in conditions_dict]) - - args = list(filter(None, args)) - - if self.builder.get_object("html5").get_active(): - if fmt["to"] == "html": - extension = "--to=%s" % "html5" - - if self.builder.get_object("smart").get_active(): - extension += '+smart' - else: - extension += '-smart' - - if fmt["to"] != "pdf": - args.append(extension) + args = list(filter(lambda arg: arg is not None, args)) css_uri = self.builder.get_object("css_filechooser").get_uri() if css_uri: @@ -350,8 +309,6 @@ class Export: bib_uri = bib_uri[7:] args.append("--bibliography=%s" % bib_uri) - args.append(output_file) - return args def allow_export(self, widget, data, signal): diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py index 3ae6d9e..45454db 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -23,6 +23,7 @@ import shutil import gi +import pypandoc from gi.overrides.Pango import Pango gi.require_version('Gtk', '3.0') @@ -50,13 +51,18 @@ def get_builder(builder_file_name): return builder -# Owais Lone : To get quick access to icons and stuff. +def path_to_file(path): + """Return a file path (file:///) for the given path""" + + return "file:///" + path + + def get_media_file(media_file_path): """Return the full path of a given filename under the media dir (starts with file:///) """ - return "file:///" + get_media_path(media_file_path) + return path_to_file(get_media_path(media_file_path)) def get_media_path(media_file_name): @@ -196,3 +202,8 @@ def get_descendant(widget, child_name, level, doPrint=False): def get_char_width(widget): return Pango.units_to_double( widget.get_pango_context().get_metrics().get_approximate_char_width()) + + +def pandoc_convert(text, fr="markdown", to="html5", args=[], outputfile=None): + args.extend(["--quiet"]) + return pypandoc.convert_text(text, to, fr, extra_args=args, outputfile=outputfile) diff --git a/uberwriter/theme.py b/uberwriter/theme.py index c1ab1a0..8139c90 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -30,7 +30,7 @@ class Theme: return current_theme @classmethod - def get_current(cls): + 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() @@ -41,6 +41,11 @@ class Theme: cls.previous = current_theme return current_theme, changed + @classmethod + def get_current(cls): + current_theme, _ = cls.get_current_changed() + return current_theme + def __eq__(self, other): return isinstance(other, self.__class__) and \ self.name == other.name and \ diff --git a/uberwriter/window.py b/uberwriter/window.py index 432f13d..cc9a387 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -19,7 +19,6 @@ import locale import logging import os import re -import subprocess import urllib import webbrowser from gettext import gettext as _ @@ -172,8 +171,7 @@ class Window(Gtk.ApplicationWindow): """Adjusts the window, CSD and preview for the current theme. """ # Get current theme - theme, changed = Theme.get_current() - + theme, changed = Theme.get_current_changed() if changed: # Set theme variant (dark/light) Gtk.Settings.get_default().set_property( @@ -398,15 +396,9 @@ class Window(Gtk.ApplicationWindow): """Copies only html without headers etc. to Clipboard """ - args = ['pandoc', '--from=markdown', '--to=html5'] - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - - text = bytes(self.text_view.get_text(), "utf-8") - output = proc.communicate(text)[0] - + output = helpers.pandoc_convert(self.text_view.get_text()) clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) - clipboard.set_text(output.decode("utf-8"), -1) + clipboard.set_text(output, -1) clipboard.store() def open_document(self, _widget=None): @@ -422,7 +414,7 @@ class Window(Gtk.ApplicationWindow): filefilter.set_name(_('MarkDown or Plain Text')) filechooser = Gtk.FileChooserDialog( - _("Open a .md-File"), + _("Open a .md file"), self, Gtk.FileChooserAction.OPEN, ("_Cancel", Gtk.ResponseType.CANCEL, @@ -557,33 +549,12 @@ class Window(Gtk.ApplicationWindow): self.preview_webview.show() self.queue_draw() else: - # We need to convert relative routes to absolute ones - # For that first we need to know if the file is saved: - if self.filename: - base_path = os.path.dirname(self.filename) - else: - base_path = '' - os.environ['PANDOC_PREFIX'] = base_path + '/' - - theme, _ = Theme.get_current() - args = ['pandoc', - '-s', - '--from=markdown', - '--to=html5', + args = ['--standalone', '--mathjax', - '--css=' + theme.web_css_path, - '--quiet', + '--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')] - - # TODO: find a way to pass something like this instead of the quiet arg - #'--metadata pagetitle="test"', - - proc = subprocess.Popen( - args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) - - text = bytes(self.text_view.get_text(), "utf-8") - output = proc.communicate(text)[0] + output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args) if self.preview_webview is None: self.preview_webview = WebKit.WebView() From 4b32617ca7d9ecc691371fcacfcc9f4be22d453d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 01:59:10 +0100 Subject: [PATCH 17/54] Update pandoc help link --- data/ui/Export.ui | 2 +- uberwriter/application.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/data/ui/Export.ui b/data/ui/Export.ui index 011d433..0c8dffa 100644 --- a/data/ui/Export.ui +++ b/data/ui/Export.ui @@ -426,7 +426,7 @@ True True none - http://johnmacfarlane.net/pandoc/README.html + https://pandoc.org/MANUAL.html
0 diff --git a/uberwriter/application.py b/uberwriter/application.py index 3d0c2e6..67f0e85 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -208,8 +208,7 @@ class Application(Gtk.Application): def on_help(self, _action, _param): """open pandoc markdown web """ - webbrowser.open( - "http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown") + webbrowser.open("https://pandoc.org/MANUAL.html#pandocs-markdown") def on_shortcuts(self, _action, _param): builder = get_builder('Shortcuts') From fbef0143e2304b169e281c79490586f53b74bba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 02:03:13 +0100 Subject: [PATCH 18/54] Use correct mime type for markdown, split markdown/plain text --- uberwriter/window.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/uberwriter/window.py b/uberwriter/window.py index cc9a387..b47565f 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -408,10 +408,14 @@ class Window(Gtk.ApplicationWindow): if self.check_change() == Gtk.ResponseType.CANCEL: return - filefilter = Gtk.FileFilter.new() - filefilter.add_mime_type('text/x-markdown') - filefilter.add_mime_type('text/plain') - filefilter.set_name(_('MarkDown or Plain Text')) + markdown_filter = Gtk.FileFilter.new() + markdown_filter.add_mime_type('text/markdown') + markdown_filter.add_mime_type('text/x-markdown') + markdown_filter.set_name(_('Markdown Files')) + + plaintext_filter = Gtk.FileFilter.new() + plaintext_filter.add_mime_type('text/plain') + plaintext_filter.set_name(_('Plain Text Files')) filechooser = Gtk.FileChooserDialog( _("Open a .md file"), @@ -420,7 +424,8 @@ class Window(Gtk.ApplicationWindow): ("_Cancel", Gtk.ResponseType.CANCEL, "_Open", Gtk.ResponseType.OK) ) - filechooser.add_filter(filefilter) + filechooser.add_filter(markdown_filter) + filechooser.add_filter(plaintext_filter) response = filechooser.run() if response == Gtk.ResponseType.OK: filename = filechooser.get_filename() From 13535ab8393943a60bd721be9da2fa8bb3e8fc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 02:13:42 +0100 Subject: [PATCH 19/54] Delete unused UI files --- data/ui/UberwriterAdvancedExportDialog.ui | 548 ------------------ data/ui/uberwriter_advanced_export_dialog.xml | 9 - 2 files changed, 557 deletions(-) delete mode 100644 data/ui/UberwriterAdvancedExportDialog.ui delete mode 100644 data/ui/uberwriter_advanced_export_dialog.xml diff --git a/data/ui/UberwriterAdvancedExportDialog.ui b/data/ui/UberwriterAdvancedExportDialog.ui deleted file mode 100644 index 4e647b8..0000000 --- a/data/ui/UberwriterAdvancedExportDialog.ui +++ /dev/null @@ -1,548 +0,0 @@ - - - - - - False - 5 - dialog - - - False - vertical - 2 - - - False - end - - - gtk-cancel - False - True - True - True - False - True - - - False - True - 1 - - - - - Export - False - True - True - True - True - True - True - True - False - - - False - True - 2 - - - - - False - True - end - 0 - - - - - True - False - vertical - - - True - False - - - False - True - 0 - - - - - True - False - 0 - out - - - True - False - 12 - - - True - False - 5 - 5 - 5 - vertical - - - Smart - False - True - True - False - Pandoc can automatically make "--" to a long dash and more - False - 0 - True - True - - - False - True - 0 - - - - - Normalize - False - True - True - False - Removes things like double spaces or spaces at the beginning of a paragraph - False - 0 - True - - - False - True - 1 - - - - - Table of Contents - False - True - True - False - False - 0 - True - - - False - True - 2 - - - - - Standalone - False - True - True - False - Use a header and footer to include things like stylesheets and meta information - False - 0 - True - True - - - False - True - 3 - - - - - Number Sections - False - True - True - False - False - 0 - True - - - False - True - 4 - - - - - Strict Markdown - False - True - True - False - Use "strict" markdown instead of "pandoc" markdown - False - 0 - True - - - False - True - 5 - - - - - Slideshow incremental bullets - False - True - True - False - Show one bullet point after another in a slideshow - False - 0 - True - - - False - True - 6 - - - - - - - - - True - False - <b>General Options</b> - True - - - - - False - True - 5 - 4 - - - - - True - False - 0 - out - - - True - False - 12 - - - True - False - 5 - 5 - 5 - vertical - - - Highlight syntax - False - True - True - False - False - 0 - True - True - - - False - True - 0 - - - - - True - False - - - True - False - Choose a color theme for syntax highlighting - Highlight style - - - False - True - 0 - - - - - True - False - Choose a color theme for syntax highlighting - 0 - 0 - 1 - 0 - - pygments - kate - monochrome - espresso - zenburn - haddock - tango - - - - True - True - end - 1 - - - - - False - True - 1 - - - - - - - - - True - False - <b>Syntax highlighting</b> (HTML, LaTeX) - True - - - - - False - True - 5 - 5 - - - - - True - False - 0 - out - - - True - False - 12 - - - True - False - 5 - 5 - 5 - vertical - True - - - Self Contained - False - True - True - False - Produces a HTML that has no external dependencies (all images and stylesheets are included) - False - 0 - True - - - False - True - 0 - - - - - HTML 5 - False - True - True - False - Use HTML 5 syntax - False - 0 - True - - - False - True - 1 - - - - - True - False - - - True - False - Choose a CSS File that you want to use - 10 - CSS File - - - False - True - 0 - - - - - True - False - Choose a CSS File that you want to use - vertical - - - True - True - 1 - - - - - False - True - 2 - - - - - - - - - True - False - <b>HTML Options</b> - True - - - - - False - True - 5 - 6 - - - - - True - False - 0 - out - - - True - False - 12 - - - True - False - 5 - 5 - 5 - vertical - 5 - - - - - - - True - False - <b>Bibliography File</b> - True - - - - - False - True - 5 - 7 - - - - - True - False - True - - - Commandline Reference - False - True - True - True - True - False - none - http://johnmacfarlane.net/pandoc/README.html - - - False - True - 5 - 0 - - - - - True - True - 9 - - - - - False - True - 1 - - - - - - button2 - button1 - - - diff --git a/data/ui/uberwriter_advanced_export_dialog.xml b/data/ui/uberwriter_advanced_export_dialog.xml deleted file mode 100644 index d1585a2..0000000 --- a/data/ui/uberwriter_advanced_export_dialog.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - From 13ea2cc8fe6ea65255b8cafdb03779159cf96316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 02:14:29 +0100 Subject: [PATCH 20/54] Make advanced options match defaults, fix capitalization/typos Having advanced options match defaults helps as a self-documenting reference. --- data/ui/Export.ui | 15 +++++++-------- help/C/preview.page | 4 ++-- help/stump/help.md | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/data/ui/Export.ui b/data/ui/Export.ui index 0c8dffa..30101f3 100644 --- a/data/ui/Export.ui +++ b/data/ui/Export.ui @@ -42,7 +42,6 @@ False Pandoc can automatically make "--" to a long dash and more start - True True @@ -120,7 +119,7 @@
- Slideshow incremental bullets + Slideshow Incremental Bullets False True True @@ -179,13 +178,12 @@ vertical - Highlight syntax + Highlight Syntax False True True False start - True True @@ -250,7 +248,7 @@ True False - <b>Syntax highlighting</b> (HTML, LaTeX) + <b>Syntax Highlighting</b> (HTML, LaTeX) True @@ -326,7 +324,7 @@ True - Self Contained + Self-contained False True True @@ -343,13 +341,14 @@ - HTML 5 + HTML5 False True True False - Use HTML 5 syntax + Use HTML5 syntax start + True True diff --git a/help/C/preview.page b/help/C/preview.page index beb7711..f7ca957 100644 --- a/help/C/preview.page +++ b/help/C/preview.page @@ -13,7 +13,7 @@ UberWriter Preview -

There are 2 different ways to preview your MarkDown files in UberWriter and +

There are 2 different ways to preview your Markdown files in UberWriter and quickly check, what you have written.

@@ -29,6 +29,6 @@ quickly check, what you have written. Complete Preview

If you want a complete Preview of your document, you just need to hit the preview Button on the statusbar at the bottom of the UberWriter window. - It will render the complete HTML Output of your MarkDown file.

+ It will render the complete HTML Output of your Markdown file.

diff --git a/help/stump/help.md b/help/stump/help.md index 9b0dc25..e4a98a9 100644 --- a/help/stump/help.md +++ b/help/stump/help.md @@ -1183,7 +1183,7 @@ Sergey Astanin. [Slidy]: http://www.w3.org/Talks/Tools/Slidy/ [Slideous]: http://goessner.net/articles/slideous/ [HTML]: http://www.w3.org/TR/html40/ -[HTML 5]: http://www.w3.org/TR/html5/ +[HTML5]: http://www.w3.org/TR/html5/ [XHTML]: http://www.w3.org/TR/xhtml1/ [LaTeX]: http://www.latex-project.org/ [beamer]: http://www.tex.ac.uk/CTAN/macros/latex/contrib/beamer From 0c94f0c168eacecfc68e865ba41253a478adbf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 02:40:44 +0100 Subject: [PATCH 21/54] Fix padding/margins in export dialog UI --- data/ui/Export.ui | 63 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/data/ui/Export.ui b/data/ui/Export.ui index 30101f3..9c19871 100644 --- a/data/ui/Export.ui +++ b/data/ui/Export.ui @@ -6,7 +6,12 @@ True False center + 2 + 2 + 2 + 2 True + 2 True @@ -19,6 +24,7 @@ True False center + 8 0 out @@ -30,8 +36,9 @@ True False - 5 - 5 + 8 + 4 + 4 vertical @@ -157,6 +164,7 @@ True False center + 8 vertical @@ -173,13 +181,12 @@ True False - 5 - 5 + 4 + 4 vertical Highlight Syntax - False True True False @@ -196,11 +203,13 @@ True False + 4 True False Choose a color theme for syntax highlighting + 4 Highlight style @@ -214,6 +223,8 @@ True False Choose a color theme for syntax highlighting + 8 + 8 0 0 @@ -269,13 +280,40 @@ True False + 4 + 4 12 - + True False - 5 - 5 + + + True + False + Choose a bibliography file + File + + + False + True + 1 + + + + + True + False + Choose a bibliography file + 8 + 8 + + + True + True + 2 + + @@ -284,7 +322,7 @@ True False - <b>Bibliography File</b> + <b>Bibliography </b> True @@ -318,8 +356,8 @@ True False - 5 - 5 + 4 + 4 vertical True @@ -366,6 +404,7 @@ True False Choose a CSS File that you want to use + 4 CSS File @@ -379,6 +418,8 @@ True False Choose a CSS File that you want to use + 8 + 8 True From 351f7834a566d9eb124ad54aa6e5c21569733eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 02:45:23 +0100 Subject: [PATCH 22/54] Remove ODT as default export option Subjective, but a sub-window with 6 buttons on the top row is crowdy. --- data/ui/Export.ui | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/data/ui/Export.ui b/data/ui/Export.ui index 9c19871..3ea4419 100644 --- a/data/ui/Export.ui +++ b/data/ui/Export.ui @@ -587,6 +587,19 @@ True False crossfade + + + True + False + save + html_filter + + + html + HTML + 2 + + True @@ -610,32 +623,6 @@ 1 - - - True - False - save - html_filter - - - html - HTML - 2 - - - - - True - False - save - odt_filter - - - odt - ODT - 3 - - True From ce4a13278df863674eb6d3af9fd881099db986a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 14 Apr 2019 03:03:44 +0100 Subject: [PATCH 23/54] Organize export formats by type, more copy fixes --- uberwriter/export_dialog.py | 123 +++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index c652a3f..8d176f2 100644 --- a/uberwriter/export_dialog.py +++ b/uberwriter/export_dialog.py @@ -42,29 +42,59 @@ class Export: formats = [ { - "name": "LaTeX Source", - "ext": "tex", - "to": "latex" - }, - { - "name": "LaTeX PDF", + "name": "LaTeX (pdf)", "ext": "pdf", "to": "pdf" }, { - "name": "LaTeX beamer slide show Source .tex", - "ext": "tex", - "to": "beamer" - }, - { - "name": "LaTeX beamer slide show PDF", + "name": "LaTeX Beamer Slideshow (pdf)", "ext": "pdf", "to": "beamer" }, + { + "name": "LaTeX (tex)", + "ext": "tex", + "to": "latex" + }, + { + "name": "LaTeX Beamer Slideshow (tex)", + "ext": "tex", + "to": "beamer" + }, + { + "name": "ConTeXt", + "ext": "tex", + "to": "context" + }, { "name": "HTML", "ext": "html", - "to": "html5" + "to": "html" + }, + { + "name": "HTML and JavaScript Slideshow (Slidy)", + "ext": "html", + "to": "slidy" + }, + { + "name": "HTML and JavaScript Slideshow (Slideous)", + "ext": "html", + "to": "slideous" + }, + { + "name": "HTML5 and JavaScript Slideshow (DZSlides)", + "ext": "html", + "to": "dzslides" + }, + { + "name": "HTML5 and JavaScript Slideshow (reveal.js)", + "ext": "html", + "to": "revealjs" + }, + { + "name": "HTML and JavaScript Slideshow (S5)", + "ext": "html", + "to": "s5" }, { "name": "Textile", @@ -72,74 +102,49 @@ class Export: "to": "textile" }, { - "name": "OpenOffice text document", - "ext": "odt", - "to": "odt" - }, - { - "name": "Word docx", - "ext": "docx", - "to": "docx" - }, - { - "name": "reStructuredText txt", + "name": "reStructuredText", "ext": "txt", "to": "rst" }, { - "name": "ConTeXt tex", - "ext": "tex", - "to": "context" - }, - { - "name": "groff man", - "ext": "man", - "to": "man" - }, - { - "name": "MediaWiki markup", + "name": "MediaWiki Markup", "ext": "txt", "to": "mediawiki" }, { - "name": "OpenDocument XML", + "name": "OpenDocument (xml)", "ext": "xml", "to": "opendocument" }, { - "name": "OpenDocument XML", + "name": "OpenDocument (texi)", "ext": "texi", "to": "texinfo" }, { - "name": "Slidy HTML and javascript slide show", - "ext": "html", - "to": "slidy" + "name": "OpenOffice Text Document", + "ext": "odt", + "to": "odt" }, { - "name": "Slideous HTML and javascript slide show", - "ext": "html", - "to": "slideous" + "name": "Microsoft Word (docx)", + "ext": "docx", + "to": "docx" }, { - "name": "HTML5 + javascript slide show", - "ext": "html", - "to": "dzslides" - }, - { - "name": "S5 HTML and javascript slide show", - "ext": "html", - "to": "s5" - }, - { - "name": "EPub electronic publication", - "ext": "epub", - "to": "epub" - }, - { - "name": "RTF Rich Text Format", + "name": "Rich Text Format", "ext": "rtf", "to": "rtf" + }, + { + "name": "Groff Man", + "ext": "man", + "to": "man" + }, + { + "name": "EPUB v3", + "ext": "epub", + "to": "epub" } ] From 9f41bfac8d3f7f03b9b0e3e180c6634c5a708fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Mon, 15 Apr 2019 02:35:24 +0100 Subject: [PATCH 24/54] Add input format setting Supports Pandoc's Markdown, Commonmark, GitHub Flavored Markdown, MultiMarkdown and Plain Markdown. Fixes #17 Fixes #122 --- data/de.wolfvollprecht.UberWriter.gschema.xml | 11 +- data/ui/Menu.ui | 16 +- data/ui/Preferences.ui | 169 +++++++----- uberwriter/application.py | 259 +++++++----------- uberwriter/helpers.py | 6 +- uberwriter/preferences_dialog.py | 120 +++++++- uberwriter/settings.py | 3 +- uberwriter/window.py | 30 +- 8 files changed, 352 insertions(+), 262 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 4d3f7ca..7a3b93b 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -20,19 +20,26 @@ true - Spellcheck + Check spelling while typing Enable or disable spellchecking. false - Show gradient overlay + Draw scroll gradient Show a gradient overlay over the text at the top anf bottom of the window. It can cause performance problems to some users. + + "markdown" + Input format + + Input format to use when previewing and exporting using Pandoc. + + true Allow Uberwriter to poll cursor motion diff --git a/data/ui/Menu.ui b/data/ui/Menu.ui index e07092c..f7302dc 100644 --- a/data/ui/Menu.ui +++ b/data/ui/Menu.ui @@ -30,17 +30,7 @@ Copy HTML - app.HTML_copy - - -
- - Open Tutorial - app.open_examples - - - app.help - Pandoc _Help + app.copy_html
@@ -52,6 +42,10 @@ app.shortcuts _Keyboard Shortcuts + + Open Tutorial + app.open_tutorial + app.about _About UberWriter diff --git a/data/ui/Preferences.ui b/data/ui/Preferences.ui index 5b34d2b..f37e161 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -2,6 +2,11 @@ + + True + False + gtk-info + False True @@ -30,17 +35,17 @@ False center center - 30 - 30 - 30 - 30 - 10 - 10 + 16 + 16 + 16 + 16 + 8 + 8 - + True False - end + start Set dark mode automatically right @@ -50,10 +55,21 @@ - + + True + True + end + + + 2 + 0 + + + + True False - end + start Force dark mode right @@ -63,10 +79,21 @@ - + + True + True + end + + + 2 + 1 + + + + True False - end + start Check spelling while typing right @@ -76,10 +103,21 @@ - + + True + True + end + + + 2 + 2 + + + + True False - end + start Draw scroll gradient right @@ -89,65 +127,74 @@ - + True True - app.dark_mode_auto + end - 1 - 0 - - - - - True - True - app.dark_mode - - - 1 - 1 - - - - - True - True - app.spellcheck - - - 1 - 2 - - - - - True - True - start - app.draw_gradient - - - 1 + 2 3 - - - True - - - - - True - False - page 1 + + + True + False + start + Input format + right + + + 0 + 4 + + + + + True + True + False + help + + + 1 + 4 + + + + + True + True + end + 0 + 0 + + + 2 + 4 + + + + + + + + + + + + + + False + + + diff --git a/uberwriter/application.py b/uberwriter/application.py index 67f0e85..39038c0 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -11,17 +11,14 @@ # with this program. If not, see <http://www.gnu.org/licenses/>. import argparse -import webbrowser from gettext import gettext as _ import gi - gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position -from gi.repository import GLib, Gio, Gtk, Gdk, GdkPixbuf +from gi.repository import GLib, Gio, Gtk, GdkPixbuf from uberwriter import window -from uberwriter.theme import Theme from uberwriter.settings import Settings from uberwriter.helpers import set_up_logging from uberwriter.preferences_dialog import PreferencesDialog @@ -36,80 +33,12 @@ class Application(Gtk.Application): **kwargs) self.window = None self.settings = Settings.new() - self.dark_mode_action = None def do_startup(self, *args, **kwargs): Gtk.Application.do_startup(self) - # Actions - - action = Gio.SimpleAction.new("help", None) - action.connect("activate", self.on_help) - self.add_action(action) - - action = Gio.SimpleAction.new("shortcuts", None) - action.connect("activate", self.on_shortcuts) - self.add_action(action) - - action = Gio.SimpleAction.new("about", None) - action.connect("activate", self.on_about) - self.add_action(action) - - action = Gio.SimpleAction.new("quit", None) - action.connect("activate", self.on_quit) - self.add_action(action) - - dark_mode_auto = self.settings.get_value("dark-mode-auto") - action = Gio.SimpleAction.new_stateful("dark_mode_auto", None, - GLib.Variant.new_boolean(dark_mode_auto)) - action.connect("change-state", self.on_dark_mode_auto) - self.add_action(action) - - dark_mode = self.settings.get_value("dark-mode") - action = Gio.SimpleAction.new_stateful("dark_mode", - None, - GLib.Variant.new_boolean(dark_mode)) - action.connect("change-state", self.on_dark_mode) - self.add_action(action) - - action = Gio.SimpleAction.new_stateful("focus_mode", None, - GLib.Variant.new_boolean(False)) - action.connect("change-state", self.on_focus_mode) - self.add_action(action) - - action = Gio.SimpleAction.new_stateful("hemingway_mode", None, - GLib.Variant.new_boolean(False)) - action.connect("change-state", self.on_hemingway_mode) - self.add_action(action) - - action = Gio.SimpleAction.new_stateful("fullscreen", None, - GLib.Variant.new_boolean(False)) - action.connect("change-state", self.on_fullscreen) - self.add_action(action) - - action = Gio.SimpleAction.new_stateful("preview", None, - GLib.Variant.new_boolean(False)) - action.connect("change-state", self.on_preview) - self.add_action(action) - - action = Gio.SimpleAction.new("search", None) - action.connect("activate", self.on_search) - self.add_action(action) - - spellcheck = self.settings.get_value("spellcheck") - action = Gio.SimpleAction.new_stateful("spellcheck", None, - GLib.Variant.new_boolean(spellcheck)) - action.connect("change-state", self.on_spellcheck) - self.add_action(action) - - gradient_overlay = self.settings.get_value("gradient-overlay") - action = Gio.SimpleAction.new_stateful("draw_gradient", None, - GLib.Variant.new_boolean(gradient_overlay)) - action.connect("change-state", self.on_draw_gradient) - self.add_action(action) - - # Menu Actions + self.settings.connect("changed", self.on_settings_changed) action = Gio.SimpleAction.new("new", None) action.connect("activate", self.on_new) @@ -123,14 +52,34 @@ class Application(Gtk.Application): action.connect("activate", self.on_open_recent) self.add_action(action) - action = Gio.SimpleAction.new("open_examples", None) - action.connect("activate", self.on_example) - self.add_action(action) - action = Gio.SimpleAction.new("save", None) action.connect("activate", self.on_save) self.add_action(action) + action = Gio.SimpleAction.new("search", None) + action.connect("activate", self.on_search) + self.add_action(action) + + action = Gio.SimpleAction.new_stateful( + "focus_mode", None, GLib.Variant.new_boolean(False)) + action.connect("change-state", self.on_focus_mode) + self.add_action(action) + + action = Gio.SimpleAction.new_stateful( + "hemingway_mode", None, GLib.Variant.new_boolean(False)) + action.connect("change-state", self.on_hemingway_mode) + self.add_action(action) + + action = Gio.SimpleAction.new_stateful( + "preview", None, GLib.Variant.new_boolean(False)) + action.connect("change-state", self.on_preview) + self.add_action(action) + + action = Gio.SimpleAction.new_stateful( + "fullscreen", None, GLib.Variant.new_boolean(False)) + action.connect("change-state", self.on_fullscreen) + self.add_action(action) + action = Gio.SimpleAction.new("save_as", None) action.connect("activate", self.on_save_as) self.add_action(action) @@ -139,14 +88,30 @@ class Application(Gtk.Application): action.connect("activate", self.on_export) self.add_action(action) - action = Gio.SimpleAction.new("HTML_copy", None) - action.connect("activate", self.on_html_copy) + action = Gio.SimpleAction.new("copy_html", None) + action.connect("activate", self.on_copy_html) self.add_action(action) action = Gio.SimpleAction.new("preferences", None) action.connect("activate", self.on_preferences) self.add_action(action) + action = Gio.SimpleAction.new("shortcuts", None) + action.connect("activate", self.on_shortcuts) + self.add_action(action) + + action = Gio.SimpleAction.new("open_tutorial", None) + action.connect("activate", self.on_open_tutorial) + self.add_action(action) + + action = Gio.SimpleAction.new("about", None) + action.connect("activate", self.on_about) + self.add_action(action) + + action = Gio.SimpleAction.new("quit", None) + action.connect("activate", self.on_quit) + self.add_action(action) + # Shortcuts # TODO: be aware that a couple of shortcuts are defined in _gtk_base.css @@ -184,8 +149,7 @@ class Application(Gtk.Application): help=_("Show debug messages (-vv debugs uberwriter also)")) parser.add_argument( "-e", "--experimental-features", help=_("Use experimental features"), - action='store_true' - ) + action='store_true') (self.options, self.args) = parser.parse_known_args() set_up_logging(self.options) @@ -193,82 +157,15 @@ class Application(Gtk.Application): self.activate() return 0 - def on_about(self, _action, _param): - builder = get_builder('About') - about_dialog = builder.get_object("AboutDialog") - about_dialog.set_transient_for(self.window) - - logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg") - logo = GdkPixbuf.Pixbuf.new_from_file(logo_file) - - about_dialog.set_logo(logo) - - about_dialog.present() - - def on_help(self, _action, _param): - """open pandoc markdown web - """ - webbrowser.open("https://pandoc.org/MANUAL.html#pandocs-markdown") - - def on_shortcuts(self, _action, _param): - builder = get_builder('Shortcuts') - builder.get_object("shortcuts").set_transient_for(self.window) - builder.get_object("shortcuts").show() - - def on_dark_mode_auto(self, action, value, update_dark_mode_auto=True): - action.set_state(value) - self.settings.set_value("dark-mode-auto", GLib.Variant("b", value)) - - if update_dark_mode_auto: - self.on_dark_mode(self.lookup_action("dark_mode"), - GLib.Variant.new_boolean(not value.get_boolean()), - False) - - def on_dark_mode(self, action, value, update_dark_mode_auto=True): - action.set_state(value) - self.settings.set_value("dark-mode", GLib.Variant("b", value)) - - if update_dark_mode_auto: - self.on_dark_mode_auto(self.lookup_action("dark_mode_auto"), - GLib.Variant.new_boolean(not value.get_boolean()), - False) - - self.window.apply_current_theme() - - def on_focus_mode(self, action, value): - action.set_state(value) - self.window.set_focus_mode(value) - - def on_hemingway_mode(self, action, value): - action.set_state(value) - self.window.set_hemingway_mode(value) - - def on_fullscreen(self, action, value): - action.set_state(value) - self.window.set_fullscreen(value) - - def on_preview(self, action, value): - action.set_state(value) - self.window.toggle_preview(value) - - def on_search(self, _action, _value): - self.window.open_search_and_replace() - - def on_spellcheck(self, action, value): - action.set_state(value) - self.settings.set_value("spellcheck", - GLib.Variant("b", value)) - self.window.toggle_spellcheck(value) - - def on_draw_gradient(self, action, value): - action.set_state(value) - self.settings.set_value("gradient-overlay", - GLib.Variant("b", value)) - if value: - self.window.overlay = self.window.scrolled_window.connect_after( - "draw", self.window.draw_gradient) - else: - self.window.scrolled_window.disconnect(self.window.overlay) + def on_settings_changed(self, settings, key): + if key == "dark-mode-auto" or key == "dark-mode": + self.window.apply_current_theme() + elif key == "spellcheck": + self.window.toggle_spellcheck(settings.get_value(key)) + elif key == "gradient-overlay": + self.window.toggle_gradient_overlay(settings.get_value(key)) + elif key == "input-format": + self.window.reload_preview() def on_new(self, _action, _value): self.window.new_document() @@ -279,26 +176,58 @@ class Application(Gtk.Application): def on_open_recent(self, file): self.window.load_file(file.get_current_uri()) - def on_example(self, _action, _value): - self.window.open_uberwriter_markdown() - def on_save(self, _action, _value): self.window.save_document() + def on_search(self, _action, _value): + self.window.open_search_and_replace() + + def on_focus_mode(self, action, value): + action.set_state(value) + self.window.set_focus_mode(value) + + def on_hemingway_mode(self, action, value): + action.set_state(value) + self.window.set_hemingway_mode(value) + + def on_preview(self, action, value): + action.set_state(value) + self.window.toggle_preview(value) + + def on_fullscreen(self, action, value): + action.set_state(value) + self.window.set_fullscreen(value) + def on_save_as(self, _action, _value): self.window.save_document_as() def on_export(self, _action, _value): self.window.open_advanced_export() - def on_html_copy(self, _action, _value): + def on_copy_html(self, _action, _value): self.window.copy_html_to_clipboard() def on_preferences(self, _action, _value): - preferences_window = PreferencesDialog() - preferences_window.set_application(self) - preferences_window.set_transient_for(self.window) - preferences_window.show() + PreferencesDialog(self.settings).show(self.window) + + def on_shortcuts(self, _action, _param): + builder = get_builder('Shortcuts') + builder.get_object("shortcuts").set_transient_for(self.window) + builder.get_object("shortcuts").show() + + def on_open_tutorial(self, _action, _value): + self.window.open_uberwriter_markdown() + + def on_about(self, _action, _param): + builder = get_builder('About') + about_dialog = builder.get_object("AboutDialog") + about_dialog.set_transient_for(self.window) + + logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg") + logo = GdkPixbuf.Pixbuf.new_from_file(logo_file) + + about_dialog.set_logo(logo) + about_dialog.present() def on_quit(self, _action, _param): self.quit() diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py index 45454db..9272c2b 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -21,11 +21,12 @@ import logging import os import shutil - import gi import pypandoc from gi.overrides.Pango import Pango +from uberwriter.settings import Settings + gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: disable=E0611 @@ -204,6 +205,7 @@ def get_char_width(widget): widget.get_pango_context().get_metrics().get_approximate_char_width()) -def pandoc_convert(text, fr="markdown", to="html5", args=[], outputfile=None): +def pandoc_convert(text, to="html5", args=[], outputfile=None): + fr = Settings.new().get_value('input-format').get_string() or "markdown" args.extend(["--quiet"]) return pypandoc.convert_text(text, to, fr, extra_args=args, outputfile=outputfile) diff --git a/uberwriter/preferences_dialog.py b/uberwriter/preferences_dialog.py index 937dcf4..fffda4b 100644 --- a/uberwriter/preferences_dialog.py +++ b/uberwriter/preferences_dialog.py @@ -18,23 +18,119 @@ """this dialog adjusts values in gsettings """ +import webbrowser + import gi + gi.require_version('Gtk', '3.0') -from gi.repository import Gtk # pylint: disable=E0611 +from gi.repository import Gtk, Pango, GLib # pylint: disable=E0611 import logging logger = logging.getLogger('uberwriter') -from uberwriter.helpers import get_builder, show_uri, get_help_uri +from uberwriter.helpers import get_builder + + +class PreferencesDialog: -class PreferencesDialog(Gtk.Window): __gtype_name__ = "PreferencesDialog" - def __new__(cls): - """Special static method that's automatically called by Python when - constructing a new instance of this class. - - Returns a fully instantiated PreferencesDialog object. - """ - builder = get_builder('Preferences') - new_object = builder.get_object("PreferencesWindow") - return new_object + formats = [ + { + "name": "Pandoc's Markdown", + "format": "markdown", + "help": "https://pandoc.org/MANUAL.html#pandocs-markdown" + }, + { + "name": "CommonMark", + "format": "commonmark", + "help": "https://commonmark.org" + }, + { + "name": "GitHub Flavored Markdown", + "format": "gfm", + "help": "https://help.github.com/en/categories/writing-on-github" + }, + { + "name": "MultiMarkdown", + "format": "markdown_mmd", + "help": "https://fletcherpenney.net/multimarkdown" + }, + { + "name": "Plain Markdown", + "format": "markdown_strict", + "help": "https://daringfireball.net/projects/markdown" + } + ] + + def __init__(self, settings): + self.settings = settings + self.builder = get_builder("Preferences") + + self.dark_mode_auto_switch = self.builder.get_object("dark_mode_auto_switch") + self.dark_mode_auto_switch.set_active(self.settings.get_value("dark-mode-auto")) + self.dark_mode_auto_switch.connect("state-set", self.on_dark_mode_auto) + + self.dark_mode_switch = self.builder.get_object("dark_mode_switch") + self.dark_mode_switch.set_active(self.settings.get_value("dark-mode")) + self.dark_mode_switch.connect("state-set", self.on_dark_mode) + + self.spellcheck_switch = self.builder.get_object("spellcheck_switch") + self.spellcheck_switch.set_active(self.settings.get_value("spellcheck")) + self.spellcheck_switch.connect("state-set", self.on_spellcheck) + + self.gradient_overlay_switch = self.builder.get_object("gradient_overlay_switch") + self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay")) + self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay) + + input_format_store = Gtk.ListStore(int, str) + input_format = self.settings.get_value("input-format").get_string() + input_format_active = 0 + for i, fmt in enumerate(self.formats): + input_format_store.append([i, fmt["name"]]) + if fmt["format"] == input_format: + input_format_active = i + self.input_format_combobox = self.builder.get_object("input_format_combobox") + self.input_format_combobox.set_model(input_format_store) + input_format_renderer = Gtk.CellRendererText() + self.input_format_combobox.pack_start(input_format_renderer, True) + self.input_format_combobox.add_attribute(input_format_renderer, "text", 1) + self.input_format_combobox.set_active(input_format_active) + self.input_format_combobox.connect("changed", self.on_input_format) + + self.input_format_help_button = self.builder.get_object("input_format_help_button") + self.input_format_help_button.connect('clicked', self.on_input_format_help) + + def show(self, window): + preferences_window = self.builder.get_object("PreferencesWindow") + preferences_window.set_application(window.get_application()) + preferences_window.set_transient_for(window) + preferences_window.show() + + def on_dark_mode_auto(self, _, state): + self.settings.set_value("dark-mode-auto", GLib.Variant.new_boolean(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)) + 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)) + return False + + def on_gradient_overlay(self, _, state): + self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(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"])) + + def on_input_format_help(self, _): + fmt = self.formats[self.input_format_combobox.get_active()] + webbrowser.open(fmt["help"]) + diff --git a/uberwriter/settings.py b/uberwriter/settings.py index 58c44b0..08bfe2b 100644 --- a/uberwriter/settings.py +++ b/uberwriter/settings.py @@ -27,7 +27,8 @@ class Settings(Gio.Settings): """ Gio.Settings.__init__(self) - def new(): + @classmethod + def new(cls): """ Return a new Settings object """ diff --git a/uberwriter/window.py b/uberwriter/window.py index b47565f..a42c24c 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -132,9 +132,8 @@ class Window(Gtk.ApplicationWindow): # some people seems to have performance problems with the overlay. # Let them disable it - - if self.settings.get_value("gradient-overlay"): - self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient) + self.overlay_id = None + self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() @@ -186,8 +185,7 @@ class Window(Gtk.ApplicationWindow): Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # Reload preview if it exists - if self.preview_webview: - self.show_preview() + self.reload_preview() # Redraw contents of window self.queue_draw() @@ -484,14 +482,14 @@ class Window(Gtk.ApplicationWindow): """ self.sidebar.toggle_sidebar() - def toggle_spellcheck(self, status): + def toggle_spellcheck(self, state): """Enable/disable the autospellchecking Arguments: status {gtk bool} -- Desired status of the spellchecking """ - if status.get_boolean(): + if state.get_boolean(): try: self.spell_checker.enable() except: @@ -525,6 +523,18 @@ class Window(Gtk.ApplicationWindow): pass return + def toggle_gradient_overlay(self, state): + """Toggle the gradient overlay + + Arguments: + state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled) + """ + + if state.get_boolean(): + self.overlay_id = self.scrolled_window.connect_after("draw", self.draw_gradient) + elif self.overlay_id: + self.scrolled_window.disconnect(self.overlay_id) + def toggle_preview(self, state): """Toggle the preview mode @@ -572,7 +582,11 @@ class Window(Gtk.ApplicationWindow): # but local files are opened in appropriate apps: self.preview_webview.connect("decide-policy", self.on_click_link) - self.preview_webview.load_html(output.decode("utf-8"), 'file://localhost/') + self.preview_webview.load_html(output, 'file://localhost/') + + def reload_preview(self): + if self.preview_webview: + self.show_preview() def load_file(self, filename=None): """Open File from command line or open / open recent etc.""" From 41377c24bb60ac06365cb719a3f2c1947bd9aafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Mon, 15 Apr 2019 03:01:59 +0100 Subject: [PATCH 25/54] Further remove unecessary prefixes --- data/ui/{UberwriterWindow.ui => Window.ui} | 0 uberwriter/__init__.py | 2 +- uberwriter/{uberwriterconfig.py => config.py} | 0 uberwriter/helpers.py | 2 +- uberwriter/sidebar.py | 2 +- uberwriter/window.py | 4 ++-- 6 files changed, 5 insertions(+), 5 deletions(-) rename data/ui/{UberwriterWindow.ui => Window.ui} (100%) rename uberwriter/{uberwriterconfig.py => config.py} (100%) diff --git a/data/ui/UberwriterWindow.ui b/data/ui/Window.ui similarity index 100% rename from data/ui/UberwriterWindow.ui rename to data/ui/Window.ui diff --git a/uberwriter/__init__.py b/uberwriter/__init__.py index 20bd475..8632281 100644 --- a/uberwriter/__init__.py +++ b/uberwriter/__init__.py @@ -22,7 +22,7 @@ gi.require_version('Gtk', '3.0') from uberwriter import window from uberwriter import application from uberwriter.helpers import set_up_logging -from uberwriter.uberwriterconfig import get_version +from uberwriter.config import get_version def main(): diff --git a/uberwriter/uberwriterconfig.py b/uberwriter/config.py similarity index 100% rename from uberwriter/uberwriterconfig.py rename to uberwriter/config.py diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py index 9272c2b..e7f2c03 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -30,7 +30,7 @@ from uberwriter.settings import Settings gi.require_version('Gtk', '3.0') from gi.repository import Gtk # pylint: disable=E0611 -from uberwriter.uberwriterconfig import get_data_file +from uberwriter.config import get_data_file from uberwriter.builder import Builder diff --git a/uberwriter/sidebar.py b/uberwriter/sidebar.py index ddc8536..16b7104 100644 --- a/uberwriter/sidebar.py +++ b/uberwriter/sidebar.py @@ -58,7 +58,7 @@ class Sidebar(): Presentational class for shelves and files managed by the "sidebar" parentwindow: - Reference to UberwriterWindow + Reference to Window """ def __init__(self, parentwindow): """ diff --git a/uberwriter/window.py b/uberwriter/window.py index a42c24c..5575cb7 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -76,7 +76,7 @@ class Window(Gtk.ApplicationWindow): title="Uberwriter") # Set UI - self.builder = get_builder('UberwriterWindow') + self.builder = get_builder('Window') root = self.builder.get_object("FullscreenOverlay") root.connect('style-updated', self.apply_current_theme) self.add(root) @@ -742,7 +742,7 @@ class Window(Gtk.ApplicationWindow): return False def on_mnu_close_activate(self, _widget, _data=None): - """Signal handler for closing the UberwriterWindow. + """Signal handler for closing the Window. Overriden from parent Window Class """ if self.on_delete_called(self): # Really destroy? From c9958b6c12cf6100d1365ac197271baee54f2c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Tue, 16 Apr 2019 01:15:11 +0100 Subject: [PATCH 26/54] Prevent exception when exiting while waiting for scroll --- uberwriter/text_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 3d6ca2e..df945df 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -161,6 +161,8 @@ class TextView(Gtk.TextView): margin = 80 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: return From 8159ad9e25a9432371daad5382e2bbb276ec97ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 01:03:37 +0100 Subject: [PATCH 27/54] Tweak breakpoints to ensure it conforms to the documented spec --- uberwriter/text_view_markup_handler.py | 16 ++++++---------- uberwriter/window.py | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py index 374445e..029871a 100644 --- a/uberwriter/text_view_markup_handler.py +++ b/uberwriter/text_view_markup_handler.py @@ -93,7 +93,6 @@ class MarkupHandler: # Margin and indents # A baseline margin is set to allow negative offsets for formatting headers, lists, etc - self.baseline_margin = 0 self.margins_indents = {} self.update_margins_indents() @@ -283,14 +282,17 @@ class MarkupHandler: else: return self.margins_indents[level] - def get_margin_indent(self, margin_level, indent_level, char_width=None): + 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() if char_width is None: char_width = helpers.get_char_width(self.text_view) - margin = max(self.baseline_margin + char_width * margin_level, 0) + margin = max(baseline_margin + char_width * margin_level, 0) indent = char_width * indent_level return margin, indent def update_margins_indents(self): + baseline_margin = self.text_view.get_left_margin() char_width = helpers.get_char_width(self.text_view) # Adjust tab size, as character width can change @@ -298,14 +300,8 @@ class MarkupHandler: tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width) self.text_view.set_tabs(tab_array) - # Adjust baseline margin, as character width can change - # Baseline needs to account for - self.baseline_margin = char_width * 10 - self.text_view.set_left_margin(self.baseline_margin) - self.text_view.set_right_margin(self.baseline_margin) - # Adjust margins and indents, as character width can change for level, tag in self.margins_indents.items(): - margin, indent = self.get_margin_indent(*level, char_width) + margin, indent = self.get_margin_indent(*level, baseline_margin, char_width) tag.set_property("left-margin", margin) tag.set_property("indent", indent) diff --git a/uberwriter/window.py b/uberwriter/window.py index 5575cb7..f541773 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -258,7 +258,7 @@ class Window(Gtk.ApplicationWindow): self.text_view.set_hemingway_mode(state.get_boolean()) self.text_view.grab_focus() - def window_resize(self, window, _data=None): + def window_resize(self, window, event=None): """set paddings dependant of the window size """ @@ -266,24 +266,33 @@ class Window(Gtk.ApplicationWindow): # - 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 - w_width = window.get_allocation().width + # + # 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: - width_request = 700 # ~66 characters + font_size = 14 self.get_style_context().add_class("small") self.get_style_context().remove_class("large") - elif w_width < 1200: - width_request = 870 # ~66 characters + elif w_width < 1280: + font_size = 16 self.get_style_context().remove_class("small") self.get_style_context().remove_class("large") else: - width_request = 830 # ~66 characters + font_size = 18 self.get_style_context().remove_class("small") self.get_style_context().add_class("large") + 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 # TODO: refactorizable From d98208e0e90d41e222e40d3bbf40727346f83929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 02:10:19 +0100 Subject: [PATCH 28/54] Cleanup left-over from removing ODT as a default export option --- uberwriter/export_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index 8d176f2..d65b583 100644 --- a/uberwriter/export_dialog.py +++ b/uberwriter/export_dialog.py @@ -159,7 +159,7 @@ class Export: filename = filename or _("Untitled document.md") self.filechoosers = {export_format: self.stack.get_child_by_name(export_format) - for export_format in ["pdf", "html", "odt", "advanced"]} + for export_format in ["pdf", "html", "advanced"]} for export_format, filechooser in self.filechoosers.items(): filechooser.set_do_overwrite_confirmation(True) filechooser.set_current_folder(os.path.dirname(filename)) From fc824fc2efe4888c939e42fd765797b87eacfca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 02:13:00 +0100 Subject: [PATCH 29/54] Add missing tooltip for search's regular expression option --- data/ui/Window.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/data/ui/Window.ui b/data/ui/Window.ui index b30c922..55db4ac 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -328,6 +328,7 @@ True True True + Regular Expression False From 567e74c99ea90e16cf5c2046bd56b60c0cd66a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 02:15:59 +0100 Subject: [PATCH 30/54] Fix active search going out of bounds --- uberwriter/search_and_replace.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index f137707..24fb1d9 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -143,10 +143,7 @@ class SearchAndReplace: def scrollto(self, index): if not self.matches: return - if index < len(self.matches): - self.active = index - else: - self.active = 0 + self.active = index % len(self.matches) match = self.matches[self.active] start_iter = self.textbuffer.get_iter_at_offset(match[0]) From 53d3fc6026daf814e25a05d7e12f51ae555cf240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 02:52:56 +0100 Subject: [PATCH 31/54] Use symbolic icon for input format documentation --- data/ui/Preferences.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/ui/Preferences.ui b/data/ui/Preferences.ui index f37e161..b057517 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -5,7 +5,7 @@ True False - gtk-info + dialog-information-symbolic False From 432ef9d55e4c3b3fb2345a8ad4b016de3427b656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 02:54:55 +0100 Subject: [PATCH 32/54] Set tooltips on the whole button, not just the icon --- data/ui/Window.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/ui/Window.ui b/data/ui/Window.ui index 55db4ac..66571fe 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -16,7 +16,6 @@ True False - Next Match go-down-symbolic @@ -33,7 +32,6 @@ True False - Open Replace edit-find-replace-symbolic @@ -283,6 +281,7 @@ True True True + Next Match avall @@ -341,6 +340,7 @@ True True True + Open Replace reemplaza From 83c15361ea8c13eb2181844e2681b01008f172a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Wed, 17 Apr 2019 21:34:49 +0200 Subject: [PATCH 33/54] v2.2.0-beta1 install scripts --- .gitignore | 1 - bin/uberwriter | 59 +++++++++++++++++++ data/de.wolfvollprecht.UberWriter.appdata.xml | 7 +++ setup.py | 8 +-- 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100755 bin/uberwriter diff --git a/.gitignore b/.gitignore index d5e4595..7575b05 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ build/ debian/uberwriter/DEBIAN debian/uberwriter/opt debian/uberwriter/usr -bin/ flatpak/* !flatpak/fonts-download !flatpak/pandoc-download diff --git a/bin/uberwriter b/bin/uberwriter new file mode 100755 index 0000000..69fcd8b --- /dev/null +++ b/bin/uberwriter @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# Copyright (C) 2012, Wolf Vollprecht +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +import sys +import os + +import pkg_resources + +import gettext +import locale + +# Add project root directory (enable symlink and trunk execution) +PROJECT_ROOT_DIRECTORY = os.path.abspath( + os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))) + +# Set the path if needed. This allows uberwriter to run without installing it :) +python_path = [] +if os.path.abspath(__file__).startswith('/opt'): + gettext.bindtextdomain('uberwriter', '/opt/extras.ubuntu.com/uberwriter/share/locale') + syspath = sys.path[:] # copy to avoid infinite loop in pending objects + for path in syspath: + opt_path = path.replace('/usr', '/opt/extras.ubuntu.com/uberwriter') + python_path.insert(0, opt_path) + sys.path.insert(0, opt_path) + os.putenv("XDG_DATA_DIRS", "%s:%s" % ("/opt/extras.ubuntu.com/uberwriter/share/", os.getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/"))) +if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'uberwriter')) + and PROJECT_ROOT_DIRECTORY not in sys.path): + python_path.insert(0, PROJECT_ROOT_DIRECTORY) + sys.path.insert(0, PROJECT_ROOT_DIRECTORY) +if python_path: + os.putenv('PYTHONPATH', "%s:%s" % (os.getenv('PYTHONPATH', ''), ':'.join(python_path))) # for subprocesses + +import uberwriter + +locale_dir = os.path.abspath(os.path.join(os.path.dirname(uberwriter.__file__),'../po/')) + +# L10n +locale.textdomain('uberwriter') +locale.bindtextdomain('uberwriter', locale_dir) +gettext.textdomain('uberwriter') +gettext.bindtextdomain('uberwriter', locale_dir) + +uberwriter.main() diff --git a/data/de.wolfvollprecht.UberWriter.appdata.xml b/data/de.wolfvollprecht.UberWriter.appdata.xml index ac95924..fa00397 100644 --- a/data/de.wolfvollprecht.UberWriter.appdata.xml +++ b/data/de.wolfvollprecht.UberWriter.appdata.xml @@ -28,6 +28,13 @@ + + +
    +
  • ...
  • +
+
+
    diff --git a/setup.py b/setup.py index 8671284..f9d08d3 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ else: setup( name='uberwriter', - version='2.1.4', + version='2.2.0-beta1', license='GPL-3', author='Wolf Vollprecht', author_email='w.vollprecht@gmail.com', @@ -64,7 +64,6 @@ setup( "uberwriter.pylocales", # "uberwriter.pressagio", "uberwriter", - "uberwriter", "po" # "uberwriter.plugins" # "uberwriter.plugins.bibtex" @@ -75,10 +74,11 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['bin/uberwriter']), + (app_prefix + 'bin', ['uberwriter.in']), (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), - (app_prefix + 'share/applications', ['de.wolfvollprecht.UberWriter.desktop']), + (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), + (app_prefix + 'share/applications', ['/data/de.wolfvollprecht.UberWriter.desktop']), (app_prefix + 'share/uberwriter/data/ui', extra_files_ui), (app_prefix + 'share/uberwriter/data/media', extra_files_media), (app_prefix + 'share/uberwriter/data/lua', extra_files_scripts) From 6baa3989957523f6fcded89007d5d5c8921916e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Wed, 17 Apr 2019 21:57:51 +0200 Subject: [PATCH 34/54] fix binary path --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9d08d3..e3e5316 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['uberwriter.in']), + (app_prefix + 'bin', ['/bin/uberwriter']), (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), From f67eaedcb62f072921a0f9c6f72adb80aac7779b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Wed, 17 Apr 2019 21:57:51 +0200 Subject: [PATCH 35/54] fix binary path --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9d08d3..d7a19e8 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['uberwriter.in']), + (app_prefix + 'bin', ['bin/uberwriter']), (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), From d99e4c65f9eefb0ee3639f22e4fa789b24731da1 Mon Sep 17 00:00:00 2001 From: somas95 Date: Wed, 17 Apr 2019 22:17:42 +0200 Subject: [PATCH 36/54] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3e5316..d7a19e8 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['/bin/uberwriter']), + (app_prefix + 'bin', ['bin/uberwriter']), (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), From 21e27eaaf01baba5cb8696a1496f9a51140c8139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Wed, 17 Apr 2019 22:27:06 +0200 Subject: [PATCH 37/54] fix paths --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7a19e8..f88cee2 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ setup( (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), - (app_prefix + 'share/applications', ['/data/de.wolfvollprecht.UberWriter.desktop']), + (app_prefix + 'share/applications', ['data/de.wolfvollprecht.UberWriter.desktop']), (app_prefix + 'share/uberwriter/data/ui', extra_files_ui), (app_prefix + 'share/uberwriter/data/media', extra_files_media), (app_prefix + 'share/uberwriter/data/lua', extra_files_scripts) From dfe7cc420e3e8eb87dd972db62d894145b2699c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 00:00:33 +0100 Subject: [PATCH 38/54] Fix opening recents popover affecting font size --- uberwriter/window.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/uberwriter/window.py b/uberwriter/window.py index f541773..7abdca8 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -262,6 +262,11 @@ class Window(Gtk.ApplicationWindow): """set paddings dependant of the window size """ + # 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 From 1a7443fd3caf986583c4c5e1e3ad2b0f40ae9cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Wed, 17 Apr 2019 03:02:04 +0100 Subject: [PATCH 39/54] Switch to a more powerful and accurate stats counter The new stats counter is able to count characters, words, sentences, and reading time. It does so more accurately than before, by leveraging Pandoc's plain format, and a few simple regular expressions that besides accuracy, also improve support for Asian languages. It's all done on a background thread to avoid hogging the UI. --- uberwriter/stats_counter.py | 70 +++++++++++++++++++++++++++++++++++++ uberwriter/window.py | 49 +++++++++++--------------- 2 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 uberwriter/stats_counter.py diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py new file mode 100644 index 0000000..0f8fbd8 --- /dev/null +++ b/uberwriter/stats_counter.py @@ -0,0 +1,70 @@ +import math +import re +from queue import Queue +from threading import Thread + +from gi.repository import GLib + +from uberwriter import helpers + + +class StatsCounter: + """Counts characters, words, sentences and reading 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 Asian letters, general symbols and hieroglyphs, + # as well as sequences of word characters optionally containing non-word characters in-between. + WORDS = re.compile(r"[\u3040-\uffff]|(?:\w+\S?\w*)+", re.UNICODE) + + # Regexp that matches sentence-ending punctuation characters, ie. full stop, question mark, + # exclamation mark, paragraph, and variants. + SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+") + + def __init__(self): + super().__init__() + + self.queue = Queue() + worker = Thread(target=self.__do_count_stats, name="stats-counter") + worker.daemon = True + worker.start() + + def count_stats(self, text, callback): + """Count stats for text, calling callback with a result when done. + + The callback argument contains the result, in the form: + + (characters, words, sentences, (hours, minutes, seconds))""" + + self.queue.put((text, callback)) + + def stop(self): + """Stops the background worker. StatsCounter shouldn't be used after this.""" + + self.queue.put((None, None)) + + def __do_count_stats(self): + while True: + while True: + (text, callback) = self.queue.get() + if text is None and callback is None: + return + if self.queue.empty(): + break + + text = helpers.pandoc_convert(text, to="plain") + + character_count = len(re.findall(self.CHARACTERS, text)) + + word_count = len(re.findall(self.WORDS, text)) + + sentence_count = len(re.findall(self.SENTENCES, text)) + + dec_, int_ = math.modf(word_count / 200) + hours = int(int_ / 60) + minutes = int(int_ % 60) + seconds = round(dec_ * 0.6) + reading_time = (hours, minutes, seconds) + + GLib.idle_add(callback, (character_count, word_count, sentence_count, reading_time)) diff --git a/uberwriter/window.py b/uberwriter/window.py index 7abdca8..95c4fe1 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -26,6 +26,7 @@ from gettext import gettext as _ import gi from uberwriter.export_dialog import Export +from uberwriter.stats_counter import StatsCounter from uberwriter.text_view import TextView gi.require_version('Gtk', '3.0') @@ -66,8 +67,6 @@ class Window(Gtk.ApplicationWindow): 'close-window': (GObject.SIGNAL_ACTION, None, ()) } - WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE) - def __init__(self, app): """Set up the main window""" @@ -83,6 +82,8 @@ class Window(Gtk.ApplicationWindow): self.set_default_size(900, 500) + self.connect('delete-event', self.on_destroy) + # Preferences self.settings = Settings.new() @@ -130,6 +131,9 @@ class Window(Gtk.ApplicationWindow): self.scrolled_window.add(self.text_view) self.editor_viewport = self.builder.get_object('editor_viewport') + # Stats counter + self.stats_counter = StatsCounter() + # some people seems to have performance problems with the overlay. # Let them disable it self.overlay_id = None @@ -190,25 +194,13 @@ class Window(Gtk.ApplicationWindow): # Redraw contents of window self.queue_draw() - def update_line_and_char_count(self): - """it... it updates line and characters count + def update_stats_counts(self, stats): + """Updates line and character counts. """ - if self.status_bar_visible is False: - return - text = self.text_view.get_text() - self.char_count.set_text(str(len(text))) - words = re.split(self.WORDCOUNT, text) - length = len(words) - # Last word a "space" - if not words[-1]: - length = length - 1 - # First word a "space" (happens in focus mode...) - if not words[0]: - length = length - 1 - if length == -1: - length = 0 - self.word_count.set_text(str(length)) + (characters, words, sentences, (hours, minutes, seconds)) = stats + self.char_count.set_text(str(characters)) + self.word_count.set_text(str(words)) def on_text_changed(self, *_args): """called when the text changes, sets the self.did_change to true and @@ -221,7 +213,9 @@ class Window(Gtk.ApplicationWindow): self.set_headerbar_title("* " + title) self.buffer_modified_for_status_bar = True - self.update_line_and_char_count() + + if self.status_bar_visible: + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides @@ -703,7 +697,7 @@ class Window(Gtk.ApplicationWindow): self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True @@ -716,7 +710,7 @@ class Window(Gtk.ApplicationWindow): self.headerbar.hb.props.opacity = 1 self.status_bar_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() + self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the @@ -764,12 +758,6 @@ class Window(Gtk.ApplicationWindow): self.destroy() return - def on_destroy(self, _widget, _data=None): - """Called when the TexteditorWindow is closed. - """ - # Clean up code for saving application state should be added here. - Gtk.main_quit() - def set_headerbar_title(self, title): """set the desired headerbar title """ @@ -801,3 +789,8 @@ class Window(Gtk.ApplicationWindow): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up" + + def on_destroy(self, _widget, _data=None): + """Called when the Window is closing. + """ + self.stats_counter.stop() From e76b85e8371775351ff103bffc7c9ff755e78e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 11:21:37 +0100 Subject: [PATCH 40/54] Add official markdown mime type support in Recents --- data/ui/Recents.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/data/ui/Recents.ui b/data/ui/Recents.ui index cfe3ada..ae75e95 100644 --- a/data/ui/Recents.ui +++ b/data/ui/Recents.ui @@ -4,6 +4,7 @@ + text/markdown text/x-markdown From c19f57f64b0dd6e7d5f7f167da62998cf4cb5ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 06:24:43 +0100 Subject: [PATCH 41/54] Add statistics for sentences and read time, allow user to toggle default Fixes #63 --- data/de.wolfvollprecht.UberWriter.gschema.xml | 14 +++ data/media/css/_gtk_base.css | 41 ++----- data/ui/Window.ui | 91 ++-------------- uberwriter/application.py | 18 +++ uberwriter/stats_counter.py | 18 ++- uberwriter/stats_handler.py | 103 ++++++++++++++++++ uberwriter/window.py | 68 ++++-------- 7 files changed, 180 insertions(+), 173 deletions(-) create mode 100644 uberwriter/stats_handler.py diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 7a3b93b..6d2b13a 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -1,6 +1,13 @@ + + + + + + + @@ -54,6 +61,13 @@ Open file paths of the current session + + "words" + Default statistic + + Which statistic is shown on the main window. + + diff --git a/data/media/css/_gtk_base.css b/data/media/css/_gtk_base.css index 84261ab..b4fe25b 100644 --- a/data/media/css/_gtk_base.css +++ b/data/media/css/_gtk_base.css @@ -86,15 +86,10 @@ } -.status-bar-box label { - color: #666; -} - -.status-bar-box button { - /* finding reset */ +.stats-counter { + color: alpha(@foreground_color, 0.6); background-color: @background_color; text-shadow: inherit; - /*icon-shadow: inherit;*/ box-shadow: initial; background-clip: initial; background-origin: initial; @@ -106,37 +101,15 @@ border-image-repeat: initial; border-image-slice: initial; border-image-width: initial; - border-style: none; - -button-images: true; - border-radius: 2px; - color: #666; - padding: 3px 5px; + padding: 0px 16px; transition: 100ms ease-in; } -.status-bar-box button:hover, -.status-bar-box button:checked { - transition: 0s ease-in; - color: @background_color; - background-color: #666; -} - -.status-bar-box button:hover label, -.status-bar-box button:checked label { - color: @background_color; -} - -.status-bar-box button:active { - color: #EEE; - background-color: #EEE; - background-image: none; - box-shadow: 0 0 2px rgba(0,0,0,0.4) -} - -.status-bar-box separator { - border-color: #999; - border-right: none; +.stats-counter:hover, +.stats-counter:checked { + color: @foreground_color; + background-color: lighter(@background_color); } #PreviewMenuItem image { diff --git a/data/ui/Window.ui b/data/ui/Window.ui index 66571fe..bca48f4 100644 --- a/data/ui/Window.ui +++ b/data/ui/Window.ui @@ -75,97 +75,20 @@ False True - + True False crossfade 750 True - + + 0 Words True - False - - - True - False - end - True - Words: - - - 3 - 0 - - - - - True - False - end - 0 - right - 4 - 1 - - - 4 - 0 - - - - - True - False - end - 10 - 10 - 10 - 10 - vertical - - - 5 - 0 - - - - - True - False - end - Characters: - - - 6 - 0 - - - - - True - False - end - 11 - 11 - 0 - 6 - 1 - - - 7 - 0 - - - - - - - - - - - + True + True + Show Statistics + end diff --git a/uberwriter/application.py b/uberwriter/application.py index 39038c0..0ad632f 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -40,6 +40,8 @@ class Application(Gtk.Application): self.settings.connect("changed", self.on_settings_changed) + # Header bar + action = Gio.SimpleAction.new("new", None) action.connect("activate", self.on_new) self.add_action(action) @@ -60,6 +62,8 @@ class Application(Gtk.Application): action.connect("activate", self.on_search) self.add_action(action) + # App Menu + action = Gio.SimpleAction.new_stateful( "focus_mode", None, GLib.Variant.new_boolean(False)) action.connect("change-state", self.on_focus_mode) @@ -112,6 +116,14 @@ class Application(Gtk.Application): action.connect("activate", self.on_quit) self.add_action(action) + # Stats Menu + + 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)) + action.connect("activate", self.on_stat_default) + self.add_action(action) + # Shortcuts # TODO: be aware that a couple of shortcuts are defined in _gtk_base.css @@ -166,6 +178,8 @@ class Application(Gtk.Application): self.window.toggle_gradient_overlay(settings.get_value(key)) elif key == "input-format": self.window.reload_preview() + elif key == "stat-default": + self.window.update_default_stat() def on_new(self, _action, _value): self.window.new_document() @@ -232,6 +246,10 @@ class Application(Gtk.Application): def on_quit(self, _action, _param): self.quit() + def on_stat_default(self, action, value): + action.set_state(value) + self.settings.set_string("stat-default", value.get_string()) + # ~ if __name__ == "__main__": # ~ app = Application() # ~ app.run(sys.argv) diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py index 0f8fbd8..b753143 100644 --- a/uberwriter/stats_counter.py +++ b/uberwriter/stats_counter.py @@ -9,7 +9,7 @@ from uberwriter import helpers class StatsCounter: - """Counts characters, words, sentences and reading time using a background thread.""" + """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))") @@ -26,11 +26,11 @@ class StatsCounter: super().__init__() self.queue = Queue() - worker = Thread(target=self.__do_count_stats, name="stats-counter") + worker = Thread(target=self.__do_count, name="stats-counter") worker.daemon = True worker.start() - def count_stats(self, text, callback): + def count(self, text, callback): """Count stats for text, calling callback with a result when done. The callback argument contains the result, in the form: @@ -44,7 +44,7 @@ class StatsCounter: self.queue.put((None, None)) - def __do_count_stats(self): + def __do_count(self): while True: while True: (text, callback) = self.queue.get() @@ -61,10 +61,8 @@ class StatsCounter: sentence_count = len(re.findall(self.SENTENCES, text)) - dec_, int_ = math.modf(word_count / 200) - hours = int(int_ / 60) - minutes = int(int_ % 60) - seconds = round(dec_ * 0.6) - reading_time = (hours, minutes, seconds) + read_m, read_s = divmod(word_count / 200 * 60, 60) + read_h, read_m = divmod(read_m, 60) + read_time = (int(read_h), int(read_m), int(read_s)) - GLib.idle_add(callback, (character_count, word_count, sentence_count, reading_time)) + GLib.idle_add(callback, (character_count, word_count, sentence_count, read_time)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py new file mode 100644 index 0000000..ec9f40a --- /dev/null +++ b/uberwriter/stats_handler.py @@ -0,0 +1,103 @@ +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 + + +class StatsHandler: + """Shows a default statistic on the stats button, and allows the user to toggle which one.""" + + def __init__(self, stats_button, text_view): + super().__init__() + + self.stats_button = stats_button + self.stats_button.connect("clicked", self.on_stats_button_clicked) + self.stats_button.connect("destroy", self.on_destroy) + + self.text_view = text_view + self.text_view.get_buffer().connect("changed", self.on_text_changed) + + self.popover = None + + self.characters = 0 + self.words = 0 + self.sentences = 0 + self.read_time = (0, 0, 0) + + self.settings = Settings.new() + self.default_stat = self.settings.get_enum("stat-default") + + self.stats_counter = StatsCounter() + + self.update_default_stat() + + def on_stats_button_clicked(self, _button): + self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False) + + menu = Gio.Menu() + characters_menu_item = Gio.MenuItem.new(self.get_text_for_stat(0), None) + characters_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("characters")) + menu.append_item(characters_menu_item) + words_menu_item = Gio.MenuItem.new(self.get_text_for_stat(1), None) + words_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("words")) + menu.append_item(words_menu_item) + sentences_menu_item = Gio.MenuItem.new(self.get_text_for_stat(2), None) + sentences_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("sentences")) + menu.append_item(sentences_menu_item) + read_time_menu_item = Gio.MenuItem.new(self.get_text_for_stat(3), None) + read_time_menu_item.set_action_and_target_value( + "app.stat_default", GLib.Variant.new_string("read_time")) + menu.append_item(read_time_menu_item) + self.popover = Gtk.Popover.new_from_model(self.stats_button, menu) + self.popover.connect('closed', self.on_popover_closed) + self.popover.popup() + + def on_popover_closed(self, _popover): + self.stats_button.unset_state_flags(Gtk.StateFlags.CHECKED) + + self.popover = None + self.text_view.grab_focus() + + def on_text_changed(self, buf): + self.stats_counter.count( + buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False), + 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 _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time)) + + def update_stats(self, stats): + (characters, words, sentences, read_time) = stats + self.characters = characters + self.words = words + self.sentences = sentences + self.read_time = read_time + self.update_default_stat(False) + + def update_default_stat(self, close_popover=True): + stat = self.settings.get_enum("stat-default") + text = self.get_text_for_stat(stat) + self.stats_button.set_label(text) + if close_popover and self.popover: + self.popover.popdown() + + def on_destroy(self, _widget): + self.stats_counter.stop() diff --git a/uberwriter/window.py b/uberwriter/window.py index 95c4fe1..ce7bf25 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -18,7 +18,6 @@ import codecs import locale import logging import os -import re import urllib import webbrowser from gettext import gettext as _ @@ -26,7 +25,7 @@ from gettext import gettext as _ import gi from uberwriter.export_dialog import Export -from uberwriter.stats_counter import StatsCounter +from uberwriter.stats_handler import StatsHandler from uberwriter.text_view import TextView gi.require_version('Gtk', '3.0') @@ -82,8 +81,6 @@ class Window(Gtk.ApplicationWindow): self.set_default_size(900, 500) - self.connect('delete-event', self.on_destroy) - # Preferences self.settings = Settings.new() @@ -95,17 +92,6 @@ class Window(Gtk.ApplicationWindow): self.title_end = " – UberWriter" self.set_headerbar_title("New File" + self.title_end) - self.word_count = self.builder.get_object('word_count') - self.char_count = self.builder.get_object('char_count') - - # Setup status bar hide after 3 seconds - self.status_bar = self.builder.get_object('status_bar_box') - self.statusbar_revealer = self.builder.get_object('status_bar_revealer') - self.status_bar.get_style_context().add_class('status-bar-box') - self.status_bar_visible = True - self.was_motion = True - self.buffer_modified_for_status_bar = False - self.timestamp_last_mouse_motion = 0 if self.settings.get_value("poll-motion"): self.connect("motion-notify-event", self.on_motion_notify) @@ -118,11 +104,10 @@ class Window(Gtk.ApplicationWindow): self.text_view = TextView() self.text_view.props.halign = Gtk.Align.CENTER 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() - self.text_view.get_buffer().connect('changed', self.on_text_changed) - # Setup preview webview self.preview_webview = None @@ -132,7 +117,15 @@ class Window(Gtk.ApplicationWindow): self.editor_viewport = self.builder.get_object('editor_viewport') # Stats counter - self.stats_counter = StatsCounter() + 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') + self.stats_handler = StatsHandler(self.stats_button, self.text_view) + + # Setup header/stats bar hide after 3 seconds + self.top_bottom_bars_visible = True + self.was_motion = True + self.buffer_modified_for_status_bar = False # some people seems to have performance problems with the overlay. # Let them disable it @@ -194,14 +187,6 @@ class Window(Gtk.ApplicationWindow): # Redraw contents of window self.queue_draw() - def update_stats_counts(self, stats): - """Updates line and character counts. - """ - - (characters, words, sentences, (hours, minutes, seconds)) = stats - self.char_count.set_text(str(characters)) - self.word_count.set_text(str(words)) - 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 @@ -214,9 +199,6 @@ class Window(Gtk.ApplicationWindow): self.buffer_modified_for_status_bar = True - if self.status_bar_visible: - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) - def set_fullscreen(self, state): """Puts the application in fullscreen mode and show/hides the poller for motion in the top border @@ -485,6 +467,9 @@ class Window(Gtk.ApplicationWindow): self.set_filename() self.set_headerbar_title(_("New File") + self.title_end) + def update_default_stat(self): + self.stats_handler.update_default_stat() + def menu_toggle_sidebar(self, _widget=None): """WIP """ @@ -666,13 +651,13 @@ class Window(Gtk.ApplicationWindow): """ if (self.was_motion is False - and self.status_bar_visible + and self.top_bottom_bars_visible 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.statusbar_revealer.set_reveal_child(False) + self.stats_counter_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) - self.status_bar_visible = False + self.top_bottom_bars_visible = False self.buffer_modified_for_status_bar = False self.was_motion = False @@ -691,26 +676,24 @@ class Window(Gtk.ApplicationWindow): return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + 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.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + 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.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.stats_counter.count_stats(self.text_view.get_text(), self.update_stats_counts) def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the @@ -789,8 +772,3 @@ class Window(Gtk.ApplicationWindow): webbrowser.open(web_view.get_uri()) decision.ignore() return True # Don't let the event "bubble up" - - def on_destroy(self, _widget, _data=None): - """Called when the Window is closing. - """ - self.stats_counter.stop() From f6c62fb45977cde3595571ba3eeb8a436f0569b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 11:21:53 +0100 Subject: [PATCH 42/54] Tooltip capitalization fix --- uberwriter/headerbars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index 6ee4ed8..110ba52 100644 --- a/uberwriter/headerbars.py +++ b/uberwriter/headerbars.py @@ -141,7 +141,7 @@ def buttons(app): btn.open_recent.pack_start(open_button, False, False, 0) btn.open_recent.pack_end(recents_button, False, False, 0) - btn.search.set_tooltip_text(_("Search and replace")) + btn.search.set_tooltip_text(_("Search and Replace")) btn.menu.set_tooltip_text(_("Menu")) btn.menu.set_image(Gtk.Image.new_from_icon_name("open-menu-symbolic", Gtk.IconSize.BUTTON)) From dccc645430f081360e8f3a19b43d2b5ad0ea3991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 11:20:47 +0100 Subject: [PATCH 43/54] Add paragraphs stat --- data/de.wolfvollprecht.UberWriter.gschema.xml | 5 ++-- uberwriter/stats_counter.py | 9 +++++- uberwriter/stats_handler.py | 29 ++++++++----------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index 6d2b13a..e7d4bfc 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -1,14 +1,15 @@ + - + + - diff --git a/uberwriter/stats_counter.py b/uberwriter/stats_counter.py index b753143..fa4b103 100644 --- a/uberwriter/stats_counter.py +++ b/uberwriter/stats_counter.py @@ -22,6 +22,9 @@ class StatsCounter: # exclamation mark, paragraph, and variants. SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+") + # Regexp that matches paragraphs, ie. anything separated by newlines. + PARAGRAPHS = re.compile(r".+\n?") + def __init__(self): super().__init__() @@ -61,8 +64,12 @@ class StatsCounter: sentence_count = len(re.findall(self.SENTENCES, text)) + paragraph_count = len(re.findall(self.PARAGRAPHS, text)) + read_m, read_s = divmod(word_count / 200 * 60, 60) read_h, read_m = divmod(read_m, 60) read_time = (int(read_h), int(read_m), int(read_s)) - GLib.idle_add(callback, (character_count, word_count, sentence_count, read_time)) + GLib.idle_add( + callback, + (character_count, word_count, sentence_count, paragraph_count, read_time)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py index ec9f40a..8fce782 100644 --- a/uberwriter/stats_handler.py +++ b/uberwriter/stats_handler.py @@ -30,6 +30,7 @@ class StatsHandler: self.characters = 0 self.words = 0 self.sentences = 0 + self.paragraphs = 0 self.read_time = (0, 0, 0) self.settings = Settings.new() @@ -43,22 +44,11 @@ class StatsHandler: self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False) menu = Gio.Menu() - characters_menu_item = Gio.MenuItem.new(self.get_text_for_stat(0), None) - characters_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("characters")) - menu.append_item(characters_menu_item) - words_menu_item = Gio.MenuItem.new(self.get_text_for_stat(1), None) - words_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("words")) - menu.append_item(words_menu_item) - sentences_menu_item = Gio.MenuItem.new(self.get_text_for_stat(2), None) - sentences_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("sentences")) - menu.append_item(sentences_menu_item) - read_time_menu_item = Gio.MenuItem.new(self.get_text_for_stat(3), None) - read_time_menu_item.set_action_and_target_value( - "app.stat_default", GLib.Variant.new_string("read_time")) - menu.append_item(read_time_menu_item) + stats = self.settings.props.settings_schema.get_key("stat-default").get_range()[1] + for i, stat in enumerate(stats): + menu_item = Gio.MenuItem.new(self.get_text_for_stat(i), None) + menu_item.set_action_and_target_value("app.stat_default", GLib.Variant.new_string(stat)) + menu.append_item(menu_item) self.popover = Gtk.Popover.new_from_model(self.stats_button, menu) self.popover.connect('closed', self.on_popover_closed) self.popover.popup() @@ -82,13 +72,18 @@ class StatsHandler: 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)) + else: + raise ValueError("Unknown stat {}".format(stat)) def update_stats(self, stats): - (characters, words, sentences, read_time) = stats + (characters, words, sentences, paragraphs, read_time) = stats self.characters = characters self.words = words self.sentences = sentences + self.paragraphs = paragraphs self.read_time = read_time self.update_default_stat(False) From 8e5ccfc01db37d6f75bf98f33a59b4506320bc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Fri, 19 Apr 2019 14:02:39 +0200 Subject: [PATCH 44/54] preserve the /data/media folder structure --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f88cee2..7b2ad3f 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,8 @@ def package_files(directory): return paths extra_files_ui = package_files('./data/ui') -extra_files_media = package_files('./data/media') +extra_files_media_css = package_files('./data/media/css') +extra_files_media_fonts = package_files('./data/media/fonts') extra_files_scripts = package_files('./data/lua') from pprint import pprint pprint(extra_files_ui) @@ -79,8 +80,10 @@ setup( (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), (app_prefix + 'share/applications', ['data/de.wolfvollprecht.UberWriter.desktop']), + (app_prefix + 'share/uberwriter/data/media', ['data/media/uberwriter_markdown.md']), (app_prefix + 'share/uberwriter/data/ui', extra_files_ui), - (app_prefix + 'share/uberwriter/data/media', extra_files_media), + (app_prefix + 'share/uberwriter/data/media/css', extra_files_media_css), + (app_prefix + 'share/uberwriter/data/media/fonts', extra_files_media_fonts), (app_prefix + 'share/uberwriter/data/lua', extra_files_scripts) ] ) From dc4c4d9c2c2eb9accdc74cba0b92a3952a606a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Fri, 19 Apr 2019 14:02:57 +0200 Subject: [PATCH 45/54] update flatpak json --- .../de.wolfvollprecht.UberWriter.pipdeps.json | 35 ++++++ flatpak/uberwriter.json | 105 ++++++------------ 2 files changed, 71 insertions(+), 69 deletions(-) create mode 100644 flatpak/de.wolfvollprecht.UberWriter.pipdeps.json diff --git a/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json b/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json new file mode 100644 index 0000000..dde1450 --- /dev/null +++ b/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json @@ -0,0 +1,35 @@ + + { + "name": "pipdeps", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz", + "sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz", + "sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz", + "sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz", + "sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz", + "sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" + } + ] + } \ No newline at end of file diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index 620c285..c162175 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -1,25 +1,24 @@ { "app-id": "de.wolfvollprecht.UberWriter", "runtime": "org.gnome.Platform", - "runtime-version": "3.28", + "runtime-version": "3.32", "sdk": "org.gnome.Sdk", - "command": "/app/usr/bin/uberwriter", + "command": "uberwriter", "finish-args": [ "--socket=x11", + "--socket=wayland", "--share=ipc", + "--share=network", "--filesystem=host", - "--env=IN_FLATPAK=1", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf", - "--env=XDG_DATA_DIRS=/app/usr/share", - "--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/usr/bin:/app/bin" + "--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin" ], "build-options" : { "env": { - "PYTHON": "python3", - "IN_FLATPAK": "1" + "PYTHON": "python3" } }, "add-extensions": { @@ -33,21 +32,18 @@ }, "modules": [ { - "name": "uberwriter", + "name": "enchant", + "config-opts": ["--disable-static", "--with-myspell-dir=/usr/share/hunspell"], + "cleanup": [ + "/bin" + ], "sources": [ - { - "type" : "git", - "url" : "../", - "branch" : "refactoring" - } - ], - "build-commands": [ - "install -Dm644 flatpak/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " - ], - "post-install": [ - "glib-compile-schemas /app/usr/share/glib-2.0/schemas", - "install -d /app/extensions" - ] + { + "type" : "archive", + "url" : "https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", + "sha256" : "bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" + } + ] }, { "name": "pandoc", @@ -56,8 +52,9 @@ ], "buildsystem": "simple", "build-commands": [ - "cp bin/pandoc /app/usr/bin/pandoc", - "cp bin/pandoc-citeproc /app/usr/bin/pandoc-citeproc" + "mkdir -p /app/bin", + "cp bin/pandoc /app/bin/pandoc", + "cp bin/pandoc-citeproc /app/bin/pandoc-citeproc" ], "sources": [ { @@ -67,40 +64,7 @@ } ] }, - { - "name": "pipdeps", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz", - "sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz", - "sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz", - "sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz", - "sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz", - "sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" - } - ] - }, + "de.wolfvollprecht.UberWriter.pipdeps.json", { "name": "fonts", "buildsystem": "simple", @@ -117,18 +81,21 @@ ] }, { - "name": "appdata", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/appdata", - "install -Dm644 de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml" - ], + "name": "uberwriter", "sources": [ - { - "type": "file", - "path": "de.wolfvollprecht.UberWriter.appdata.xml" - } - ] + { + "type" : "git", + "url" : "../", + "branch" : "various-improvements" + } + ], + "build-commands": [ + "install -Dm644 data/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " + ], + "post-install": [ + "glib-compile-schemas /app/share/glib-2.0/schemas", + "install -d /app/extensions" + ] } ] - } + } \ No newline at end of file From f466171eda2883e37a42ae58b32faefbdcd85f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Fri, 19 Apr 2019 14:39:06 +0200 Subject: [PATCH 46/54] print correct folders --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b2ad3f..a84509f 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ extra_files_media_fonts = package_files('./data/media/fonts') extra_files_scripts = package_files('./data/lua') from pprint import pprint pprint(extra_files_ui) -pprint(extra_files_media) +pprint(extra_files_media_css) +pprint(extra_files_media_fonts) if os.path.isfile("/.flatpak-info"): app_prefix = '/app/' From 32bb70a26132cfa147e390afe8de8fe06a53c010 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Fri, 19 Apr 2019 13:06:13 -0500 Subject: [PATCH 47/54] Flatpak fixes (ref. #111) --- .gitignore | 5 +- Makefile | 11 ++- README.md | 5 +- autogen.sh | 1 - configure | 1 - flatpak/uberwriter.json | 154 ++++++++++++++------------------- requirements.txt | 4 +- setup.py | 63 +++++++------- uberwriter/uberwriterconfig.py | 1 - 9 files changed, 107 insertions(+), 138 deletions(-) delete mode 100755 autogen.sh delete mode 100755 configure diff --git a/.gitignore b/.gitignore index d5e4595..cc528d3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,12 @@ debian/uberwriter/opt debian/uberwriter/usr bin/ flatpak/* -!flatpak/fonts-download -!flatpak/pandoc-download -!flatpak/pip-download !flatpak/uberwriter.json !flatpak/de.wolfvollprecht.UberWriter.* !flatpak/flatpak_texlive.json !flatpak/texlive_install.sh +!flatpak/python3-enchant.json +!flatpak/python3-packages.json *.py~ data/ui/shortcut_handlers *.ui~ diff --git a/Makefile b/Makefile index 4e6ad1e..0f57ea2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ -all: - python3 ./setup.py build +.PHONY: flatpak-user-install flatpak-generate-python-modules -install: - python3 ./setup.py install --prefix=/app --skip-build --optimize=1 +flatpak-user-install: + cd flatpak; flatpak-builder --force-clean --install --user _build uberwriter.json +flatpak-generate-python-modules: + # gtkspellcheck's setup.py wants enchant to already be installed + flatpak-pip-generator --output flatpak/python3-enchant.json pyenchant + flatpak-pip-generator --output flatpak/python3-packages.json `grep -v enchant requirements.txt` diff --git a/README.md b/README.md index 802d3f9..0c367dc 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ but you'll need to install and compile the schemas before: It's also possible to build, run and debug a flatpak package. You'll need flatpak-builder for this: -- cd to the flatpak dir of the repo -- `flatpak-builder --install --force-clean some_folder_name uberwriter.json` (this installs and cleans the build folder) +- `make flatpak-user-install` (this installs the Flatpak) - `flatpak run de.wolfvollprecht.UberWriter` If you can't find Uberwriter after this, it's due to a Flatpak bug. Try to export it to a local repo before installing it: @@ -54,4 +53,4 @@ If you want to update an existing installation, just run You can also debug it with the following: `flatpak-builder --run --share=network some_folder_name uberwriter.json sh` -If you want to install it using setuptools, simply run `python3 setup.py build install` \ No newline at end of file +If you want to install it using setuptools, simply run `python3 setup.py build install` diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index 8b13789..0000000 --- a/autogen.sh +++ /dev/null @@ -1 +0,0 @@ - diff --git a/configure b/configure deleted file mode 100755 index 8b13789..0000000 --- a/configure +++ /dev/null @@ -1 +0,0 @@ - diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index 620c285..10472e7 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -1,27 +1,18 @@ { "app-id": "de.wolfvollprecht.UberWriter", "runtime": "org.gnome.Platform", - "runtime-version": "3.28", + "runtime-version": "3.30", "sdk": "org.gnome.Sdk", - "command": "/app/usr/bin/uberwriter", + "command": "start-uberwriter", "finish-args": [ "--socket=x11", "--share=ipc", "--filesystem=host", - "--env=IN_FLATPAK=1", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", - "--env=DCONF_USER_CONFIG_DIR=.config/dconf", - "--env=XDG_DATA_DIRS=/app/usr/share", - "--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/usr/bin:/app/bin" + "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], - "build-options" : { - "env": { - "PYTHON": "python3", - "IN_FLATPAK": "1" - } - }, "add-extensions": { "de.wolfvollprecht.UberWriter.Plugin": { "directory": "extensions", @@ -32,75 +23,6 @@ } }, "modules": [ - { - "name": "uberwriter", - "sources": [ - { - "type" : "git", - "url" : "../", - "branch" : "refactoring" - } - ], - "build-commands": [ - "install -Dm644 flatpak/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " - ], - "post-install": [ - "glib-compile-schemas /app/usr/share/glib-2.0/schemas", - "install -d /app/extensions" - ] - }, - { - "name": "pandoc", - "only-arches": [ - "x86_64" - ], - "buildsystem": "simple", - "build-commands": [ - "cp bin/pandoc /app/usr/bin/pandoc", - "cp bin/pandoc-citeproc /app/usr/bin/pandoc-citeproc" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", - "sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" - } - ] - }, - { - "name": "pipdeps", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc" - ], - "sources": [ - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz", - "sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz", - "sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz", - "sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz", - "sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz", - "sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" - } - ] - }, { "name": "fonts", "buildsystem": "simple", @@ -117,18 +39,72 @@ ] }, { - "name": "appdata", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/appdata", - "install -Dm644 de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml" + "name": "pandoc", + "only-arches": [ + "x86_64" + ], + "buildsystem": "simple", + "build-commands": [ + "install -Dm 755 bin/pandoc /app/bin/pandoc", + "install -Dm 755 bin/pandoc-citeproc /app/bin/pandoc-citeproc" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", + "sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" + } + ] + }, + { + "name": "enchant", + "config-opts": [ + "--with-myspell-dir=/usr/share/myspell" ], "sources": [ { - "type": "file", - "path": "de.wolfvollprecht.UberWriter.appdata.xml" + "type": "archive", + "url": "https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", + "sha256": "bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" } ] + }, + "python3-enchant.json", + "python3-packages.json", + { + "name": "uberwriter", + "buildsystem": "simple", + "build-commands": [ + "desktop-file-edit --set-key=Exec --set-value='uberwriter.in %U' data/de.wolfvollprecht.UberWriter.desktop", + "python3 -m pip install --prefix=/app --install-option=--optimize=1 ." + ], + "sources": [ + { + "type" : "dir", + "path" : "../" + } + ], + "post-install": [ + "install -d /app/extensions", + "glib-compile-schemas /app/share/glib-2.0/schemas" + ] + }, + { + "name": "scripts", + "buildsystem": "simple", + "build-commands": [ + "install -Dm 755 start-uberwriter.sh /app/bin/start-uberwriter" + ], + "sources": [ + { + "type": "script", + "dest-filename": "start-uberwriter.sh", + "commands": [ + "export PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/$(uname -a)-linux:$PATH", + "exec uberwriter.in \"$@\"" + ] + } + ] } ] - } +} diff --git a/requirements.txt b/requirements.txt index 340d2e0..eed1919 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ regex -enchant -python-gtkspellcheck pandoc +pyenchant +pygtkspellcheck diff --git a/setup.py b/setup.py index 8671284..e2995be 100644 --- a/setup.py +++ b/setup.py @@ -2,16 +2,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2012, Wolf Vollprecht -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -22,24 +22,20 @@ from setuptools import setup import os -def package_files(directory): - paths = [] - for (path, directories, filenames) in os.walk(directory): +def data_files(basename): + data = os.path.join('.', 'data') + root = os.path.join(data, basename) + extra_files = [] + for path, directories, filenames in os.walk(root): + paths = [] for filename in filenames: paths.append(os.path.join(path, filename)) - return paths + extra_files.append(('share/uberwriter/data/{}'.format(os.path.relpath(path, data)), paths)) + return extra_files -extra_files_ui = package_files('./data/ui') -extra_files_media = package_files('./data/media') -extra_files_scripts = package_files('./data/lua') -from pprint import pprint -pprint(extra_files_ui) -pprint(extra_files_media) - -if os.path.isfile("/.flatpak-info"): - app_prefix = '/app/' -else: - app_prefix = '/usr/' +extra_files_ui = data_files('ui') +extra_files_media = data_files('media') +extra_files_scripts = data_files('lua') setup( name='uberwriter', @@ -49,10 +45,10 @@ setup( author_email='w.vollprecht@gmail.com', description='A beautiful, simple and distraction free markdown editor.', long_description="""UberWriter, beautiful distraction free writing - With UberWriter you get only one thing: An empty textbox, that is to - fill with your ideas. There are no settings, you don't have to choose a - font, it is only for writing.You can use markdown for all your markup - needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it + With UberWriter you get only one thing: An empty textbox, that is to + fill with your ideas. There are no settings, you don't have to choose a + font, it is only for writing.You can use markdown for all your markup + needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it is also required that you choose to install the texlive-luatex package.""", url='https://github.com/wolfv/uberwriter/', # cmdclass={'install': InstallAndUpdateDataDirectory}, @@ -75,12 +71,11 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['bin/uberwriter']), - (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), - (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), - (app_prefix + 'share/applications', ['de.wolfvollprecht.UberWriter.desktop']), - (app_prefix + 'share/uberwriter/data/ui', extra_files_ui), - (app_prefix + 'share/uberwriter/data/media', extra_files_media), - (app_prefix + 'share/uberwriter/data/lua', extra_files_scripts) + ('bin', ['uberwriter.in']), + ('share/applications', ['data/de.wolfvollprecht.UberWriter.desktop']), + ('share/metainfo', ['data/de.wolfvollprecht.UberWriter.appdata.xml']), + ('share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), + ('share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), + *(extra_files_ui + extra_files_media + extra_files_scripts) ] ) diff --git a/uberwriter/uberwriterconfig.py b/uberwriter/uberwriterconfig.py index fc3f7b1..5d2da16 100644 --- a/uberwriter/uberwriterconfig.py +++ b/uberwriter/uberwriterconfig.py @@ -53,7 +53,6 @@ def get_data_path(): """ # Get pathname absolute or relative. - # TODO: Abstract this (the old env IN_FLATPAK) if os.path.isfile("/.flatpak-info"): return '/app/share/uberwriter/data/' From e87de1424e5fb1f228e7bb032e2a7156ba7205d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 12:01:43 +0100 Subject: [PATCH 48/54] Remove hard-coding from Gtk-related CSS It's unecessary, since Adwaita and most popular themes provide a defined set of public colors. For reference, see: https://github.com/GNOME/gtk/blob/master/gtk/theme/Adwaita/_colors-public.scss --- data/media/css/gtk_adwaita.css | 5 -- data/media/css/gtk_adwaita_dark.css | 5 -- data/media/css/gtk_arc.css | 5 -- data/media/css/gtk_arc_dark.css | 5 -- data/media/css/gtk_arc_darker.css | 5 -- .../media/css/{_gtk_base.css => gtk_base.css} | 54 +++++++++---------- data/media/css/gtk_high_contrast.css | 5 -- data/media/css/gtk_high_contrast_inverse.css | 5 -- .../css/{_web_base.css => web__base.css} | 0 data/media/css/web_adwaita.css | 2 +- data/media/css/web_adwaita_dark.css | 2 +- data/media/css/web_arc.css | 2 +- data/media/css/web_arc_dark.css | 2 +- ...high_contrast.css => web_highcontrast.css} | 2 +- ...verse.css => web_highcontrast_inverse.css} | 2 +- uberwriter/application.py | 2 +- uberwriter/theme.py | 25 +++------ uberwriter/window.py | 2 +- 18 files changed, 43 insertions(+), 87 deletions(-) delete mode 100644 data/media/css/gtk_adwaita.css delete mode 100644 data/media/css/gtk_adwaita_dark.css delete mode 100644 data/media/css/gtk_arc.css delete mode 100644 data/media/css/gtk_arc_dark.css delete mode 100644 data/media/css/gtk_arc_darker.css rename data/media/css/{_gtk_base.css => gtk_base.css} (74%) delete mode 100644 data/media/css/gtk_high_contrast.css delete mode 100644 data/media/css/gtk_high_contrast_inverse.css rename data/media/css/{_web_base.css => web__base.css} (100%) rename data/media/css/{web_high_contrast.css => web_highcontrast.css} (94%) rename data/media/css/{web_high_contrast_inverse.css => web_highcontrast_inverse.css} (94%) diff --git a/data/media/css/gtk_adwaita.css b/data/media/css/gtk_adwaita.css deleted file mode 100644 index f75dcde..0000000 --- a/data/media/css/gtk_adwaita.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #2e3436; -@define-color background_color #f6f5f4; -@define-color math_text_color #00364c; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_adwaita_dark.css b/data/media/css/gtk_adwaita_dark.css deleted file mode 100644 index a41d20c..0000000 --- a/data/media/css/gtk_adwaita_dark.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #eeeeec; -@define-color background_color #353535; -@define-color math_text_color #ffc9b3; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc.css b/data/media/css/gtk_arc.css deleted file mode 100644 index 967c919..0000000 --- a/data/media/css/gtk_arc.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #3b3e45; -@define-color background_color #f5f6f7; -@define-color math_text_color #00364c; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc_dark.css b/data/media/css/gtk_arc_dark.css deleted file mode 100644 index 3edd842..0000000 --- a/data/media/css/gtk_arc_dark.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #d3dae3; -@define-color background_color #383c4a; -@define-color math_text_color #ffc9b3; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc_darker.css b/data/media/css/gtk_arc_darker.css deleted file mode 100644 index 44c667b..0000000 --- a/data/media/css/gtk_arc_darker.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #3b3e45; -@define-color background_color #f5f6f7; -@define-color math_text_color #00364C; - -@import url("_gtk_base.css"); diff --git a/data/media/css/_gtk_base.css b/data/media/css/gtk_base.css similarity index 74% rename from data/media/css/_gtk_base.css rename to data/media/css/gtk_base.css index b4fe25b..cd3d5a9 100644 --- a/data/media/css/_gtk_base.css +++ b/data/media/css/gtk_base.css @@ -17,12 +17,15 @@ bind "z" { "redo" () }; } +@define-color math_text_color mix(@theme_fg_color, #00b5ff, 0.15); + /* Main window and text colors */ .uberwriter-window { /*border-radius: 7px 7px 3px 3px;*/ - background: @background_color; - caret-color: @foreground_color; + background: @theme_base_color; + color: @theme_fg_color; + caret-color: @theme_fg_color; } .uberwriter-window .uberwriter-editor { @@ -50,27 +53,25 @@ } #titlebar-container { - background: @background_color; + background: @theme_base_color; } .uberwriter-editor { border: none; background-color: transparent; - text-decoration-color: #ff0000; - /*-GtkWidget-cursor-color: shade(#4D9FCE, 0.9);*/ - /*-GtkWidget-cursor-aspect-ratio: 0.1;*/ + text-decoration-color: @error_color; -gtk-key-bindings: editor-bindings; } .uberwriter-editor text { - background-color: @background_color; - color: @foreground_color; - caret-color: @foreground_color; + background-color: @theme_base_color; + color: @theme_fg_color; + caret-color: @theme_fg_color; } -.uberwriter-editor:selected { - background-color: #4D9FCE; - color: #FFF; +.uberwriter-editor text selection { + background-color: @theme_selected_bg_color; + color: @theme_selected_fg_color; } .uberwriter-editor button { @@ -85,10 +86,9 @@ padding: 0; } - .stats-counter { - color: alpha(@foreground_color, 0.6); - background-color: @background_color; + color: alpha(@theme_fg_color, 0.6); + background-color: @theme_base_color; text-shadow: inherit; box-shadow: initial; background-clip: initial; @@ -108,8 +108,8 @@ .stats-counter:hover, .stats-counter:checked { - color: @foreground_color; - background-color: lighter(@background_color); + color: @theme_fg_color; + background-color: mix(@theme_base_color, @theme_bg_color, 0.5); } #PreviewMenuItem image { @@ -128,9 +128,9 @@ /*font: serif 10;*/ font-family: serif; font-size: 10px; - background: @background_color; + background: @theme_bg_color; border-radius: 4px; - border-color: @background_color; + border-color: @theme_bg_color; margin: 5px; padding: 5px; } @@ -141,7 +141,7 @@ border: 1px solid #333; background: @ligth_bg; border-radius: 3px; - border-color: @background_color; + border-color: @theme_bg_color; } */ #LexikonBubble label { @@ -149,8 +149,8 @@ } #LexikonBubble { - background-color: @background_color; - border: 5px solid @background_color; + background-color: @theme_bg_color; + border: 5px solid @theme_bg_color; } #LexikonBubble .lexikon-heading { @@ -168,15 +168,15 @@ } .quick-preview-popup { - background-color: @background_color; + background-color: @theme_bg_color; } .quick-preview-popup grid { - background-color: @background_color; - color: @foreground_color; - border-color: @background_color; + background-color: @theme_bg_color; + color: @theme_fg_color; + border-color: @theme_bg_color; } .quick-preview-popup label { - color: @foreground_color; + color: @theme_fg_color; } \ No newline at end of file diff --git a/data/media/css/gtk_high_contrast.css b/data/media/css/gtk_high_contrast.css deleted file mode 100644 index 254215e..0000000 --- a/data/media/css/gtk_high_contrast.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #000000; -@define-color background_color #ffffff; -@define-color math_text_color #000000; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_high_contrast_inverse.css b/data/media/css/gtk_high_contrast_inverse.css deleted file mode 100644 index b9790d4..0000000 --- a/data/media/css/gtk_high_contrast_inverse.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #ffffff; -@define-color background_color #000000; -@define-color math_text_color #ffffff; - -@import url("_gtk_base.css"); diff --git a/data/media/css/_web_base.css b/data/media/css/web__base.css similarity index 100% rename from data/media/css/_web_base.css rename to data/media/css/web__base.css diff --git a/data/media/css/web_adwaita.css b/data/media/css/web_adwaita.css index ecfde17..500aaf6 100644 --- a/data/media/css/web_adwaita.css +++ b/data/media/css/web_adwaita.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); :root { --text-color: #2e3436; diff --git a/data/media/css/web_adwaita_dark.css b/data/media/css/web_adwaita_dark.css index 320a657..9aa875f 100644 --- a/data/media/css/web_adwaita_dark.css +++ b/data/media/css/web_adwaita_dark.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); :root { --text-color: #eeeeec; diff --git a/data/media/css/web_arc.css b/data/media/css/web_arc.css index c5ccb04..7f4c1c1 100644 --- a/data/media/css/web_arc.css +++ b/data/media/css/web_arc.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); :root { --text-color: #3b3e45; diff --git a/data/media/css/web_arc_dark.css b/data/media/css/web_arc_dark.css index b0c6a45..ad36e2e 100644 --- a/data/media/css/web_arc_dark.css +++ b/data/media/css/web_arc_dark.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); :root { --text-color: #d3dae3; diff --git a/data/media/css/web_high_contrast.css b/data/media/css/web_highcontrast.css similarity index 94% rename from data/media/css/web_high_contrast.css rename to data/media/css/web_highcontrast.css index b9db89e..ef01c0d 100644 --- a/data/media/css/web_high_contrast.css +++ b/data/media/css/web_highcontrast.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); a { text-decoration: underline; diff --git a/data/media/css/web_high_contrast_inverse.css b/data/media/css/web_highcontrast_inverse.css similarity index 94% rename from data/media/css/web_high_contrast_inverse.css rename to data/media/css/web_highcontrast_inverse.css index 646e1f0..247f968 100644 --- a/data/media/css/web_high_contrast_inverse.css +++ b/data/media/css/web_highcontrast_inverse.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web__base.css"); a { text-decoration: underline; diff --git a/uberwriter/application.py b/uberwriter/application.py index 0ad632f..65ff0c6 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -126,7 +126,7 @@ class Application(Gtk.Application): # Shortcuts - # TODO: be aware that a couple of shortcuts are defined in _gtk_base.css + # TODO: be aware that a couple of shortcuts are defined in gtk_base.css self.set_accels_for_action("app.focus_mode", ["d"]) self.set_accels_for_action("app.hemingway_mode", ["t"]) diff --git a/uberwriter/theme.py b/uberwriter/theme.py index 8139c90..3302ec4 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -14,9 +14,8 @@ class Theme: previous = None settings = Settings.new() - def __init__(self, name, gtk_css_path, web_css_path, is_dark, inverse_name): + def __init__(self, name, web_css_path, is_dark, inverse_name): self.name = name - self.gtk_css_path = gtk_css_path self.web_css_path = web_css_path self.is_dark = is_dark self.inverse_name = inverse_name @@ -49,7 +48,6 @@ class Theme: def __eq__(self, other): return isinstance(other, self.__class__) and \ self.name == other.name and \ - self.gtk_css_path == other.gtk_css_path and \ self.web_css_path == other.web_css_path and \ self.is_dark == other.is_dark and \ self.inverse_name == other.inverse_name @@ -57,20 +55,13 @@ class Theme: defaultThemes = [ # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita - Theme('Adwaita', get_css_path('gtk_adwaita.css'), - get_css_path('web_adwaita.css'), False, 'Adwaita-dark'), - Theme('Adwaita-dark', get_css_path('gtk_adwaita_dark.css'), - get_css_path('web_adwaita_dark.css'), True, 'Adwaita'), + Theme('Adwaita', get_css_path('web_adwaita.css'), False, 'Adwaita-dark'), + Theme('Adwaita-dark', get_css_path('web_adwaita_dark.css'), True, 'Adwaita'), # https://github.com/NicoHood/arc-theme/tree/master/common/gtk-3.0/3.20/sass - Theme('Arc', get_css_path('gtk_arc.css'), - get_css_path('web_arc.css'), False, 'Arc-Dark'), - Theme('Arc-Darker', get_css_path('gtk_arc_darker.css'), - get_css_path('web_arc_darker.css'), False, 'Arc-Dark'), - Theme('Arc-Dark', get_css_path('gtk_arc_dark.css'), - get_css_path('web_arc_dark.css'), True, 'Arc'), + Theme('Arc', get_css_path('web_arc.css'), False, 'Arc-Dark'), + Theme('Arc-Darker', get_css_path('web_arc_darker.css'), False, 'Arc-Dark'), + Theme('Arc-Dark', get_css_path('web_arc_dark.css'), True, 'Arc'), # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/HighContrast - Theme('HighContrast', get_css_path('gtk_high_contrast.css'), - get_css_path('web_high_contrast.css'), False, 'HighContrastInverse'), - Theme('HighContrastInverse', get_css_path('gtk_high_contrast_inverse.css'), - get_css_path('web_high_contrast_inverse.css'), True, 'HighContrast'), + Theme('HighContrast', get_css_path('web_highcontrast.css'), False, 'HighContrastInverse'), + Theme('HighContrastInverse', get_css_path('web_highcontrast_inverse.css'), True, 'HighContrast') ] diff --git a/uberwriter/window.py b/uberwriter/window.py index ce7bf25..bbee472 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -176,7 +176,7 @@ class Window(Gtk.ApplicationWindow): # Set theme css style_provider = Gtk.CssProvider() - style_provider.load_from_path(theme.gtk_css_path) + 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) From 81f9104d9f29781a5d0448cc6d88c3e7bb4cf672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Fri, 19 Apr 2019 12:08:11 +0100 Subject: [PATCH 49/54] Split gtk and web CSS into separate directories Helps clean up the CSS selection on the export dialog, as only Web CSS is useful in that scenario. --- data/media/css/{gtk_base.css => gtk/base.css} | 0 .../media/css/{web_adwaita.css => web/adwaita.css} | 2 +- .../{web_adwaita_dark.css => web/adwaita_dark.css} | 2 +- data/media/css/{web_arc.css => web/arc.css} | 2 +- .../css/{web_arc_dark.css => web/arc_dark.css} | 2 +- data/media/css/web/arc_darker.css | 1 + data/media/css/{web__base.css => web/base.css} | 5 ++--- .../{web_highcontrast.css => web/highcontrast.css} | 2 +- .../highcontrast_inverse.css} | 2 +- data/media/css/web_arc_darker.css | 1 - uberwriter/application.py | 2 +- uberwriter/theme.py | 14 +++++++------- uberwriter/window.py | 2 +- 13 files changed, 18 insertions(+), 19 deletions(-) rename data/media/css/{gtk_base.css => gtk/base.css} (100%) rename data/media/css/{web_adwaita.css => web/adwaita.css} (93%) rename data/media/css/{web_adwaita_dark.css => web/adwaita_dark.css} (93%) rename data/media/css/{web_arc.css => web/arc.css} (93%) rename data/media/css/{web_arc_dark.css => web/arc_dark.css} (93%) create mode 100644 data/media/css/web/arc_darker.css rename data/media/css/{web__base.css => web/base.css} (95%) rename data/media/css/{web_highcontrast.css => web/highcontrast.css} (93%) rename data/media/css/{web_highcontrast_inverse.css => web/highcontrast_inverse.css} (93%) delete mode 100644 data/media/css/web_arc_darker.css diff --git a/data/media/css/gtk_base.css b/data/media/css/gtk/base.css similarity index 100% rename from data/media/css/gtk_base.css rename to data/media/css/gtk/base.css diff --git a/data/media/css/web_adwaita.css b/data/media/css/web/adwaita.css similarity index 93% rename from data/media/css/web_adwaita.css rename to data/media/css/web/adwaita.css index 500aaf6..e784b20 100644 --- a/data/media/css/web_adwaita.css +++ b/data/media/css/web/adwaita.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("base.css"); :root { --text-color: #2e3436; diff --git a/data/media/css/web_adwaita_dark.css b/data/media/css/web/adwaita_dark.css similarity index 93% rename from data/media/css/web_adwaita_dark.css rename to data/media/css/web/adwaita_dark.css index 9aa875f..df71e44 100644 --- a/data/media/css/web_adwaita_dark.css +++ b/data/media/css/web/adwaita_dark.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("web/web__base.css"); :root { --text-color: #eeeeec; diff --git a/data/media/css/web_arc.css b/data/media/css/web/arc.css similarity index 93% rename from data/media/css/web_arc.css rename to data/media/css/web/arc.css index 7f4c1c1..688fb87 100644 --- a/data/media/css/web_arc.css +++ b/data/media/css/web/arc.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("web/web__base.css"); :root { --text-color: #3b3e45; diff --git a/data/media/css/web_arc_dark.css b/data/media/css/web/arc_dark.css similarity index 93% rename from data/media/css/web_arc_dark.css rename to data/media/css/web/arc_dark.css index ad36e2e..aeba9ac 100644 --- a/data/media/css/web_arc_dark.css +++ b/data/media/css/web/arc_dark.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("web/web__base.css"); :root { --text-color: #d3dae3; diff --git a/data/media/css/web/arc_darker.css b/data/media/css/web/arc_darker.css new file mode 100644 index 0000000..3eeede8 --- /dev/null +++ b/data/media/css/web/arc_darker.css @@ -0,0 +1 @@ +@import url("web/web_arc.css"); \ No newline at end of file diff --git a/data/media/css/web__base.css b/data/media/css/web/base.css similarity index 95% rename from data/media/css/web__base.css rename to data/media/css/web/base.css index 3dd9ecc..dd54a03 100644 --- a/data/media/css/web__base.css +++ b/data/media/css/web/base.css @@ -1,11 +1,11 @@ @font-face { font-family: fira-sans; - src: url("../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); + src: url("../../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); } @font-face { font-family: fira-mono; - src: url("../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); + src: url("../../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); } @font-face { @@ -56,7 +56,6 @@ body { background-color: var(--background-color); font-family: "Fira Sans", fira-sans, sans-serif, color-emoji; line-height: 1.5; - text-size-adjust: 100%; word-wrap: break-word; padding: 2em; } diff --git a/data/media/css/web_highcontrast.css b/data/media/css/web/highcontrast.css similarity index 93% rename from data/media/css/web_highcontrast.css rename to data/media/css/web/highcontrast.css index ef01c0d..8e12c4f 100644 --- a/data/media/css/web_highcontrast.css +++ b/data/media/css/web/highcontrast.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("web/web__base.css"); a { text-decoration: underline; diff --git a/data/media/css/web_highcontrast_inverse.css b/data/media/css/web/highcontrast_inverse.css similarity index 93% rename from data/media/css/web_highcontrast_inverse.css rename to data/media/css/web/highcontrast_inverse.css index 247f968..e4ed265 100644 --- a/data/media/css/web_highcontrast_inverse.css +++ b/data/media/css/web/highcontrast_inverse.css @@ -1,4 +1,4 @@ -@import url("web__base.css"); +@import url("web/web__base.css"); a { text-decoration: underline; diff --git a/data/media/css/web_arc_darker.css b/data/media/css/web_arc_darker.css deleted file mode 100644 index cc6a95e..0000000 --- a/data/media/css/web_arc_darker.css +++ /dev/null @@ -1 +0,0 @@ -@import url("web_arc.css"); \ No newline at end of file diff --git a/uberwriter/application.py b/uberwriter/application.py index 65ff0c6..4a0f34c 100644 --- a/uberwriter/application.py +++ b/uberwriter/application.py @@ -126,7 +126,7 @@ class Application(Gtk.Application): # Shortcuts - # TODO: be aware that a couple of shortcuts are defined in gtk_base.css + # TODO: be aware that a couple of shortcuts are defined in base.css self.set_accels_for_action("app.focus_mode", ["d"]) self.set_accels_for_action("app.hemingway_mode", ["t"]) diff --git a/uberwriter/theme.py b/uberwriter/theme.py index 3302ec4..db47156 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -55,13 +55,13 @@ class Theme: defaultThemes = [ # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita - Theme('Adwaita', get_css_path('web_adwaita.css'), False, 'Adwaita-dark'), - Theme('Adwaita-dark', get_css_path('web_adwaita_dark.css'), True, 'Adwaita'), + Theme('Adwaita', get_css_path('web/adwaita.css'), False, 'Adwaita-dark'), + Theme('Adwaita-dark', get_css_path('web/adwaita_dark.css'), True, 'Adwaita'), # https://github.com/NicoHood/arc-theme/tree/master/common/gtk-3.0/3.20/sass - Theme('Arc', get_css_path('web_arc.css'), False, 'Arc-Dark'), - Theme('Arc-Darker', get_css_path('web_arc_darker.css'), False, 'Arc-Dark'), - Theme('Arc-Dark', get_css_path('web_arc_dark.css'), True, 'Arc'), + Theme('Arc', get_css_path('web/arc.css'), False, 'Arc-Dark'), + Theme('Arc-Darker', get_css_path('web/arc_darker.css'), False, 'Arc-Dark'), + Theme('Arc-Dark', get_css_path('web/arc_dark.css'), True, 'Arc'), # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/HighContrast - Theme('HighContrast', get_css_path('web_highcontrast.css'), False, 'HighContrastInverse'), - Theme('HighContrastInverse', get_css_path('web_highcontrast_inverse.css'), True, 'HighContrast') + Theme('HighContrast', get_css_path('web/highcontrast.css'), False, 'HighContrastInverse'), + Theme('HighContrastInverse', get_css_path('web/highcontrast_inverse.css'), True, 'HighContrast') ] diff --git a/uberwriter/window.py b/uberwriter/window.py index bbee472..8274b12 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -176,7 +176,7 @@ class Window(Gtk.ApplicationWindow): # Set theme css style_provider = Gtk.CssProvider() - style_provider.load_from_path(helpers.get_css_path("gtk_base.css")) + 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) From ddcf76df472f1e5be86d838ef7468bbf4e4e2630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sat, 20 Apr 2019 05:09:02 +0100 Subject: [PATCH 50/54] Set text view padding in Python instead of CSS Otherwise scrolling calculations will be slightly offset, as the CSS padding is added *on top* of any other margin. --- data/media/css/gtk/base.css | 2 -- uberwriter/text_view.py | 8 +++----- uberwriter/window.py | 8 ++++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css index cd3d5a9..ff0736d 100644 --- a/data/media/css/gtk/base.css +++ b/data/media/css/gtk/base.css @@ -31,8 +31,6 @@ .uberwriter-window .uberwriter-editor { font-family: 'Fira Mono', monospace; font-size: 16px; - padding-top: 80px; - padding-bottom: 16px; } .uberwriter-window.small .uberwriter-editor { diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index df945df..006c253 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -84,8 +84,6 @@ class TextView(Gtk.TextView): # Focus mode self.focus_mode = False - self.original_top_margin = self.props.top_margin - self.original_bottom_margin = self.props.bottom_margin self.connect('button-release-event', self.on_button_release_event) # Hemingway mode @@ -127,8 +125,8 @@ class TextView(Gtk.TextView): self.props.top_margin = height / 2 self.props.bottom_margin = height / 2 else: - self.props.top_margin = self.original_top_margin - self.props.bottom_margin = self.original_bottom_margin + self.props.top_margin = 80 + self.props.bottom_margin = 64 def on_button_release_event(self, _widget, _event): if self.focus_mode: @@ -159,7 +157,7 @@ class TextView(Gtk.TextView): If mark is unspecified, the cursor is used.""" - margin = 80 + margin = 32 scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__) if not scrolled_window: return diff --git a/uberwriter/window.py b/uberwriter/window.py index 8274b12..271fc82 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -701,7 +701,7 @@ class Window(Gtk.ApplicationWindow): """ bg_color = self.get_style_context().get_background_color(Gtk.StateFlags.ACTIVE) - lg_top = cairo.LinearGradient(0, 0, 0, 35) # pylint: disable=no-member + lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member lg_top.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba( @@ -710,12 +710,12 @@ class Window(Gtk.ApplicationWindow): width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height - cr.rectangle(0, 0, width, 35) + cr.rectangle(0, 0, width, 32) cr.set_source(lg_top) cr.fill() - cr.rectangle(0, height - 35, width, height) + cr.rectangle(0, height - 32, width, height) - lg_btm = cairo.LinearGradient(0, height - 35, 0, height) # pylint: disable=no-member + lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba( From 878bbdb67c029f22515d065ea1ca6e581538adeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Silva?= Date: Sun, 21 Apr 2019 01:50:17 +0100 Subject: [PATCH 51/54] Cap preview width similar to GitHub It is presumed that this was not included in the original stylesheets (which were heavily based on GitHub's) because it is set in JS. --- data/media/css/web/base.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/media/css/web/base.css b/data/media/css/web/base.css index dd54a03..65dfbb2 100644 --- a/data/media/css/web/base.css +++ b/data/media/css/web/base.css @@ -57,6 +57,8 @@ body { font-family: "Fira Sans", fira-sans, sans-serif, color-emoji; line-height: 1.5; word-wrap: break-word; + max-width: 978px; + margin: auto; padding: 2em; } From 15c69190d840ed168bdcc0cdf941b51c845a6946 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Mon, 22 Apr 2019 18:37:43 -0400 Subject: [PATCH 52/54] gitignore: Add generated folders These folders are not tracked by us and thus should be ignored. Prevents them from accidentally being added. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7575b05..4b56dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ data/ui/shortcut_handlers *.glade~ dist/uberwriter-2.0b0-py3.7.egg builddir/* +dist/ +uberwriter.egg-info From e0cea3654a69fec06d846b0953558986345ceb24 Mon Sep 17 00:00:00 2001 From: Christopher Davis Date: Mon, 22 Apr 2019 17:12:51 -0400 Subject: [PATCH 53/54] Migrate to gspell gtkspellcheck is abandoned; Use gspell instead. Closes https://github.com/UberWriter/uberwriter/issues/93 --- PKGBUILD | 2 +- flatpak/uberwriter.json | 147 +++--- requirements.txt | 1 - setup.py | 1 - uberwriter/gtkspellcheck/__init__.py | 53 -- uberwriter/gtkspellcheck/oxt_extract.py | 294 ----------- uberwriter/gtkspellcheck/spellcheck.py | 660 ------------------------ uberwriter/text_view.py | 8 +- uberwriter/window.py | 39 +- 9 files changed, 91 insertions(+), 1114 deletions(-) delete mode 100644 uberwriter/gtkspellcheck/__init__.py delete mode 100644 uberwriter/gtkspellcheck/oxt_extract.py delete mode 100644 uberwriter/gtkspellcheck/spellcheck.py diff --git a/PKGBUILD b/PKGBUILD index b7b4cb2..24c2817 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc='A distraction free Markdown editor for GNU/Linux made with GTK+' arch=('any') url='http://uberwriter.github.io/uberwriter/' license=('GPL3') -depends=('gtk3' 'pandoc' 'python-gtkspellcheck') +depends=('gtk3' 'pandoc' 'gspell') makedepends=('python-setuptools') optdepends=('texlive-core' 'otf-fira-mono: Recommended font') provides=("$_pkgname") diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index c162175..bc14512 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -1,10 +1,10 @@ { - "app-id": "de.wolfvollprecht.UberWriter", - "runtime": "org.gnome.Platform", - "runtime-version": "3.32", - "sdk": "org.gnome.Sdk", - "command": "uberwriter", - "finish-args": [ + "app-id":"de.wolfvollprecht.UberWriter", + "runtime":"org.gnome.Platform", + "runtime-version":"3.32", + "sdk":"org.gnome.Sdk", + "command":"uberwriter", + "finish-args":[ "--socket=x11", "--socket=wayland", "--share=ipc", @@ -16,86 +16,99 @@ "--env=DCONF_USER_CONFIG_DIR=.config/dconf", "--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin" ], - "build-options" : { - "env": { - "PYTHON": "python3" + "build-options":{ + "env":{ + "PYTHON":"python3" } }, - "add-extensions": { - "de.wolfvollprecht.UberWriter.Plugin": { - "directory": "extensions", - "version": "stable", - "subdirectories": true, - "no-autodownload": true, - "autodelete": true - } + "add-extensions":{ + "de.wolfvollprecht.UberWriter.Plugin":{ + "directory":"extensions", + "version":"stable", + "subdirectories":true, + "no-autodownload":true, + "autodelete":true + } }, - "modules": [ + "modules":[ { - "name": "enchant", - "config-opts": ["--disable-static", "--with-myspell-dir=/usr/share/hunspell"], - "cleanup": [ - "/bin" - ], - "sources": [ - { - "type" : "archive", - "url" : "https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", - "sha256" : "bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" - } - ] + "name":"enchant", + "config-opts":[ + "--disable-static", + "--with-myspell-dir=/usr/share/hunspell" + ], + "cleanup":[ + "/bin" + ], + "sources":[ + { + "type":"archive", + "url":"https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", + "sha256":"bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" + } + ] }, { - "name": "pandoc", - "only-arches": [ - "x86_64" - ], - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/bin", - "cp bin/pandoc /app/bin/pandoc", - "cp bin/pandoc-citeproc /app/bin/pandoc-citeproc" - ], - "sources": [ + "name":"gspell", + "sources":[ { - "type": "archive", - "url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", - "sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" + "type":"archive", + "url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.1.tar.xz", + "sha256":"819a1d23c7603000e73f5e738bdd284342e0cd345fb0c7650999c31ec741bbe5" + } + ] + }, + { + "name":"pandoc", + "only-arches":[ + "x86_64" + ], + "buildsystem":"simple", + "build-commands":[ + "mkdir -p /app/bin", + "cp bin/pandoc /app/bin/pandoc", + "cp bin/pandoc-citeproc /app/bin/pandoc-citeproc" + ], + "sources":[ + { + "type":"archive", + "url":"https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", + "sha256":"06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" } ] }, "de.wolfvollprecht.UberWriter.pipdeps.json", { - "name": "fonts", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/fonts/", - "cp ttf/* /app/share/fonts/" + "name":"fonts", + "buildsystem":"simple", + "build-commands":[ + "mkdir -p /app/share/fonts/", + "cp ttf/* /app/share/fonts/" ], - "sources": [ + "sources":[ { - "type": "git", - "url": "https://github.com/mozilla/Fira", - "tag": "4.202" + "type":"git", + "url":"https://github.com/mozilla/Fira", + "tag":"4.202" } ] }, { - "name": "uberwriter", - "sources": [ - { - "type" : "git", - "url" : "../", - "branch" : "various-improvements" - } - ], - "build-commands": [ - "install -Dm644 data/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " - ], - "post-install": [ + "name":"uberwriter", + "sources":[ + { + "type":"git", + "url":"../", + "branch":"various-improvements" + } + ], + "build-commands":[ + "install -Dm644 data/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " + ], + "post-install":[ "glib-compile-schemas /app/share/glib-2.0/schemas", "install -d /app/extensions" - ] + ] } ] - } \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index 971e047..e89d8ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ regex enchant -python-gtkspellcheck pypandoc==1.4 diff --git a/setup.py b/setup.py index a84509f..6dd97c2 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,6 @@ setup( # "": '/opt/uberwriter/' }, packages=[ - "uberwriter.gtkspellcheck", "uberwriter.pylocales", # "uberwriter.pressagio", "uberwriter", diff --git a/uberwriter/gtkspellcheck/__init__.py b/uberwriter/gtkspellcheck/__init__.py deleted file mode 100644 index 3f30951..0000000 --- a/uberwriter/gtkspellcheck/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import unicode_literals - -__version__ = '4.0.5' -__project__ = 'Python GTK Spellcheck' -__short_name__ = 'pygtkspellcheck' -__authors__ = 'Maximilian Köhl & Carlos Jenkins' -__emails__ = 'linuxmaxi@googlemail.com & carlos@jenkins.co.cr' -__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html' -__download_url__ = 'https://github.com/koehlma/pygtkspellcheck/tarball/master' -__source__ = 'https://github.com/koehlma/pygtkspellcheck/' -__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git' -__copyright__ = '2012, Maximilian Köhl & Carlos Jenkins' -__desc_short__ = ('a simple but quite powerful Python spell checking library ' - 'for GtkTextViews based on Enchant') -__desc_long__ = ('A simple but quite powerful spellchecking library written in ' - 'pure Python for Gtk based on Enchant. It supports PyGObject ' - 'as well as PyGtk for Python 2 and 3 with automatic switching ' - 'and binding detection. For automatic translation of the user ' - 'interface it can use Gedit’s translation files.') - -__metadata__ = {'__version__' : __version__, - '__project__' : __project__, - '__short_name__' : __short_name__, - '__authors__' : __authors__, - '__emails__' : __emails__, - '__website__' : __website__, - '__download_url__' : __download_url__, - '__source__' : __source__, - '__vcs__' : __vcs__, - '__copyright__' : __copyright__, - '__desc_short__' : __desc_short__, - '__desc_long__' : __desc_long__} - -from .spellcheck import (SpellChecker, NoDictionariesFound, - NoGtkBindingFound) diff --git a/uberwriter/gtkspellcheck/oxt_extract.py b/uberwriter/gtkspellcheck/oxt_extract.py deleted file mode 100644 index ce1c45a..0000000 --- a/uberwriter/gtkspellcheck/oxt_extract.py +++ /dev/null @@ -1,294 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Carlos Jenkins -# Copyright (C) 2012-2016, Maximilian Köhl -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -This module extracts the .dic and .aff (Hunspell) dictionaries from any given -.oxt extension. - -Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary -""" - -import functools -import gettext -import logging -import os -import shutil -import sys -import warnings -import xml.dom.minidom -import xml.parsers.expat -import zipfile - -# enable deprecation warnings -warnings.simplefilter('always', DeprecationWarning) - -# public objects -__all__ = ['extract_oxt', 'batch_extract', 'BadXml', 'BadExtensionFile', - 'ExtractPathIsNoDirectory', 'BATCH_SUCCESS', 'BATCH_ERROR', - 'BATCH_WARNING'] - -# logger -logger = logging.getLogger(__name__) - -# translation -locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major) -_ = gettext.translation(locale_name, fallback=True).gettext - -class BadXml(Exception): - """ - The XML dictionary registry is not valid XML. - """ - -class BadExtensionFile(Exception): - """ - The extension has a wrong file format, should be a ZIP file. - """ - -class ExtractPathIsNoDirectory(Exception): - """ - The given `extract_path` is no directory. - """ - - -def find_dictionaries(registry): - def oor_name(name, element): - return element.attributes['oor:name'].value.lower() == name - - def get_property(name, properties): - property = list(filter(functools.partial(oor_name, name), - properties)) - if property: - return property[0].getElementsByTagName('value')[0] - - result = [] - - # find all "node" elements which have "dictionaries" as "oor:name" attribute - for dictionaries in filter(functools.partial(oor_name, 'dictionaries'), - registry.getElementsByTagName('node')): - # for all "node" elements in this dictionary nodes - for dictionary in dictionaries.getElementsByTagName('node'): - # get all "prop" elements - properties = dictionary.getElementsByTagName('prop') - # get the format property as text - format = get_property('format', properties).firstChild.data.strip() - if format and format == 'DICT_SPELL': - # find the locations property - locations = get_property('locations', properties) - # if the location property is text: - # %origin%/dictionary.aff %origin%/dictionary.dic - if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE: - locations = locations.firstChild.data - locations = locations.replace('%origin%/', '').strip() - result.append(locations.split()) - # otherwise: - # %origin%/dictionary.aff %origin%/dictionary.dic - else: - locations = [item.firshChild.data.replace('%origin%/', '') \ - .strip() for item in - locations.getElementsByTagName('it')] - result.append(locations) - - return result - -def extract(filename, target, override=False): - """ - Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions. - - :param filename: path to the ``.oxt`` extension - :param target: path to extract Hunspell dictionaries to - :param override: override existing files in the target directory - :rtype: list of the extracted dictionaries - - This function extracts the Hunspell dictionaries (``.dic`` and ``.aff`` - files) from the given ``.oxt`` extension found to ``target``. - - Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary - """ - # TODO 5.0: remove this function - warnings.warn(('call to deprecated function "{}", ' - 'moved to separate package "oxt_extract", ' - 'will be removed in pygtkspellcheck 5.0').format(extract.__name__), - category=DeprecationWarning) - try: - with zipfile.ZipFile(filename, 'r') as extension: - files = extension.namelist() - - registry = 'dictionaries.xcu' - if not registry in files: - for filename in files: - if filename.lower().endswith(registry): - registry = filename - - if registry in files: - registry = xml.dom.minidom.parse(extension.open(registry)) - dictionaries = find_dictionaries(registry) - extracted = [] - for dictionary in dictionaries: - for filename in dictionary: - dict_file = os.path.join(target, - os.path.basename(filename)) - if (not os.path.exists(dict_file) - or (override and os.path.isfile(dict_file))): - if filename in files: - with open(dict_file, 'wb') as _target: - with extension.open(filename, 'r') as _source: - extracted.append(os.path.basename(filename)) - _target.write(_source.read()) - else: - logger.warning('dictionary exists in registry ' - 'but not in the extension zip') - else: - logging.warning(('dictionary file "{}" already exists ' - 'and not overriding it' - ).format(dict_file)) - return extracted - except zipfile.BadZipfile: - raise BadExtensionFile('extension is not a valid ZIP file') - except xml.parsers.expat.ExpatError: - raise BadXml('dictionary registry is not valid XML') - -BATCH_SUCCESS = 'success' -BATCH_ERROR = 'error' -BATCH_WARNING = 'warning' - -def batch_extract(oxt_path, extract_path, override=False, move_path=None): - """ - Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions. - - :param oxt_path: path to a directory containing the ``.oxt`` extensions - :param extract_path: path to extract Hunspell dictionaries files to - :param override: override already existing files - :param move_path: optional path to move the ``.oxt`` files after processing - :rtype: generator over all extensions, yielding result, extension name, - error, extracted dictionaries and translated error message - result - would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if - some error happened or :const:`BATCH_WARNING` which contain some warning - messages instead of errors - - This function extracts the Hunspell dictionaries (``.dic`` and ``.aff`` - files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to - the ``extract_path`` directory. - - Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary - - In detail, this functions does the following: - - 1. find all the ``.oxt`` extension files within ``oxt_path`` - 2. open (unzip) each extension - 3. find the dictionary definition file within (*dictionaries.xcu*) - 4. parse the dictionary definition file and locate the dictionaries files - 5. uncompress those files to ``extract_path`` - - - By default file overriding is disabled, set ``override`` parameter to True - if you want to enable it. As additional option, each processed extension can - be moved to ``move_path``. - - Example:: - - for result, name, error, dictionaries, message in oxt_extract.batch_extract(...): - if result == oxt_extract.BATCH_SUCCESS: - print('successfully extracted extension "{}"'.format(name)) - elif result == oxt_extract.BATCH_ERROR: - print('could not extract extension "{}"'.format(name)) - print(message) - print('error {}'.format(error)) - elif result == oxt_extract.BATCH_WARNING: - print('warning during processing extension "{}"'.format(name)) - print(message) - print(error) - - """ - - # TODO 5.0: remove this function - warnings.warn(('call to deprecated function "{}", ' - 'moved to separate package "oxt_extract", ' - 'will be removed in pygtkspellcheck 5.0').format(extract.__name__), - category=DeprecationWarning) - - # get the real, absolute and normalized path - oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path))) - - # check that the input directory exists - if not os.path.isdir(oxt_path): - return - - # create extract directory if not exists - if not os.path.exists(extract_path): - os.makedirs(extract_path) - - # check that the extract path is a directory - if not os.path.isdir(extract_path): - raise ExtractPathIsNoDirectory('extract path is not a valid directory') - - # get all .oxt extension at given path - oxt_files = [extension for extension in os.listdir(oxt_path) - if extension.lower().endswith('.oxt')] - - for extension_name in oxt_files: - extension_path = os.path.join(oxt_path, extension_name) - - try: - dictionaries = extract(extension_path, extract_path, override) - yield BATCH_SUCCESS, extension_name, None, dictionaries, '' - except BadExtensionFile as error: - logger.error(('extension "{}" is not a valid ZIP file' - ).format(extension_name)) - yield (BATCH_ERROR, extension_name, error, [], - _('extension "{}" is not a valid ZIP file' - ).format(extension_name)) - except BadXml as error: - logger.error(('extension "{}" has no valid XML dictionary registry' - ).format(extension_name)) - yield (BATCH_ERROR, extension_name, error, [], - _('extension "{}" has no valid XML dictionary registry' - ).format(extension_name)) - - # move the extension after processing if user requires it - if move_path is not None: - # create move path if it doesn't exists - if not os.path.exists(move_path): - os.makedirs(move_path) - # move to the given path only if it is a directory and target - # doesn't exists - if os.path.isdir(move_path): - if (not os.path.exists(os.path.join(move_path, extension_name)) - or override): - shutil.move(extension_path, move_path) - else: - logger.warning(('unable to move extension, file with same ' - 'name exists within move_path')) - yield (BATCH_WARNING, extension_name, - ('unable to move extension, file with same name ' - 'exists within move_path'), [], - _('unable to move extension, file with same name ' - 'exists within move_path')) - else: - logger.warning(('unable to move extension, move_path is not a ' - 'directory')) - yield (BATCH_WARNING, extension_name, - ('unable to move extension, move_path is not a ' - 'directory'), [], - _('unable to move extension, move_path is not a ' - 'directory')) \ No newline at end of file diff --git a/uberwriter/gtkspellcheck/spellcheck.py b/uberwriter/gtkspellcheck/spellcheck.py deleted file mode 100644 index 0112fbf..0000000 --- a/uberwriter/gtkspellcheck/spellcheck.py +++ /dev/null @@ -1,660 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -A simple but quite powerful spellchecking library written in pure Python for Gtk -based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with -automatic switching and binding detection. For automatic translation of the user -interface it can use Gedit’s translation files. -""" - -import enchant -import gettext -import logging -import re -import sys - -from uberwriter.pylocales import code_to_name as _code_to_name -from uberwriter.pylocales import LanguageNotFound, CountryNotFound - -# public objects -__all__ = ['SpellChecker', 'NoDictionariesFound', 'NoGtkBindingFound'] - -# logger -logger = logging.getLogger(__name__) - -class NoDictionariesFound(Exception): - """ - There aren't any dictionaries installed on the current system so - spellchecking could not work in any way. - """ - -class NoGtkBindingFound(Exception): - """ - Could not find any loaded Gtk binding. - """ - -if sys.version_info.major == 3: - _py3k = True -else: - _py3k = False - -if _py3k: - # there is only the gi binding for Python 3 - import gi - gi.require_version('Gtk', '3.0') - from gi.repository import Gtk as gtk - _pygobject = True -else: - # find any loaded gtk binding - if 'gi.repository.Gtk' in sys.modules: - gtk = sys.modules['gi.repository.Gtk'] - _pygobject = True - elif 'gtk' in sys.modules: - gtk = sys.modules['gtk'] - _pygobject = False - else: - raise NoGtkBindingFound('could not find any loaded Gtk binding') - -# select base list class -try: - from collections import UserList - _list = UserList -except ImportError: - _list = list - - - -# select base string -if _py3k: - basestring = str - -# map between Gedit's translation and PyGtkSpellcheck's -_GEDIT_MAP = {'Languages' : 'Languages', - 'Ignore All' : 'Ignore _All', - 'Suggestions' : 'Suggestions', - '(no suggestions)' : '(no suggested words)', - 'Add "{}" to Dictionary' : 'Add w_ord', - 'Unknown' : 'Unknown'} - -# translation -if gettext.find('gedit'): - _gedit = gettext.translation('gedit', fallback=True).gettext - _ = lambda message: _gedit(_GEDIT_MAP[message]).replace('_', '') -else: - locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major) - _ = gettext.translation(locale_name, fallback=True).gettext - -def code_to_name(code, separator='_'): - try: - return _code_to_name(code, separator) - except (LanguageNotFound, CountryNotFound): - return '{} ({})'.format(_('Unknown'), code) - -class SpellChecker(object): - """ - Main spellchecking class, everything important happens here. - - :param view: GtkTextView the SpellChecker should be attached to. - :param language: The language which should be used for spellchecking. - Use a combination of two letter lower-case ISO 639 language code with a - two letter upper-case ISO 3166 country code, for example en_US or de_DE. - :param prefix: A prefix for some internal GtkTextMarks. - :param collapse: Enclose suggestions in its own menu. - :param params: Dictionary with Enchant broker parameters that should be set - e.g. `enchant.myspell.dictionary.path`. - - .. attribute:: languages - - A list of supported languages. - - .. function:: exists(language) - - Checks if a language exists. - - :param language: language to check - """ - FILTER_WORD = 'word' - FILTER_LINE = 'line' - FILTER_TEXT = 'text' - - DEFAULT_FILTERS = {FILTER_WORD : [r'[0-9.,]+'], - FILTER_LINE : [(r'(https?|ftp|file):((//)|(\\\\))+[\w\d:' - r'#@%/;$()~_?+-=\\.&]+'), - r'[\w\d]+@[\w\d.]+'], - FILTER_TEXT : []} - - class _LanguageList(_list): - def __init__(self, *args, **kwargs): - if sys.version_info.major == 3: - super().__init__(*args, **kwargs) - else: - _list.__init__(self, *args, **kwargs) - self.mapping = dict(self) - - @classmethod - def from_broker(cls, broker): - return cls(sorted([(language, code_to_name(language)) - for language in broker.list_languages()], - key=lambda language: language[1])) - - def exists(self, language): - return language in self.mapping - - class _Mark(): - def __init__(self, buffer, name, start): - self._buffer = buffer - self._name = name - self._mark = self._buffer.create_mark(self._name, start, True) - - @property - def iter(self): - return self._buffer.get_iter_at_mark(self._mark) - - @property - def inside_word(self): - return self.iter.inside_word() - - @property - def word(self): - start = self.iter - if not start.starts_word(): - start.backward_word_start() - end = self.iter - if end.inside_word(): - end.forward_word_end() - return start, end - - def move(self, location): - self._buffer.move_mark(self._mark, location) - - def __init__(self, view, language='en', prefix='gtkspellchecker', - collapse=True, params={}): - self._view = view - self.collapse = collapse - self._view.connect('populate-popup', - lambda entry, menu:self._extend_menu(menu)) - self._view.connect('popup-menu', self._click_move_popup) - self._view.connect('button-press-event', self._click_move_button) - self._prefix = prefix - self._broker = enchant.Broker() - for param, value in params.items(): self._broker.set_param(param, value) - self.languages = SpellChecker._LanguageList.from_broker(self._broker) - if self.languages.exists(language): - self._language = language - elif self.languages.exists('en'): - logger.warning(('no installed dictionary for language "{}", ' - 'fallback to english'.format(language))) - self._language = 'en' - else: - if self.languages: - self._language = self.languages[0][0] - logger.warning(('no installed dictionary for language "{}" ' - 'and english, fallback to first language in' - 'language list ("{}")').format(language, - self._language)) - else: - logger.critical('no dictionaries found') - raise NoDictionariesFound() - self._dictionary = self._broker.request_dict(self._language) - self._deferred_check = False - self._filters = dict(SpellChecker.DEFAULT_FILTERS) - self._regexes = {SpellChecker.FILTER_WORD : re.compile('|'.join( - self._filters[SpellChecker.FILTER_WORD])), - SpellChecker.FILTER_LINE : re.compile('|'.join( - self._filters[SpellChecker.FILTER_LINE])), - SpellChecker.FILTER_TEXT : re.compile('|'.join( - self._filters[SpellChecker.FILTER_TEXT]), - re.MULTILINE)} - self._enabled = True - self.buffer_initialize() - - @property - def language(self): - """ - The language used for spellchecking. - """ - return self._language - - @language.setter - def language(self, language): - if language != self._language and self.languages.exists(language): - self._language = language - self._dictionary = self._broker.request_dict(language) - self.recheck() - - @property - def enabled(self): - """ - Enable or disable spellchecking. - """ - return self._enabled - - @enabled.setter - def enabled(self, enabled): - if enabled and not self._enabled: - self.enable() - elif not enabled and self._enabled: - self.disable() - - def buffer_initialize(self): - """ - Initialize the GtkTextBuffer associated with the GtkTextView. If you - have associated a new GtkTextBuffer with the GtkTextView call this - method. - """ - if _pygobject: - self._misspelled = gtk.TextTag.new('{}-misspelled'\ - .format(self._prefix)) - else: - self._misspelled = gtk.TextTag('{}-misspelled'.format(self._prefix)) - self._misspelled.set_property('underline', 4) - self._buffer = self._view.get_buffer() - self._buffer.connect('insert-text', self._before_text_insert) - self._buffer.connect_after('insert-text', self._after_text_insert) - self._buffer.connect_after('delete-range', self._range_delete) - self._buffer.connect_after('mark-set', self._mark_set) - start = self._buffer.get_bounds()[0] - self._marks = {'insert-start' : SpellChecker._Mark(self._buffer, - '{}-insert-start'.format(self._prefix), start), - 'insert-end' : SpellChecker._Mark(self._buffer, - '{}-insert-end'.format(self._prefix), start), - 'click' : SpellChecker._Mark(self._buffer, - '{}-click'.format(self._prefix), start)} - self._table = self._buffer.get_tag_table() - self._table.add(self._misspelled) - self.ignored_tags = [] - def tag_added(tag, *args): - if hasattr(tag, 'spell_check') and not getattr(tag, 'spell_check'): - self.ignored_tags.append(tag) - def tag_removed(tag, *args): - if tag in self.ignored_tags: - self.ignored_tags.remove(tag) - self._table.connect('tag-added', tag_added) - self._table.connect('tag-removed', tag_removed) - self._table.foreach(tag_added, None) - self.no_spell_check = self._table.lookup('no-spell-check') - if not self.no_spell_check: - if _pygobject: - self.no_spell_check = gtk.TextTag.new('no-spell-check') - else: - self.no_spell_check = gtk.TextTag('no-spell-check') - self._table.add(self.no_spell_check) - self.recheck() - - def recheck(self): - """ - Rechecks the spelling of the whole text. - """ - start, end = self._buffer.get_bounds() - self.check_range(start, end, True) - - def disable(self): - """ - Disable spellchecking. - """ - self._enabled = False - start, end = self._buffer.get_bounds() - self._buffer.remove_tag(self._misspelled, start, end) - - def enable(self): - """ - Enable spellchecking. - """ - self._enabled = True - self.recheck() - - def append_filter(self, regex, filter_type): - """ - Append a new filter to the filter list. Filters are useful to ignore - some misspelled words based on regular expressions. - - :param regex: The regex used for filtering. - :param filter_type: The type of the filter. - - Filter Types: - - :const:`SpellChecker.FILTER_WORD`: The regex must match the whole word - you want to filter. The word separation is done by Pango's word - separation algorithm so, for example, urls won't work here because - they are split in many words. - - :const:`SpellChecker.FILTER_LINE`: If the expression you want to match - is a single line expression use this type. It should not be an open - end expression because then the rest of the line with the text you - want to filter will become correct. - - :const:`SpellChecker.FILTER_TEXT`: Use this if you want to filter - multiline expressions. The regex will be compiled with the - `re.MULTILINE` flag. Same with open end expressions apply here. - """ - self._filters[filter_type].append(regex) - if filter_type == SpellChecker.FILTER_TEXT: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type]), re.MULTILINE) - else: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type])) - - def remove_filter(self, regex, filter_type): - """ - Remove a filter from the filter list. - - :param regex: The regex which used for filtering. - :param filter_type: The type of the filter. - """ - self._filters[filter_type].remove(regex) - if filter_type == SpellChecker.FILTER_TEXT: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type]), re.MULTILINE) - else: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type])) - - def append_ignore_tag(self, tag): - """ - Appends a tag to the list of ignored tags. A string will be automatic - resolved into a tag object. - - :param tag: Tag object or tag name. - """ - if isinstance(tag, basestring): - tag = self._table.lookup(tag) - self.ignored_tags.append(tag) - - def remove_ignore_tag(self, tag): - """ - Removes a tag from the list of ignored tags. A string will be automatic - resolved into a tag object. - - :param tag: Tag object or tag name. - """ - if isinstance(tag, basestring): - tag = self._table.lookup(tag) - self.ignored_tags.remove(tag) - - def add_to_dictionary(self, word): - """ - Adds a word to user's dictionary. - - :param word: The word to add. - """ - self._dictionary.add_to_pwl(word) - self.recheck() - - def ignore_all(self, word): - """ - Ignores a word for the current session. - - :param word: The word to ignore. - """ - self._dictionary.add_to_session(word) - self.recheck() - - def check_range(self, start, end, force_all=False): - """ - Checks a specified range between two GtkTextIters. - - :param start: Start iter - checking starts here. - :param end: End iter - checking ends here. - """ - if not self._enabled: - return - if end.inside_word(): end.forward_word_end() - if not start.starts_word() and (start.inside_word() or - start.ends_word()): - start.backward_word_start() - self._buffer.remove_tag(self._misspelled, start, end) - cursor = self._buffer.get_iter_at_mark(self._buffer.get_insert()) - precursor = cursor.copy() - precursor.backward_char() - highlight = (cursor.has_tag(self._misspelled) or - precursor.has_tag(self._misspelled)) - if not start.get_offset(): - start.forward_word_end() - start.backward_word_start() - word_start = start.copy() - while word_start.compare(end) < 0: - word_end = word_start.copy() - word_end.forward_word_end() - in_word = ((word_start.compare(cursor) < 0) and - (cursor.compare(word_end) <= 0)) - if in_word and not force_all: - if highlight: - self._check_word(word_start, word_end) - else: - self._deferred_check = True - else: - self._check_word(word_start, word_end) - self._deferred_check = False - word_end.forward_word_end() - word_end.backward_word_start() - if word_start.equal(word_end): - break - word_start = word_end.copy() - - def _languages_menu(self): - def _set_language(item, code): - self.language = code - if _pygobject: - menu = gtk.Menu.new() - group = [] - else: - menu = gtk.Menu() - group = gtk.RadioMenuItem() - connect = [] - for code, name in self.languages: - if _pygobject: - item = gtk.RadioMenuItem.new_with_label(group, name) - group.append(item) - else: - item = gtk.RadioMenuItem(group, name) - if code == self.language: - item.set_active(True) - connect.append((item, code)) - menu.append(item) - for item, code in connect: - item.connect('activate', _set_language, code) - return menu - - def _suggestion_menu(self, word): - menu = [] - suggestions = self._dictionary.suggest(word) - if not suggestions: - if _pygobject: - item = gtk.MenuItem.new() - label = gtk.Label.new('') - else: - item = gtk.MenuItem() - label = gtk.Label() - try: - label.set_halign(gtk.Align.LEFT) - except AttributeError: - label.set_alignment(0.0, 0.5) - label.set_markup('{text}'.format(text=_('(no suggestions)'))) - item.add(label) - menu.append(item) - else: - for suggestion in suggestions: - if _pygobject: - item = gtk.MenuItem.new() - label = gtk.Label.new('') - else: - item = gtk.MenuItem() - label = gtk.Label() - label.set_markup('{text}'.format(text=suggestion)) - try: - label.set_halign(gtk.Align.LEFT) - except AttributeError: - label.set_alignment(0.0, 0.5) - item.add(label) - item.connect('activate', self._replace_word, word, suggestion) - menu.append(item) - if _pygobject: - menu.append(gtk.SeparatorMenuItem.new()) - item = gtk.MenuItem.new_with_label( - _('Add "{}" to Dictionary').format(word)) - else: - menu.append(gtk.SeparatorMenuItem()) - item = gtk.MenuItem(_('Add "{}" to Dictionary').format(word)) - item.connect('activate', lambda *args: self.add_to_dictionary(word)) - menu.append(item) - if _pygobject: - item = gtk.MenuItem.new_with_label(_('Ignore All')) - else: - item = gtk.MenuItem(_('Ignore All')) - item.connect('activate', lambda *args: self.ignore_all(word)) - menu.append(item) - return menu - - def _extend_menu(self, menu): - if not self._enabled: - return - if _pygobject: - separator = gtk.SeparatorMenuItem.new() - else: - separator = gtk.SeparatorMenuItem() - separator.show() - menu.prepend(separator) - if _pygobject: - languages = gtk.MenuItem.new_with_label(_('Languages')) - else: - languages = gtk.MenuItem(_('Languages')) - languages.set_submenu(self._languages_menu()) - languages.show_all() - menu.prepend(languages) - if self._marks['click'].inside_word: - start, end = self._marks['click'].word - if start.has_tag(self._misspelled): - if _py3k: - word = self._buffer.get_text(start, end, False) - else: - word = self._buffer.get_text(start, end, - False).decode('utf-8') - items = self._suggestion_menu(word) - if self.collapse: - if _pygobject: - suggestions = gtk.MenuItem.new_with_label( - _('Suggestions')) - submenu = gtk.Menu.new() - else: - suggestions = gtk.MenuItem(_('Suggestions')) - submenu = gtk.Menu() - for item in items: - submenu.append(item) - suggestions.set_submenu(submenu) - suggestions.show_all() - menu.prepend(suggestions) - else: - items.reverse() - for item in items: - menu.prepend(item) - menu.show_all() - - def _click_move_popup(self, *args): - self._marks['click'].move(self._buffer.get_iter_at_mark( - self._buffer.get_insert())) - return False - - def _click_move_button(self, widget, event): - if event.button == 3: - if self._deferred_check: self._check_deferred_range(True) - x, y = self._view.window_to_buffer_coords(2, int(event.x), - int(event.y)) - iter = self._view.get_iter_at_location(x, y) - if isinstance(iter, tuple): - iter = iter[1] - self._marks['click'].move(iter) - return False - - def _before_text_insert(self, textbuffer, location, text, length): - self._marks['insert-start'].move(location) - - def _after_text_insert(self, textbuffer, location, text, length): - start = self._marks['insert-start'].iter - self.check_range(start, location) - self._marks['insert-end'].move(location) - - def _range_delete(self, textbuffer, start, end): - self.check_range(start, end) - - def _mark_set(self, textbuffer, location, mark): - if mark == self._buffer.get_insert() and self._deferred_check: - self._check_deferred_range(False) - - def _replace_word(self, item, old_word, new_word): - start, end = self._marks['click'].word - offset = start.get_offset() - self._buffer.begin_user_action() - self._buffer.delete(start, end) - self._buffer.insert(self._buffer.get_iter_at_offset(offset), new_word) - self._buffer.end_user_action() - self._dictionary.store_replacement(old_word, new_word) - - def _check_deferred_range(self, force_all): - start = self._marks['insert-start'].iter - end = self._marks['insert-end'].iter - self.check_range(start, end, force_all) - - def _check_word(self, start, end): - if start.has_tag(self.no_spell_check): - return - for tag in self.ignored_tags: - if start.has_tag(tag): - return - if _py3k: - word = self._buffer.get_text(start, end, False).strip() - else: - word = self._buffer.get_text(start, end, False).decode('utf-8').strip() - if not word: - return - if len(self._filters[SpellChecker.FILTER_WORD]): - if self._regexes[SpellChecker.FILTER_WORD].match(word): - return - if len(self._filters[SpellChecker.FILTER_LINE]): - line_start = self._buffer.get_iter_at_line(start.get_line()) - line_end = end.copy() - line_end.forward_to_line_end() - if _py3k: - line = self._buffer.get_text(line_start, line_end, False) - else: - line = self._buffer.get_text(line_start, line_end, - False).decode('utf-8') - for match in self._regexes[SpellChecker.FILTER_LINE].finditer(line): - if match.start() <= start.get_line_offset() <= match.end(): - start = self._buffer.get_iter_at_line_offset( - start.get_line(), match.start()) - end = self._buffer.get_iter_at_line_offset(start.get_line(), - match.end()) - self._buffer.remove_tag(self._misspelled, start, end) - return - if len(self._filters[SpellChecker.FILTER_TEXT]): - text_start, text_end = self._buffer.get_bounds() - if _py3k: - text = self._buffer.get_text(text_start, text_end, False) - else: - text = self._buffer.get_text(text_start, text_end, - False).decode('utf-8') - for match in self._regexes[SpellChecker.FILTER_TEXT].finditer(text): - if match.start() <= start.get_offset() <= match.end(): - start = self._buffer.get_iter_at_offset(match.start()) - end = self._buffer.get_iter_at_offset(match.end()) - self._buffer.remove_tag(self._misspelled, start, end) - return - if not self._dictionary.check(word): - self._buffer.apply_tag(self._misspelled, start, end) diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py index 006c253..7436372 100644 --- a/uberwriter/text_view.py +++ b/uberwriter/text_view.py @@ -8,7 +8,8 @@ from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, from uberwriter.scroller import Scroller gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, GObject, GLib +gi.require_version('Gspell', '1') +from gi.repository import Gtk, Gdk, GObject, GLib, Gspell import logging LOGGER = logging.getLogger('uberwriter') @@ -52,6 +53,10 @@ class TextView(Gtk.TextView): self.get_buffer().connect('changed', self.on_text_changed) self.connect('size-allocate', self.on_size_allocate) + # Spell checking + self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self) + self.gspell_view.basic_setup() + # Undo / redo self.undo_redo = UndoRedoHandler() self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text) @@ -115,6 +120,7 @@ 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() diff --git a/uberwriter/window.py b/uberwriter/window.py index 271fc82..10cdb97 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -38,7 +38,6 @@ import cairo from uberwriter import helpers from uberwriter.theme import Theme from uberwriter.helpers import get_builder -from uberwriter.gtkspellcheck import SpellChecker from uberwriter.sidebar import Sidebar from uberwriter.search_and_replace import SearchAndReplace @@ -223,8 +222,6 @@ class Window(Gtk.ApplicationWindow): focus_mode = state.get_boolean() self.text_view.set_focus_mode(focus_mode) - if self.spell_checker: - self.spell_checker._misspelled.set_property('underline', 0 if focus_mode else 4) self.text_view.grab_focus() def set_hemingway_mode(self, state): @@ -482,39 +479,9 @@ class Window(Gtk.ApplicationWindow): status {gtk bool} -- Desired status of the spellchecking """ - if state.get_boolean(): - try: - self.spell_checker.enable() - except: - try: - self.spell_checker = SpellChecker( - self.text_view, locale.getdefaultlocale()[0], - collapse=False) - if self.auto_correct: - self.auto_correct.set_language(self.spell_checker.language) - self.spell_checker.connect_language_change( # pylint: disable=no-member - self.auto_correct.set_language) - except: - self.spell_checker = None - dialog = Gtk.MessageDialog(self, - Gtk.DialogFlags.MODAL \ - | Gtk.DialogFlags.DESTROY_WITH_PARENT, - Gtk.MessageType.INFO, - Gtk.ButtonsType.NONE, - _("You can not enable the Spell Checker.") - ) - dialog.format_secondary_text( - _("Please install 'hunspell' or 'aspell' dictionaries" - + " for your language from the software center.")) - _response = dialog.run() - return - return - else: - try: - self.spell_checker.disable() - except: - pass - return + self.text_view.gspell_view\ + .set_inline_spell_checking(state.get_boolean() + and not self.text_view.focus_mode) def toggle_gradient_overlay(self, state): """Toggle the gradient overlay From cd6e5a86aafc0a0f8a5e842193a81d4fd0198bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Genov=C3=A9s?= Date: Wed, 1 May 2019 16:57:04 +0200 Subject: [PATCH 54/54] linting --- flatpak/uberwriter.json | 153 ++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 75 deletions(-) diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index 05b3183..5f62444 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -1,10 +1,10 @@ { - "app-id": "de.wolfvollprecht.UberWriter", - "runtime": "org.gnome.Platform", - "runtime-version": "3.32", - "sdk": "org.gnome.Sdk", - "command": "start-uberwriter", - "finish-args": [ + "app-id":"de.wolfvollprecht.UberWriter", + "runtime":"org.gnome.Platform", + "runtime-version":"3.32", + "sdk":"org.gnome.Sdk", + "command":"start-uberwriter", + "finish-args":[ "--socket=x11", "--socket=wayland", "--share=ipc", @@ -15,98 +15,101 @@ "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], - "add-extensions": { - "de.wolfvollprecht.UberWriter.Plugin": { - "directory": "extensions", - "version": "stable", - "subdirectories": true, - "no-autodownload": true, - "autodelete": true - } + "add-extensions":{ + "de.wolfvollprecht.UberWriter.Plugin":{ + "directory":"extensions", + "version":"stable", + "subdirectories":true, + "no-autodownload":true, + "autodelete":true + } }, - "modules": [ + "modules":[ { - "name": "enchant", - "config-opts": ["--disable-static", "--with-myspell-dir=/usr/share/myspell"], - "cleanup": [ - "/bin" - ], - "sources": [ - { - "type" : "archive", - "url" : "https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", - "sha256" : "bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" - } - ] - }, - { - "name": "fonts", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/fonts/", - "cp ttf/* /app/share/fonts/" + "name":"enchant", + "config-opts":[ + "--disable-static", + "--with-myspell-dir=/usr/share/myspell" ], - "sources": [ + "cleanup":[ + "/bin" + ], + "sources":[ { - "type": "git", - "url": "https://github.com/mozilla/Fira", - "tag": "4.202" + "type":"archive", + "url":"https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", + "sha256":"bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" } ] }, { - "name": "pandoc", - "only-arches": [ - "x86_64" + "name":"fonts", + "buildsystem":"simple", + "build-commands":[ + "mkdir -p /app/share/fonts/", + "cp ttf/* /app/share/fonts/" ], - "buildsystem": "simple", - "build-commands": [ - "install -Dm 755 bin/pandoc /app/bin/pandoc", - "install -Dm 755 bin/pandoc-citeproc /app/bin/pandoc-citeproc" - ], - "sources": [ + "sources":[ { - "type": "archive", - "url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", - "sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" + "type":"git", + "url":"https://github.com/mozilla/Fira", + "tag":"4.202" + } + ] + }, + { + "name":"pandoc", + "only-arches":[ + "x86_64" + ], + "buildsystem":"simple", + "build-commands":[ + "install -Dm 755 bin/pandoc /app/bin/pandoc", + "install -Dm 755 bin/pandoc-citeproc /app/bin/pandoc-citeproc" + ], + "sources":[ + { + "type":"archive", + "url":"https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", + "sha256":"06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" } ] }, "de.wolfvollprecht.UberWriter.pipdeps.json", { - "name": "uberwriter", - "buildsystem": "simple", - "build-commands": [ - "desktop-file-edit --set-key=Exec --set-value='uberwriter.in %U' data/de.wolfvollprecht.UberWriter.desktop", - "python3 -m pip install --prefix=/app --install-option=--optimize=1 ." - ], - "sources": [ + "name":"uberwriter", + "buildsystem":"simple", + "build-commands":[ + "desktop-file-edit --set-key=Exec --set-value='uberwriter.in %U' data/de.wolfvollprecht.UberWriter.desktop", + "python3 -m pip install --prefix=/app --install-option=--optimize=1 ." + ], + "sources":[ { - "type" : "dir", - "path" : "../" + "type":"dir", + "path":"../" } ], - "post-install": [ + "post-install":[ "install -d /app/extensions", "glib-compile-schemas /app/share/glib-2.0/schemas" ] }, { - "name": "scripts", - "buildsystem": "simple", - "build-commands": [ + "name":"scripts", + "buildsystem":"simple", + "build-commands":[ "install -Dm 755 start-uberwriter.sh /app/bin/start-uberwriter" - ], - "sources": [ - { - "type": "script", - "dest-filename": "start-uberwriter.sh", - "commands": [ - "export PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/$(uname -a)-linux:$PATH", - "exec uberwriter.in \"$@\"" - ] - } - ] + ], + "sources":[ + { + "type":"script", + "dest-filename":"start-uberwriter.sh", + "commands":[ + "export PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/$(uname -a)-linux:$PATH", + "exec uberwriter.in \"$@\"" + ] + } + ] } ] -} +} \ No newline at end of file