diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index e7fdb28..7a3b93b 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -4,35 +4,47 @@ + + 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 - 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 + Check spelling while typing - Enable/disable spellchecking in the application. + 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 + 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 - 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/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/Export.ui b/data/ui/Export.ui index 011d433..3ea4419 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 @@ -42,7 +49,6 @@ False Pandoc can automatically make "--" to a long dash and more start - True True @@ -120,7 +126,7 @@ - Slideshow incremental bullets + Slideshow Incremental Bullets False True True @@ -158,6 +164,7 @@ True False center + 8 vertical @@ -174,18 +181,16 @@ True False - 5 - 5 + 4 + 4 vertical - Highlight syntax - False + Highlight Syntax True True False start - True True @@ -198,11 +203,13 @@ True False + 4 True False Choose a color theme for syntax highlighting + 4 Highlight style @@ -216,6 +223,8 @@ True False Choose a color theme for syntax highlighting + 8 + 8 0 0 @@ -250,7 +259,7 @@ True False - <b>Syntax highlighting</b> (HTML, LaTeX) + <b>Syntax Highlighting</b> (HTML, LaTeX) True @@ -271,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 + + @@ -286,7 +322,7 @@ True False - <b>Bibliography File</b> + <b>Bibliography </b> True @@ -320,13 +356,13 @@ True False - 5 - 5 + 4 + 4 vertical True - Self Contained + Self-contained False True True @@ -343,13 +379,14 @@ - HTML 5 + HTML5 False True True False - Use HTML 5 syntax + Use HTML5 syntax start + True True @@ -367,6 +404,7 @@ True False Choose a CSS File that you want to use + 4 CSS File @@ -380,6 +418,8 @@ True False Choose a CSS File that you want to use + 8 + 8 True @@ -426,7 +466,7 @@ True True none - http://johnmacfarlane.net/pandoc/README.html + https://pandoc.org/MANUAL.html 0 @@ -547,6 +587,19 @@ True False crossfade + + + True + False + save + html_filter + + + html + HTML + 2 + + True @@ -570,32 +623,6 @@ 1 - - - True - False - save - html_filter - - - html - HTML - 2 - - - - - True - False - save - odt_filter - - - odt - ODT - 3 - - True 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 3880dc1..b057517 100644 --- a/data/ui/Preferences.ui +++ b/data/ui/Preferences.ui @@ -2,6 +2,11 @@ + + True + False + dialog-information-symbolic + False True @@ -30,19 +35,18 @@ False center center - 30 - 30 - 30 - 30 - 10 - 10 + 16 + 16 + 16 + 16 + 8 + 8 - + True False - end - start - Use dark mode + start + Set dark mode automatically right @@ -51,11 +55,22 @@ - + + True + True + end + + + 2 + 0 + + + + True False - end - Autospellcheck + start + Force dark mode right @@ -64,67 +79,122 @@ - + + True + True + end + + + 2 + 1 + + + + True False + start + Check spelling while typing + right + + + 0 + 2 + + + + + True + True end + + + 2 + 2 + + + + + True + False + start Draw scroll gradient right 0 - 2 + 3 - + True True - app.dark_mode + end - 1 - 0 + 2 + 3 - + True - True - app.spellcheck + False + start + Input format + right - 1 - 1 + 0 + 4 - + True True - start - app.draw_gradient + False + help 1 - 2 + 4 - - - True - - - - - True - False - page 1 + + + True + True + end + 0 + 0 + + + 2 + 4 + + + + + + + + + + + + + + False + + + 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/UberwriterWindow.ui b/data/ui/Window.ui similarity index 96% rename from data/ui/UberwriterWindow.ui rename to data/ui/Window.ui index 162d6f9..66571fe 100644 --- a/data/ui/UberwriterWindow.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 @@ -186,23 +184,16 @@ natural none - + + 500 True - False - center + True + True True True + adjustment1 - - 500 - True - True - True - adjustment1 - - - - + @@ -290,6 +281,7 @@ True True True + Next Match avall @@ -335,6 +327,7 @@ True True True + Regular Expression False @@ -347,6 +340,7 @@ True True True + Open Replace reemplaza 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 @@ - - - - - - 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 c63bf5b..e4a98a9 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.~~ @@ -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 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/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/__init__.py b/uberwriter/__init__.py index f062c1e..8632281 100644 --- a/uberwriter/__init__.py +++ b/uberwriter/__init__.py @@ -15,36 +15,21 @@ ### 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 from uberwriter.helpers import set_up_logging -from uberwriter.uberwriterconfig import get_version +from uberwriter.config 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 - # ~ 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 304fed1..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 @@ -41,74 +38,7 @@ class Application(Gtk.Application): 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) - - set_dark_mode = self.settings.get_value("dark-mode") - action = Gio.SimpleAction.new_stateful("dark_mode", - None, - GLib.Variant.new_boolean(set_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) - - set_spellcheck = self.settings.get_value("spellcheck") - action = Gio.SimpleAction.new_stateful("spellcheck", - None, - GLib.Variant.new_boolean(set_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)) - 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) @@ -122,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) @@ -138,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 @@ -163,8 +129,6 @@ class Application(Gtk.Application): self.set_accels_for_action("app.save_as", ["s"]) self.set_accels_for_action("app.quit", ["w", "q"]) - 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: @@ -174,8 +138,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() @@ -187,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) @@ -196,90 +157,15 @@ 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") - 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( - "http://johnmacfarlane.net/pandoc/README.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(self, action, value): - action.set_state(value) - self.settings.set_value("dark-mode", GLib.Variant("b", value)) - - # this changes the headerbar theme accordingly - self.apply_current_theme() - - # adjust window for theme - 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() @@ -290,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/uberwriterconfig.py b/uberwriter/config.py similarity index 100% rename from uberwriter/uberwriterconfig.py rename to uberwriter/config.py diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py index 7a2362f..d65b583 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 @@ -35,6 +32,7 @@ from uberwriter.helpers import get_builder LOGGER = logging.getLogger('uberwriter') + class Export: """ Manages all the export operations and dialogs @@ -42,6 +40,114 @@ class Export: __gtype_name__ = "export_dialog" + formats = [ + { + "name": "LaTeX (pdf)", + "ext": "pdf", + "to": "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": "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", + "ext": "txt", + "to": "textile" + }, + { + "name": "reStructuredText", + "ext": "txt", + "to": "rst" + }, + { + "name": "MediaWiki Markup", + "ext": "txt", + "to": "mediawiki" + }, + { + "name": "OpenDocument (xml)", + "ext": "xml", + "to": "opendocument" + }, + { + "name": "OpenDocument (texi)", + "ext": "texi", + "to": "texinfo" + }, + { + "name": "OpenOffice Text Document", + "ext": "odt", + "to": "odt" + }, + { + "name": "Microsoft Word (docx)", + "ext": "docx", + "to": "docx" + }, + { + "name": "Rich Text Format", + "ext": "rtf", + "to": "rtf" + }, + { + "name": "Groff Man", + "ext": "man", + "to": "man" + }, + { + "name": "EPUB v3", + "ext": "epub", + "to": "epub" + } + ] + def __init__(self, filename): """Set up the about dialog""" self.builder = get_builder('Export') @@ -52,8 +158,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", "advanced"]} for export_format, filechooser in self.filechoosers.items(): filechooser.set_do_overwrite_confirmation(True) filechooser.set_current_folder(os.path.dirname(filename)) @@ -75,9 +181,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) @@ -86,171 +195,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 @@ -264,78 +258,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: @@ -349,8 +314,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/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 e70b04e..6ee4ed8 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 @@ -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_duration = 750 self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE self.hb_revealer.show() self.hb_revealer.set_reveal_child(True) - self.hb_container = Gtk.Frame(name='titlebar_container') + self.hb_container = 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() @@ -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/helpers.py b/uberwriter/helpers.py index efd0ef5..e7f2c03 100644 --- a/uberwriter/helpers.py +++ b/uberwriter/helpers.py @@ -21,12 +21,16 @@ 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 -from uberwriter.uberwriterconfig import get_data_file +from uberwriter.config import get_data_file from uberwriter.builder import Builder @@ -48,13 +52,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): @@ -160,6 +169,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 +198,14 @@ 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()) + + +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/inline_preview.py b/uberwriter/inline_preview.py index 6396411..ae5f9d9 100644 --- a/uberwriter/inline_preview.py +++ b/uberwriter/inline_preview.py @@ -14,32 +14,29 @@ # 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? - # 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 @@ -240,7 +237,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 +245,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 +261,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 +304,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 +360,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 c413482..0000000 --- a/uberwriter/markup_buffer.py +++ /dev/null @@ -1,321 +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(): - - def __init__(self, Parent, TextBuffer, base_leftmargin): - self.multiplier = 10 - self.parent = Parent - self.text_buffer = TextBuffer - - # 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('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() - - 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') - if not found: - (_, color) = self.parent.get_style_context().lookup_color('foreground_color') - - self.math_text.set_property("foreground", color.to_string()) - - 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.multiplier = multiplier - - def recalculate(self, lm): - multiplier = self.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/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/scroller.py b/uberwriter/scroller.py new file mode 100644 index 0000000..518167f --- /dev/null +++ b/uberwriter/scroller.py @@ -0,0 +1,48 @@ +class Scroller: + def __init__(self, scrolled_window, source_pos, target_pos): + super().__init__() + + self.scrolled_window = scrolled_window + self.source_pos = source_pos + self.target_pos = target_pos + self.duration = max(200, (target_pos - source_pos) / 50) * 1000 + + self.is_started = False + self.is_setup = False + self.start_time = 0 + self.end_time = 0 + self.tick_callback_id = 0 + + def start(self): + self.is_started = True + self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick) + + def end(self): + self.scrolled_window.remove_tick_callback(self.tick_callback_id) + self.is_started = False + + def setup(self, time): + self.start_time = time + self.end_time = time + self.duration + self.is_setup = True + + def on_tick(self, widget, frame_clock): + def ease_out_cubic(value): + return pow(value - 1, 3) + 1 + + now = frame_clock.get_frame_time() + if not self.is_setup: + self.setup(now) + + if now < self.end_time: + time = float(now - self.start_time) / float(self.end_time - self.start_time) + else: + time = 1 + self.end() + + time = ease_out_cubic(time) + pos = self.source_pos + (time * (self.target_pos - self.source_pos)) + widget.get_vadjustment().props.value = pos + return True + + diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py index 63c40a0..24fb1d9 100644 --- a/uberwriter/search_and_replace.py +++ b/uberwriter/search_and_replace.py @@ -14,22 +14,30 @@ # 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.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") @@ -41,9 +49,6 @@ 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.nextbutton = parentwindow.builder.get_object("next_result") self.prevbutton = parentwindow.builder.get_object("previous_result") self.regexbutton = parentwindow.builder.get_object("regex") @@ -63,18 +68,17 @@ 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.texteditor.connect("focus-in-event", self.focused_texteditor) + 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 """ - 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): @@ -100,13 +104,11 @@ 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 - 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) @@ -121,12 +123,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) @@ -139,43 +141,40 @@ 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): - self.active = index - else: - self.active = 0 + self.active = index % len(self.matches) - matchiter = self.matchiters[self.active] - self.texteditor.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) self.box.set_reveal_child(False) 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) 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.parentwindow.MarkupBuffer.markup_buffer() 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()) 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/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/text_editor.py b/uberwriter/text_editor.py deleted file mode 100644 index a94a1b2..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 TextEditor(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 = TextEditor() - 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..df945df --- /dev/null +++ b/uberwriter/text_view.py @@ -0,0 +1,201 @@ +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.scroller import Scroller + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, GObject, GLib + +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) + + # Preview popover + self.preview_popover = InlinePreview(self) + + # Drag and drop + self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT) + + # Scrolling + self.scroller = None + 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) + + def on_text_changed(self, *_): + self.markup.apply() + GLib.idle_add(self.scroll_to) + + def on_size_allocate(self, *_): + self.update_vertical_margin() + self.markup.update_margins_indents() + + 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() + 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.""" + + 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 + + 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, va.props.value, target_pos) + self.scroller.start() + + def on_mark_set(self, _text_buffer, _location, mark, _data=None): + if mark.get_name() == 'insert': + self.markup.apply() + if self.focus_mode: + self.scroll_to(mark) + elif mark.get_name() == 'gtk_drag_target': + self.scroll_to(mark) + return True 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..029871a --- /dev/null +++ b/uberwriter/text_view_markup_handler.py @@ -0,0 +1,307 @@ +# -*- 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 gi.overrides import GLib + +from uberwriter import helpers + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +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"~~.+?~~"), + "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), + "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): + 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.strikethrough = buffer.create_tag('strikethrough', strikethrough=True) + + self.horizontalrule = buffer.create_tag('centertext', + justification=Gtk.Justification.CENTER) + + 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) + self.table.set_property('pixels-above-lines', 0) + self.table.set_property('pixels-below-lines', 0) + + self.mathtext = buffer.create_tag('mathtext') + + 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.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): + self.version = self.version + 1 + if self.text_buffer.get_char_count() < self.max_char_sync: + self.do_apply() + else: + 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) + + # 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.horizontalrule, start, end) + buffer.remove_tag(self.plaintext, start, end) + buffer.remove_tag(self.table, start, end) + buffer.remove_tag(self.mathtext, start, end) + 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) + 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) + + 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: + 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. "* ") + length = 2 + nest = len(match.group(1).replace(" ", "\t")) + 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) + 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. ") + length = len(match.group(2)) + 1 + nest = len(match.group(1).replace(" ", "\t")) + 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_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()) + 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) + 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 "```" 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.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) + 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) + + # 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 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(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 + tab_array = Pango.TabArray.new(1, True) + tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width) + self.text_view.set_tabs(tab_array) + + # Adjust margins and indents, as character width can change + for level, tag in self.margins_indents.items(): + margin, indent = self.get_margin_indent(*level, baseline_margin, char_width) + tag.set_property("left-margin", margin) + tag.set_property("indent", indent) diff --git a/uberwriter/text_view_undo_redo_handler.py b/uberwriter/text_view_undo_redo_handler.py new file mode 100644 index 0000000..f91903c --- /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=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.""" + + 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/theme.py b/uberwriter/theme.py index e43ec57..8139c90 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): @@ -29,16 +30,30 @@ 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() 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) + changed = current_theme != cls.previous + 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 \ + 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 = [ # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita 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() diff --git a/uberwriter/window.py b/uberwriter/window.py index ba784c7..f541773 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -17,43 +17,35 @@ import codecs import locale import logging -import mimetypes import os import re -import subprocess import urllib import webbrowser from gettext import gettext as _ import gi -from gi.repository.GObject import param_spec_string +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 -# 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 @@ -62,10 +54,17 @@ 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__ = { + '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) @@ -76,38 +75,37 @@ class Window(Gtk.ApplicationWindow): application=Gio.Application.get_default(), title="Uberwriter") - self.builder = get_builder('UberwriterWindow') - self.add(self.builder.get_object("FullscreenOverlay")) + # Set UI + self.builder = get_builder('Window') + 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) - 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) - 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) @@ -116,116 +114,30 @@ class Window(Gtk.ApplicationWindow): self.add_accel_group(self.accel_group) # Setup text editor - self.text_editor = TextEditor() - 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.get_style_context().connect('changed', self.style_changed) - - 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. # Let them disable it - - 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) + self.overlay_id = None + self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay")) # Init file name with None self.set_filename() - # Markup and Shortcuts for the TextBuffer - self.markup_buffer = MarkupBuffer( - self, self.text_buffer, 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 - 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")) @@ -243,87 +155,49 @@ 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) 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() + self.get_style_context().add_class('uberwriter-window') - 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_changed() + if changed: + # Set theme variant (dark/light) + Gtk.Settings.get_default().set_property( + "gtk-application-prefer-dark-theme", + GLib.Variant("b", theme.is_dark)) - self.markup_buffer.update_style() + # 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) - # Reload preview if it exists, otherwise redraw contents of window (self) - if self.preview_webview: - self.show_preview() - else: + # Reload preview if it exists + self.reload_preview() + + # 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" @@ -336,12 +210,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 """ @@ -351,12 +220,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 @@ -374,184 +239,61 @@ 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, event=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 + # + # 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 = 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") + font_size = 14 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 < 1280: + font_size = 16 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") + font_size = 18 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) + 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 - 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.text_view.set_left_margin(horizontal_margin) + self.text_view.set_right_margin(horizontal_margin) self.scrolled_window.props.width_request = width_request - alloc = self.text_editor.get_allocation() - alloc.width = width_request - self.text_editor.size_allocate(alloc) - - def 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): @@ -563,7 +305,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 @@ -574,7 +316,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, @@ -597,7 +339,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) @@ -638,7 +380,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) @@ -661,15 +403,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.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): @@ -679,19 +415,24 @@ 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"), + _("Open a .md file"), self, Gtk.FileChooserAction.OPEN, ("_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() @@ -705,14 +446,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) @@ -739,9 +480,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() @@ -752,20 +491,20 @@ 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: 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) @@ -781,7 +520,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 @@ -793,45 +532,17 @@ 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) + def toggle_gradient_overlay(self, state): + """Toggle the gradient overlay - 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) + Arguments: + state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled) + """ - 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 + 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 @@ -849,8 +560,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() @@ -862,49 +573,17 @@ 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: - base_path = os.path.dirname(self.filename) - else: - base_path = '' - os.environ['PANDOC_PREFIX'] = base_path + '/' - - args = ['pandoc', - '-s', - '--from=markdown', - '--to=html5', + args = ['--standalone', '--mathjax', '--css=' + Theme.get_current().web_css_path, - '--quiet', '--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.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() 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) @@ -912,7 +591,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.""" @@ -923,19 +606,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: @@ -967,7 +645,7 @@ class Window(Gtk.ApplicationWindow): 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() @@ -991,7 +669,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) @@ -1064,24 +742,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. """ @@ -1091,7 +751,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?