diff --git a/.gitignore b/.gitignore index d5e4595..564b95d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,15 +5,13 @@ build/ debian/uberwriter/DEBIAN debian/uberwriter/opt debian/uberwriter/usr -bin/ flatpak/* -!flatpak/fonts-download -!flatpak/pandoc-download -!flatpak/pip-download !flatpak/uberwriter.json !flatpak/de.wolfvollprecht.UberWriter.* !flatpak/flatpak_texlive.json !flatpak/texlive_install.sh +!flatpak/python3-enchant.json +!flatpak/python3-packages.json *.py~ data/ui/shortcut_handlers *.ui~ @@ -21,3 +19,5 @@ data/ui/shortcut_handlers *.glade~ dist/uberwriter-2.0b0-py3.7.egg builddir/* +dist/ +uberwriter.egg-info diff --git a/Makefile b/Makefile index 4e6ad1e..0f57ea2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ -all: - python3 ./setup.py build +.PHONY: flatpak-user-install flatpak-generate-python-modules -install: - python3 ./setup.py install --prefix=/app --skip-build --optimize=1 +flatpak-user-install: + cd flatpak; flatpak-builder --force-clean --install --user _build uberwriter.json +flatpak-generate-python-modules: + # gtkspellcheck's setup.py wants enchant to already be installed + flatpak-pip-generator --output flatpak/python3-enchant.json pyenchant + flatpak-pip-generator --output flatpak/python3-packages.json `grep -v enchant requirements.txt` diff --git a/PKGBUILD b/PKGBUILD index b7b4cb2..24c2817 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,7 +6,7 @@ pkgdesc='A distraction free Markdown editor for GNU/Linux made with GTK+' arch=('any') url='http://uberwriter.github.io/uberwriter/' license=('GPL3') -depends=('gtk3' 'pandoc' 'python-gtkspellcheck') +depends=('gtk3' 'pandoc' 'gspell') makedepends=('python-setuptools') optdepends=('texlive-core' 'otf-fira-mono: Recommended font') provides=("$_pkgname") diff --git a/README.md b/README.md index 802d3f9..0c367dc 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ but you'll need to install and compile the schemas before: It's also possible to build, run and debug a flatpak package. You'll need flatpak-builder for this: -- cd to the flatpak dir of the repo -- `flatpak-builder --install --force-clean some_folder_name uberwriter.json` (this installs and cleans the build folder) +- `make flatpak-user-install` (this installs the Flatpak) - `flatpak run de.wolfvollprecht.UberWriter` If you can't find Uberwriter after this, it's due to a Flatpak bug. Try to export it to a local repo before installing it: @@ -54,4 +53,4 @@ If you want to update an existing installation, just run You can also debug it with the following: `flatpak-builder --run --share=network some_folder_name uberwriter.json sh` -If you want to install it using setuptools, simply run `python3 setup.py build install` \ No newline at end of file +If you want to install it using setuptools, simply run `python3 setup.py build install` diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index 8b13789..0000000 --- a/autogen.sh +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bin/uberwriter b/bin/uberwriter new file mode 100755 index 0000000..69fcd8b --- /dev/null +++ b/bin/uberwriter @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- +### BEGIN LICENSE +# Copyright (C) 2012, Wolf Vollprecht +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +### END LICENSE + +### DO NOT EDIT THIS FILE ### + +import sys +import os + +import pkg_resources + +import gettext +import locale + +# Add project root directory (enable symlink and trunk execution) +PROJECT_ROOT_DIRECTORY = os.path.abspath( + os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0])))) + +# Set the path if needed. This allows uberwriter to run without installing it :) +python_path = [] +if os.path.abspath(__file__).startswith('/opt'): + gettext.bindtextdomain('uberwriter', '/opt/extras.ubuntu.com/uberwriter/share/locale') + syspath = sys.path[:] # copy to avoid infinite loop in pending objects + for path in syspath: + opt_path = path.replace('/usr', '/opt/extras.ubuntu.com/uberwriter') + python_path.insert(0, opt_path) + sys.path.insert(0, opt_path) + os.putenv("XDG_DATA_DIRS", "%s:%s" % ("/opt/extras.ubuntu.com/uberwriter/share/", os.getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/"))) +if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'uberwriter')) + and PROJECT_ROOT_DIRECTORY not in sys.path): + python_path.insert(0, PROJECT_ROOT_DIRECTORY) + sys.path.insert(0, PROJECT_ROOT_DIRECTORY) +if python_path: + os.putenv('PYTHONPATH', "%s:%s" % (os.getenv('PYTHONPATH', ''), ':'.join(python_path))) # for subprocesses + +import uberwriter + +locale_dir = os.path.abspath(os.path.join(os.path.dirname(uberwriter.__file__),'../po/')) + +# L10n +locale.textdomain('uberwriter') +locale.bindtextdomain('uberwriter', locale_dir) +gettext.textdomain('uberwriter') +gettext.bindtextdomain('uberwriter', locale_dir) + +uberwriter.main() diff --git a/configure b/configure deleted file mode 100755 index 8b13789..0000000 --- a/configure +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/de.wolfvollprecht.UberWriter.appdata.xml b/data/de.wolfvollprecht.UberWriter.appdata.xml index ac95924..fa00397 100644 --- a/data/de.wolfvollprecht.UberWriter.appdata.xml +++ b/data/de.wolfvollprecht.UberWriter.appdata.xml @@ -28,6 +28,13 @@ + + +
    +
  • ...
  • +
+
+
    diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml index e7fdb28..e7d4bfc 100644 --- a/data/de.wolfvollprecht.UberWriter.gschema.xml +++ b/data/de.wolfvollprecht.UberWriter.gschema.xml @@ -2,37 +2,57 @@ + + + + + + + + + + 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. @@ -42,6 +62,13 @@ Open file paths of the current session + + "words" + Default statistic + + Which statistic is shown on the main window. + + diff --git a/data/media/css/_gtk_base.css b/data/media/css/_gtk_base.css deleted file mode 100644 index fdbf0cf..0000000 --- a/data/media/css/_gtk_base.css +++ /dev/null @@ -1,213 +0,0 @@ -/* - TODO: - Look into compiling resources with glib-compile-resource etc. for - inclusion in templates -*/ - -@binding-set editor-bindings { - bind "i" { "insert-italic" () }; - bind "b" { "insert-bold" () }; - bind "r" { "insert-hrule" () }; - bind "u" { "insert-ulistitem" () }; - bind "h" { "insert-heading" () }; - bind "z" { "undo" () }; - bind "y" { "redo" () }; - bind "d" { "insert-strikeout" () }; - /*bind "t" { "insert-at-cursor" ('[ ] ') };*/ - bind "z" { "redo" () }; -} - -/* Main window and text colors */ - -.uberwriter_window { - /*border-radius: 7px 7px 3px 3px;*/ - background: @background_color; - caret-color: @foreground_color; -} - -.uberwriter_window.small .uberwriter-editor { - font-family: 'Fira Mono', monospace; - font-size: 12px; -} -.uberwriter_window grid { - background-color: @background_color; -} - -#UberwriterWindow.medium .uberwriter-editor { - font-family: 'Fira Mono', monospace; - font-size: 15px; -} - -#UberwriterWindow.large .uberwriter-editor { - font-family: 'Fira Mono', monospace; - font-size: 18px; -} - -#titlebar_revealer { - padding: 0; -} - -.scrollbars-junction, -.scrollbar.trough { - background: transparent; -} - -#titlebar_container { - background: @background_color; -} - -.uberwriter-editor { - border: none; - background-color: transparent; - text-decoration-color: #ff0000; - /*-GtkWidget-cursor-color: shade(#4D9FCE, 0.9);*/ - /*-GtkWidget-cursor-aspect-ratio: 0.1;*/ - -gtk-key-bindings: editor-bindings; -} - -.uberwriter-editor text { - background-color: @background_color; - color: @foreground_color; - caret-color: @foreground_color; -} - -.uberwriter-editor:selected { - background-color: #4D9FCE; - color: #FFF; -} - -.uberwriter-editor button { - margin: 0; - padding: 0; - /*background: #CCC;*/ -} - -.uberwriter-editor toolbar { - /*background: transparent;*/ - border: none; - padding: 0; -} - - -.status_bar_box label { - color: #666; -} - -.status_bar_box button { - /* finding reset */ - background-color: @background_color; - text-shadow: inherit; - /*icon-shadow: inherit;*/ - box-shadow: initial; - background-clip: initial; - background-origin: initial; - background-size: initial; - background-position: initial; - background-repeat: initial; - background-image: initial; - border-image-source: initial; - border-image-repeat: initial; - border-image-slice: initial; - border-image-width: initial; - - border-style: none; - -button-images: true; - border-radius: 2px; - color: #666; - padding: 3px 5px; - transition: 100ms ease-in; -} - -.status_bar_box button:hover, -.status_bar_box button:checked { - transition: 0s ease-in; - color: @background_color; - background-color: #666; -} - -.status_bar_box button:hover label, -.status_bar_box button:checked label { - color: @background_color; -} - -.status_bar_box button:active { - color: #EEE; - background-color: #EEE; - background-image: none; - box-shadow: 0 0 2px rgba(0,0,0,0.4) -} - -.status_bar_box separator { - border-color: #999; - border-right: none; -} - -#PreviewMenuItem image { - border-radius: 2px; - color: #666; - padding: 3px 5px; - border: none; - background: #FFF; -} - -#UberwriterWindow treeview { - padding: 3px 3px 3px 3px; -} - -#LexikonBubble { - /*font: serif 10;*/ - font-family: serif; - font-size: 10px; - background: @background_color; - border-radius: 4px; - border-color: @background_color; - margin: 5px; - padding: 5px; -} - -/* .QuickPreviewPopup { - padding: 5px; - margin: 5px; - border: 1px solid #333; - background: @ligth_bg; - border-radius: 3px; - border-color: @background_color; -} */ - -#LexikonBubble label { - /*padding: 5px;*/ -} - -#LexikonBubble { - background-color: @background_color; - border: 5px solid @background_color; -} - -#LexikonBubble .lexikon_heading { - /*font: serif 12;*/ - font-family: serif; - font-size: 12px; - padding-bottom: 5px; - padding-top: 5px; - font-weight: bold; - padding-left: 10px; -} - -#LexikonBubble .lexikon_num { - padding-right: 5px; - padding-left: 20px; -} - -.QuickPreviewPopup { - background-color: @background_color; -} - -.QuickPreviewPopup grid { - background-color: @background_color; - color: @foreground_color; - border-color: @background_color; -} - -.QuickPreviewPopup label { - color: @foreground_color; -} \ No newline at end of file diff --git a/data/media/css/gtk/base.css b/data/media/css/gtk/base.css new file mode 100644 index 0000000..ff0736d --- /dev/null +++ b/data/media/css/gtk/base.css @@ -0,0 +1,180 @@ +/* + TODO: + Look into compiling resources with glib-compile-resource etc. for + inclusion in templates +*/ + +@binding-set editor-bindings { + bind "i" { "insert-italic" () }; + bind "b" { "insert-bold" () }; + bind "r" { "insert-hrule" () }; + bind "u" { "insert-listitem" () }; + bind "h" { "insert-header" () }; + bind "z" { "undo" () }; + bind "y" { "redo" () }; + bind "d" { "insert-strikethrough" () }; + /*bind "t" { "insert-at-cursor" ('[ ] ') };*/ + bind "z" { "redo" () }; +} + +@define-color math_text_color mix(@theme_fg_color, #00b5ff, 0.15); + +/* Main window and text colors */ + +.uberwriter-window { + /*border-radius: 7px 7px 3px 3px;*/ + background: @theme_base_color; + color: @theme_fg_color; + caret-color: @theme_fg_color; +} + +.uberwriter-window .uberwriter-editor { + font-family: 'Fira Mono', monospace; + font-size: 16px; +} + +.uberwriter-window.small .uberwriter-editor { + font-size: 14px; +} + +.uberwriter-window.large .uberwriter-editor { + font-size: 18px; +} + +#titlebar-revealer { + padding: 0; +} + +.scrollbars-junction, +.scrollbar.trough { + background: transparent; +} + +#titlebar-container { + background: @theme_base_color; +} + +.uberwriter-editor { + border: none; + background-color: transparent; + text-decoration-color: @error_color; + -gtk-key-bindings: editor-bindings; +} + +.uberwriter-editor text { + background-color: @theme_base_color; + color: @theme_fg_color; + caret-color: @theme_fg_color; +} + +.uberwriter-editor text selection { + background-color: @theme_selected_bg_color; + color: @theme_selected_fg_color; +} + +.uberwriter-editor button { + margin: 0; + padding: 0; + /*background: #CCC;*/ +} + +.uberwriter-editor toolbar { + /*background: transparent;*/ + border: none; + padding: 0; +} + +.stats-counter { + color: alpha(@theme_fg_color, 0.6); + background-color: @theme_base_color; + text-shadow: inherit; + box-shadow: initial; + background-clip: initial; + background-origin: initial; + background-size: initial; + background-position: initial; + background-repeat: initial; + background-image: initial; + border-image-source: initial; + border-image-repeat: initial; + border-image-slice: initial; + border-image-width: initial; + border-style: none; + padding: 0px 16px; + transition: 100ms ease-in; +} + +.stats-counter:hover, +.stats-counter:checked { + color: @theme_fg_color; + background-color: mix(@theme_base_color, @theme_bg_color, 0.5); +} + +#PreviewMenuItem image { + border-radius: 2px; + color: #666; + padding: 3px 5px; + border: none; + background: #FFF; +} + +.uberwriter-window treeview { + padding: 3px 3px 3px 3px; +} + +#LexikonBubble { + /*font: serif 10;*/ + font-family: serif; + font-size: 10px; + background: @theme_bg_color; + border-radius: 4px; + border-color: @theme_bg_color; + margin: 5px; + padding: 5px; +} + +/* .quick-preview-popup { + padding: 5px; + margin: 5px; + border: 1px solid #333; + background: @ligth_bg; + border-radius: 3px; + border-color: @theme_bg_color; +} */ + +#LexikonBubble label { + /*padding: 5px;*/ +} + +#LexikonBubble { + background-color: @theme_bg_color; + border: 5px solid @theme_bg_color; +} + +#LexikonBubble .lexikon-heading { + font-family: serif; + font-size: 12px; + padding-bottom: 5px; + padding-top: 5px; + font-weight: bold; + padding-left: 10px; +} + +#LexikonBubble .lexikon-num { + padding-right: 5px; + padding-left: 20px; +} + +.quick-preview-popup { + background-color: @theme_bg_color; +} + +.quick-preview-popup grid { + background-color: @theme_bg_color; + color: @theme_fg_color; + border-color: @theme_bg_color; +} + +.quick-preview-popup label { + color: @theme_fg_color; +} \ No newline at end of file diff --git a/data/media/css/gtk_adwaita.css b/data/media/css/gtk_adwaita.css deleted file mode 100644 index f75dcde..0000000 --- a/data/media/css/gtk_adwaita.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #2e3436; -@define-color background_color #f6f5f4; -@define-color math_text_color #00364c; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_adwaita_dark.css b/data/media/css/gtk_adwaita_dark.css deleted file mode 100644 index a41d20c..0000000 --- a/data/media/css/gtk_adwaita_dark.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #eeeeec; -@define-color background_color #353535; -@define-color math_text_color #ffc9b3; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc.css b/data/media/css/gtk_arc.css deleted file mode 100644 index 967c919..0000000 --- a/data/media/css/gtk_arc.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #3b3e45; -@define-color background_color #f5f6f7; -@define-color math_text_color #00364c; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc_dark.css b/data/media/css/gtk_arc_dark.css deleted file mode 100644 index 3edd842..0000000 --- a/data/media/css/gtk_arc_dark.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #d3dae3; -@define-color background_color #383c4a; -@define-color math_text_color #ffc9b3; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_arc_darker.css b/data/media/css/gtk_arc_darker.css deleted file mode 100644 index 44c667b..0000000 --- a/data/media/css/gtk_arc_darker.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #3b3e45; -@define-color background_color #f5f6f7; -@define-color math_text_color #00364C; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_high_contrast.css b/data/media/css/gtk_high_contrast.css deleted file mode 100644 index 254215e..0000000 --- a/data/media/css/gtk_high_contrast.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #000000; -@define-color background_color #ffffff; -@define-color math_text_color #000000; - -@import url("_gtk_base.css"); diff --git a/data/media/css/gtk_high_contrast_inverse.css b/data/media/css/gtk_high_contrast_inverse.css deleted file mode 100644 index b9790d4..0000000 --- a/data/media/css/gtk_high_contrast_inverse.css +++ /dev/null @@ -1,5 +0,0 @@ -@define-color foreground_color #ffffff; -@define-color background_color #000000; -@define-color math_text_color #ffffff; - -@import url("_gtk_base.css"); diff --git a/data/media/css/web_adwaita.css b/data/media/css/web/adwaita.css similarity index 93% rename from data/media/css/web_adwaita.css rename to data/media/css/web/adwaita.css index ecfde17..e784b20 100644 --- a/data/media/css/web_adwaita.css +++ b/data/media/css/web/adwaita.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("base.css"); :root { --text-color: #2e3436; diff --git a/data/media/css/web_adwaita_dark.css b/data/media/css/web/adwaita_dark.css similarity index 93% rename from data/media/css/web_adwaita_dark.css rename to data/media/css/web/adwaita_dark.css index 320a657..df71e44 100644 --- a/data/media/css/web_adwaita_dark.css +++ b/data/media/css/web/adwaita_dark.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web/web__base.css"); :root { --text-color: #eeeeec; diff --git a/data/media/css/web_arc.css b/data/media/css/web/arc.css similarity index 93% rename from data/media/css/web_arc.css rename to data/media/css/web/arc.css index c5ccb04..688fb87 100644 --- a/data/media/css/web_arc.css +++ b/data/media/css/web/arc.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web/web__base.css"); :root { --text-color: #3b3e45; diff --git a/data/media/css/web_arc_dark.css b/data/media/css/web/arc_dark.css similarity index 93% rename from data/media/css/web_arc_dark.css rename to data/media/css/web/arc_dark.css index b0c6a45..aeba9ac 100644 --- a/data/media/css/web_arc_dark.css +++ b/data/media/css/web/arc_dark.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web/web__base.css"); :root { --text-color: #d3dae3; diff --git a/data/media/css/web/arc_darker.css b/data/media/css/web/arc_darker.css new file mode 100644 index 0000000..3eeede8 --- /dev/null +++ b/data/media/css/web/arc_darker.css @@ -0,0 +1 @@ +@import url("web/web_arc.css"); \ No newline at end of file diff --git a/data/media/css/_web_base.css b/data/media/css/web/base.css similarity index 95% rename from data/media/css/_web_base.css rename to data/media/css/web/base.css index 3dd9ecc..65dfbb2 100644 --- a/data/media/css/_web_base.css +++ b/data/media/css/web/base.css @@ -1,11 +1,11 @@ @font-face { font-family: fira-sans; - src: url("../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); + src: url("../../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); } @font-face { font-family: fira-mono; - src: url("../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); + src: url("../../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2"); } @font-face { @@ -56,8 +56,9 @@ body { background-color: var(--background-color); font-family: "Fira Sans", fira-sans, sans-serif, color-emoji; line-height: 1.5; - text-size-adjust: 100%; word-wrap: break-word; + max-width: 978px; + margin: auto; padding: 2em; } diff --git a/data/media/css/web_high_contrast.css b/data/media/css/web/highcontrast.css similarity index 93% rename from data/media/css/web_high_contrast.css rename to data/media/css/web/highcontrast.css index b9db89e..8e12c4f 100644 --- a/data/media/css/web_high_contrast.css +++ b/data/media/css/web/highcontrast.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web/web__base.css"); a { text-decoration: underline; diff --git a/data/media/css/web_high_contrast_inverse.css b/data/media/css/web/highcontrast_inverse.css similarity index 93% rename from data/media/css/web_high_contrast_inverse.css rename to data/media/css/web/highcontrast_inverse.css index 646e1f0..e4ed265 100644 --- a/data/media/css/web_high_contrast_inverse.css +++ b/data/media/css/web/highcontrast_inverse.css @@ -1,4 +1,4 @@ -@import url("_web_base.css"); +@import url("web/web__base.css"); a { text-decoration: underline; diff --git a/data/media/css/web_arc_darker.css b/data/media/css/web_arc_darker.css deleted file mode 100644 index cc6a95e..0000000 --- a/data/media/css/web_arc_darker.css +++ /dev/null @@ -1 +0,0 @@ -@import url("web_arc.css"); \ No newline at end of file diff --git a/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/Recents.ui b/data/ui/Recents.ui index 4d918da..06bc23a 100644 --- a/data/ui/Recents.ui +++ b/data/ui/Recents.ui @@ -4,6 +4,7 @@ + text/markdown text/x-markdown 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 79% rename from data/ui/UberwriterWindow.ui rename to data/ui/Window.ui index 162d6f9..bca48f4 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 @@ -77,97 +75,20 @@ False True - + True False crossfade 750 True - + + 0 Words True - False - - - True - False - end - True - Words: - - - 3 - 0 - - - - - True - False - end - 0 - right - 4 - 1 - - - 4 - 0 - - - - - True - False - end - 10 - 10 - 10 - 10 - vertical - - - 5 - 0 - - - - - True - False - end - Characters: - - - 6 - 0 - - - - - True - False - end - 11 - 11 - 0 - 6 - 1 - - - 7 - 0 - - - - - - - - - - - + True + True + Show Statistics + end @@ -186,23 +107,16 @@ natural none - + + 500 True - False - center + True + True True True + adjustment1 - - 500 - True - True - True - adjustment1 - - - - + @@ -290,6 +204,7 @@ True True True + Next Match avall @@ -335,6 +250,7 @@ True True True + Regular Expression False @@ -347,6 +263,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/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json b/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json new file mode 100644 index 0000000..dde1450 --- /dev/null +++ b/flatpak/de.wolfvollprecht.UberWriter.pipdeps.json @@ -0,0 +1,35 @@ + + { + "name": "pipdeps", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz", + "sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz", + "sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz", + "sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz", + "sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz", + "sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" + } + ] + } \ No newline at end of file diff --git a/flatpak/uberwriter.json b/flatpak/uberwriter.json index 620c285..160f84f 100644 --- a/flatpak/uberwriter.json +++ b/flatpak/uberwriter.json @@ -1,134 +1,125 @@ { - "app-id": "de.wolfvollprecht.UberWriter", - "runtime": "org.gnome.Platform", - "runtime-version": "3.28", - "sdk": "org.gnome.Sdk", - "command": "/app/usr/bin/uberwriter", - "finish-args": [ + "app-id":"de.wolfvollprecht.UberWriter", + "runtime":"org.gnome.Platform", + "runtime-version":"3.32", + "sdk":"org.gnome.Sdk", + "command":"start-uberwriter", + "finish-args":[ "--socket=x11", + "--socket=wayland", "--share=ipc", + "--share=network", "--filesystem=host", - "--env=IN_FLATPAK=1", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", - "--env=DCONF_USER_CONFIG_DIR=.config/dconf", - "--env=XDG_DATA_DIRS=/app/usr/share", - "--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/usr/bin:/app/bin" + "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], - "build-options" : { - "env": { - "PYTHON": "python3", - "IN_FLATPAK": "1" + "add-extensions":{ + "de.wolfvollprecht.UberWriter.Plugin":{ + "directory":"extensions", + "version":"stable", + "subdirectories":true, + "no-autodownload":true, + "autodelete":true } }, - "add-extensions": { - "de.wolfvollprecht.UberWriter.Plugin": { - "directory": "extensions", - "version": "stable", - "subdirectories": true, - "no-autodownload": true, - "autodelete": true - } - }, - "modules": [ + "modules":[ { - "name": "uberwriter", - "sources": [ + "name":"enchant", + "config-opts":[ + "--disable-static", + "--with-myspell-dir=/usr/share/hunspell" + ], + "cleanup":[ + "/bin" + ], + "sources":[ { - "type" : "git", - "url" : "../", - "branch" : "refactoring" - } - ], - "build-commands": [ - "install -Dm644 flatpak/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml " - ], - "post-install": [ - "glib-compile-schemas /app/usr/share/glib-2.0/schemas", - "install -d /app/extensions" - ] - }, - { - "name": "pandoc", - "only-arches": [ - "x86_64" - ], - "buildsystem": "simple", - "build-commands": [ - "cp bin/pandoc /app/usr/bin/pandoc", - "cp bin/pandoc-citeproc /app/usr/bin/pandoc-citeproc" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", - "sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" + "type":"archive", + "url":"https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz", + "sha256":"bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5" } ] }, { - "name": "pipdeps", - "buildsystem": "simple", - "build-commands": [ - "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc" - ], - "sources": [ + "name":"gspell", + "sources":[ { - "type": "file", - "url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz", - "sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz", - "sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz", - "sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz", - "sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz", - "sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0" + "type":"archive", + "url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.1.tar.xz", + "sha256":"819a1d23c7603000e73f5e738bdd284342e0cd345fb0c7650999c31ec741bbe5" } ] }, { - "name": "fonts", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/fonts/", - "cp ttf/* /app/share/fonts/" + "name":"fonts", + "buildsystem":"simple", + "build-commands":[ + "mkdir -p /app/share/fonts/", + "cp ttf/* /app/share/fonts/" ], - "sources": [ + "sources":[ { - "type": "git", - "url": "https://github.com/mozilla/Fira", - "tag": "4.202" + "type":"git", + "url":"https://github.com/mozilla/Fira", + "tag":"4.202" } ] }, { - "name": "appdata", - "buildsystem": "simple", - "build-commands": [ - "mkdir -p /app/share/appdata", - "install -Dm644 de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml" + "name":"pandoc", + "only-arches":[ + "x86_64" ], - "sources": [ + "buildsystem":"simple", + "build-commands":[ + "install -Dm 755 bin/pandoc /app/bin/pandoc", + "install -Dm 755 bin/pandoc-citeproc /app/bin/pandoc-citeproc" + ], + "sources":[ { - "type": "file", - "path": "de.wolfvollprecht.UberWriter.appdata.xml" + "type":"archive", + "url":"https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz", + "sha256":"06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed" + } + ] + }, + "de.wolfvollprecht.UberWriter.pipdeps.json", + { + "name":"uberwriter", + "buildsystem":"simple", + "build-commands":[ + "desktop-file-edit --set-key=Exec --set-value='uberwriter.in %U' data/de.wolfvollprecht.UberWriter.desktop", + "python3 -m pip install --prefix=/app --install-option=--optimize=1 ." + ], + "sources":[ + { + "type":"dir", + "path":"../" + } + ], + "post-install":[ + "install -d /app/extensions", + "glib-compile-schemas /app/share/glib-2.0/schemas" + ] + }, + { + "name":"scripts", + "buildsystem":"simple", + "build-commands":[ + "install -Dm 755 start-uberwriter.sh /app/bin/start-uberwriter" + ], + "sources":[ + { + "type":"script", + "dest-filename":"start-uberwriter.sh", + "commands":[ + "export PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/$(uname -a)-linux:$PATH", + "exec uberwriter.in \"$@\"" + ] } ] } ] - } +} diff --git a/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..0a7f6c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ regex enchant -python-gtkspellcheck -pandoc +pypandoc==1.4 +pyenchant +pygtkspellcheck diff --git a/setup.py b/setup.py index dbd88c4..9345029 100644 --- a/setup.py +++ b/setup.py @@ -2,16 +2,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2012, Wolf Vollprecht -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -22,37 +22,33 @@ from setuptools import setup import os -def package_files(directory): - paths = [] - for (path, directories, filenames) in os.walk(directory): +def data_files(basename): + data = os.path.join('.', 'data') + root = os.path.join(data, basename) + extra_files = [] + for path, directories, filenames in os.walk(root): + paths = [] for filename in filenames: paths.append(os.path.join(path, filename)) - return paths + extra_files.append(('share/uberwriter/data/{}'.format(os.path.relpath(path, data)), paths)) + return extra_files -extra_files_ui = package_files('./data/ui') -extra_files_media = package_files('./data/media') -extra_files_scripts = package_files('./data/lua') -from pprint import pprint -pprint(extra_files_ui) -pprint(extra_files_media) - -if os.path.isfile("/.flatpak-info"): - app_prefix = '/app/' -else: - app_prefix = '/usr/' +extra_files_ui = data_files('ui') +extra_files_media = data_files('media') +extra_files_scripts = data_files('lua') setup( name='uberwriter', - version='2.1.4', + version='2.2.0-beta1', license='GPL-3', author='Wolf Vollprecht', author_email='w.vollprecht@gmail.com', description='A beautiful, simple and distraction free markdown editor.', long_description="""UberWriter, beautiful distraction free writing - With UberWriter you get only one thing: An empty textbox, that is to - fill with your ideas. There are no settings, you don't have to choose a - font, it is only for writing.You can use markdown for all your markup - needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it + With UberWriter you get only one thing: An empty textbox, that is to + fill with your ideas. There are no settings, you don't have to choose a + font, it is only for writing.You can use markdown for all your markup + needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it is also required that you choose to install the texlive-luatex package.""", url='https://github.com/wolfv/uberwriter/', # cmdclass={'install': InstallAndUpdateDataDirectory}, @@ -60,11 +56,9 @@ setup( # "": '/opt/uberwriter/' }, packages=[ - "uberwriter.gtkspellcheck", "uberwriter.pylocales", # "uberwriter.pressagio", "uberwriter", - "uberwriter", "po" # "uberwriter.plugins" # "uberwriter.plugins.bibtex" @@ -75,13 +69,12 @@ setup( 'uberwriter.pylocales' : ['locales.db'], }, data_files=[ - (app_prefix + 'bin', ['bin/uberwriter']), - (app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), - (app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), - (app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), - (app_prefix + 'share/applications', ['de.wolfvollprecht.UberWriter.desktop']), - (app_prefix + 'share/uberwriter/data/ui', extra_files_ui), - (app_prefix + 'share/uberwriter/data/media', extra_files_media), - (app_prefix + 'share/uberwriter/data/lua', extra_files_scripts) + ('bin', ['uberwriter.in']), + ('share/applications', ['data/de.wolfvollprecht.UberWriter.desktop']), + ('share/metainfo', ['data/de.wolfvollprecht.UberWriter.appdata.xml']), + ('share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']), + ('share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']), + ('share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']), + *(extra_files_ui + extra_files_media + extra_files_scripts) ] ) diff --git a/uberwriter/__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..4a0f34c 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,9 @@ class Application(Gtk.Application): Gtk.Application.do_startup(self) - # Actions + self.settings.connect("changed", self.on_settings_changed) - 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 + # Header bar action = Gio.SimpleAction.new("new", None) action.connect("activate", self.on_new) @@ -122,14 +54,36 @@ 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) + + # App Menu + + 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,17 +92,41 @@ 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) + + # Stats Menu + + stat_default = self.settings.get_string("stat-default") + action = Gio.SimpleAction.new_stateful( + "stat_default", GLib.VariantType.new('s'), GLib.Variant.new_string(stat_default)) + action.connect("activate", self.on_stat_default) + self.add_action(action) + # Shortcuts - # TODO: be aware that a couple of shortcuts are defined in _gtk_base.css + # TODO: be aware that a couple of shortcuts are defined in base.css self.set_accels_for_action("app.focus_mode", ["d"]) self.set_accels_for_action("app.hemingway_mode", ["t"]) @@ -163,8 +141,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 +150,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 +161,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 +169,17 @@ 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() + elif key == "stat-default": + self.window.update_default_stat() def on_new(self, _action, _value): self.window.new_document() @@ -290,30 +190,66 @@ 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() + def on_stat_default(self, action, value): + action.set_state(value) + self.settings.set_string("stat-default", value.get_string()) + # ~ if __name__ == "__main__": # ~ app = Application() # ~ app.run(sys.argv) diff --git a/uberwriter/uberwriterconfig.py b/uberwriter/config.py similarity index 97% rename from uberwriter/uberwriterconfig.py rename to uberwriter/config.py index fc3f7b1..5d2da16 100644 --- a/uberwriter/uberwriterconfig.py +++ b/uberwriter/config.py @@ -53,7 +53,6 @@ def get_data_path(): """ # Get pathname absolute or relative. - # TODO: Abstract this (the old env IN_FLATPAK) if os.path.isfile("/.flatpak-info"): return '/app/share/uberwriter/data/' 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/gtkspellcheck/__init__.py b/uberwriter/gtkspellcheck/__init__.py deleted file mode 100644 index 3f30951..0000000 --- a/uberwriter/gtkspellcheck/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import unicode_literals - -__version__ = '4.0.5' -__project__ = 'Python GTK Spellcheck' -__short_name__ = 'pygtkspellcheck' -__authors__ = 'Maximilian Köhl & Carlos Jenkins' -__emails__ = 'linuxmaxi@googlemail.com & carlos@jenkins.co.cr' -__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html' -__download_url__ = 'https://github.com/koehlma/pygtkspellcheck/tarball/master' -__source__ = 'https://github.com/koehlma/pygtkspellcheck/' -__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git' -__copyright__ = '2012, Maximilian Köhl & Carlos Jenkins' -__desc_short__ = ('a simple but quite powerful Python spell checking library ' - 'for GtkTextViews based on Enchant') -__desc_long__ = ('A simple but quite powerful spellchecking library written in ' - 'pure Python for Gtk based on Enchant. It supports PyGObject ' - 'as well as PyGtk for Python 2 and 3 with automatic switching ' - 'and binding detection. For automatic translation of the user ' - 'interface it can use Gedit’s translation files.') - -__metadata__ = {'__version__' : __version__, - '__project__' : __project__, - '__short_name__' : __short_name__, - '__authors__' : __authors__, - '__emails__' : __emails__, - '__website__' : __website__, - '__download_url__' : __download_url__, - '__source__' : __source__, - '__vcs__' : __vcs__, - '__copyright__' : __copyright__, - '__desc_short__' : __desc_short__, - '__desc_long__' : __desc_long__} - -from .spellcheck import (SpellChecker, NoDictionariesFound, - NoGtkBindingFound) diff --git a/uberwriter/gtkspellcheck/oxt_extract.py b/uberwriter/gtkspellcheck/oxt_extract.py deleted file mode 100644 index ce1c45a..0000000 --- a/uberwriter/gtkspellcheck/oxt_extract.py +++ /dev/null @@ -1,294 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Carlos Jenkins -# Copyright (C) 2012-2016, Maximilian Köhl -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -This module extracts the .dic and .aff (Hunspell) dictionaries from any given -.oxt extension. - -Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary -""" - -import functools -import gettext -import logging -import os -import shutil -import sys -import warnings -import xml.dom.minidom -import xml.parsers.expat -import zipfile - -# enable deprecation warnings -warnings.simplefilter('always', DeprecationWarning) - -# public objects -__all__ = ['extract_oxt', 'batch_extract', 'BadXml', 'BadExtensionFile', - 'ExtractPathIsNoDirectory', 'BATCH_SUCCESS', 'BATCH_ERROR', - 'BATCH_WARNING'] - -# logger -logger = logging.getLogger(__name__) - -# translation -locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major) -_ = gettext.translation(locale_name, fallback=True).gettext - -class BadXml(Exception): - """ - The XML dictionary registry is not valid XML. - """ - -class BadExtensionFile(Exception): - """ - The extension has a wrong file format, should be a ZIP file. - """ - -class ExtractPathIsNoDirectory(Exception): - """ - The given `extract_path` is no directory. - """ - - -def find_dictionaries(registry): - def oor_name(name, element): - return element.attributes['oor:name'].value.lower() == name - - def get_property(name, properties): - property = list(filter(functools.partial(oor_name, name), - properties)) - if property: - return property[0].getElementsByTagName('value')[0] - - result = [] - - # find all "node" elements which have "dictionaries" as "oor:name" attribute - for dictionaries in filter(functools.partial(oor_name, 'dictionaries'), - registry.getElementsByTagName('node')): - # for all "node" elements in this dictionary nodes - for dictionary in dictionaries.getElementsByTagName('node'): - # get all "prop" elements - properties = dictionary.getElementsByTagName('prop') - # get the format property as text - format = get_property('format', properties).firstChild.data.strip() - if format and format == 'DICT_SPELL': - # find the locations property - locations = get_property('locations', properties) - # if the location property is text: - # %origin%/dictionary.aff %origin%/dictionary.dic - if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE: - locations = locations.firstChild.data - locations = locations.replace('%origin%/', '').strip() - result.append(locations.split()) - # otherwise: - # %origin%/dictionary.aff %origin%/dictionary.dic - else: - locations = [item.firshChild.data.replace('%origin%/', '') \ - .strip() for item in - locations.getElementsByTagName('it')] - result.append(locations) - - return result - -def extract(filename, target, override=False): - """ - Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions. - - :param filename: path to the ``.oxt`` extension - :param target: path to extract Hunspell dictionaries to - :param override: override existing files in the target directory - :rtype: list of the extracted dictionaries - - This function extracts the Hunspell dictionaries (``.dic`` and ``.aff`` - files) from the given ``.oxt`` extension found to ``target``. - - Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary - """ - # TODO 5.0: remove this function - warnings.warn(('call to deprecated function "{}", ' - 'moved to separate package "oxt_extract", ' - 'will be removed in pygtkspellcheck 5.0').format(extract.__name__), - category=DeprecationWarning) - try: - with zipfile.ZipFile(filename, 'r') as extension: - files = extension.namelist() - - registry = 'dictionaries.xcu' - if not registry in files: - for filename in files: - if filename.lower().endswith(registry): - registry = filename - - if registry in files: - registry = xml.dom.minidom.parse(extension.open(registry)) - dictionaries = find_dictionaries(registry) - extracted = [] - for dictionary in dictionaries: - for filename in dictionary: - dict_file = os.path.join(target, - os.path.basename(filename)) - if (not os.path.exists(dict_file) - or (override and os.path.isfile(dict_file))): - if filename in files: - with open(dict_file, 'wb') as _target: - with extension.open(filename, 'r') as _source: - extracted.append(os.path.basename(filename)) - _target.write(_source.read()) - else: - logger.warning('dictionary exists in registry ' - 'but not in the extension zip') - else: - logging.warning(('dictionary file "{}" already exists ' - 'and not overriding it' - ).format(dict_file)) - return extracted - except zipfile.BadZipfile: - raise BadExtensionFile('extension is not a valid ZIP file') - except xml.parsers.expat.ExpatError: - raise BadXml('dictionary registry is not valid XML') - -BATCH_SUCCESS = 'success' -BATCH_ERROR = 'error' -BATCH_WARNING = 'warning' - -def batch_extract(oxt_path, extract_path, override=False, move_path=None): - """ - Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions. - - :param oxt_path: path to a directory containing the ``.oxt`` extensions - :param extract_path: path to extract Hunspell dictionaries files to - :param override: override already existing files - :param move_path: optional path to move the ``.oxt`` files after processing - :rtype: generator over all extensions, yielding result, extension name, - error, extracted dictionaries and translated error message - result - would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if - some error happened or :const:`BATCH_WARNING` which contain some warning - messages instead of errors - - This function extracts the Hunspell dictionaries (``.dic`` and ``.aff`` - files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to - the ``extract_path`` directory. - - Extensions could be found at: - - http://extensions.services.openoffice.org/dictionary - - In detail, this functions does the following: - - 1. find all the ``.oxt`` extension files within ``oxt_path`` - 2. open (unzip) each extension - 3. find the dictionary definition file within (*dictionaries.xcu*) - 4. parse the dictionary definition file and locate the dictionaries files - 5. uncompress those files to ``extract_path`` - - - By default file overriding is disabled, set ``override`` parameter to True - if you want to enable it. As additional option, each processed extension can - be moved to ``move_path``. - - Example:: - - for result, name, error, dictionaries, message in oxt_extract.batch_extract(...): - if result == oxt_extract.BATCH_SUCCESS: - print('successfully extracted extension "{}"'.format(name)) - elif result == oxt_extract.BATCH_ERROR: - print('could not extract extension "{}"'.format(name)) - print(message) - print('error {}'.format(error)) - elif result == oxt_extract.BATCH_WARNING: - print('warning during processing extension "{}"'.format(name)) - print(message) - print(error) - - """ - - # TODO 5.0: remove this function - warnings.warn(('call to deprecated function "{}", ' - 'moved to separate package "oxt_extract", ' - 'will be removed in pygtkspellcheck 5.0').format(extract.__name__), - category=DeprecationWarning) - - # get the real, absolute and normalized path - oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path))) - - # check that the input directory exists - if not os.path.isdir(oxt_path): - return - - # create extract directory if not exists - if not os.path.exists(extract_path): - os.makedirs(extract_path) - - # check that the extract path is a directory - if not os.path.isdir(extract_path): - raise ExtractPathIsNoDirectory('extract path is not a valid directory') - - # get all .oxt extension at given path - oxt_files = [extension for extension in os.listdir(oxt_path) - if extension.lower().endswith('.oxt')] - - for extension_name in oxt_files: - extension_path = os.path.join(oxt_path, extension_name) - - try: - dictionaries = extract(extension_path, extract_path, override) - yield BATCH_SUCCESS, extension_name, None, dictionaries, '' - except BadExtensionFile as error: - logger.error(('extension "{}" is not a valid ZIP file' - ).format(extension_name)) - yield (BATCH_ERROR, extension_name, error, [], - _('extension "{}" is not a valid ZIP file' - ).format(extension_name)) - except BadXml as error: - logger.error(('extension "{}" has no valid XML dictionary registry' - ).format(extension_name)) - yield (BATCH_ERROR, extension_name, error, [], - _('extension "{}" has no valid XML dictionary registry' - ).format(extension_name)) - - # move the extension after processing if user requires it - if move_path is not None: - # create move path if it doesn't exists - if not os.path.exists(move_path): - os.makedirs(move_path) - # move to the given path only if it is a directory and target - # doesn't exists - if os.path.isdir(move_path): - if (not os.path.exists(os.path.join(move_path, extension_name)) - or override): - shutil.move(extension_path, move_path) - else: - logger.warning(('unable to move extension, file with same ' - 'name exists within move_path')) - yield (BATCH_WARNING, extension_name, - ('unable to move extension, file with same name ' - 'exists within move_path'), [], - _('unable to move extension, file with same name ' - 'exists within move_path')) - else: - logger.warning(('unable to move extension, move_path is not a ' - 'directory')) - yield (BATCH_WARNING, extension_name, - ('unable to move extension, move_path is not a ' - 'directory'), [], - _('unable to move extension, move_path is not a ' - 'directory')) \ No newline at end of file diff --git a/uberwriter/gtkspellcheck/spellcheck.py b/uberwriter/gtkspellcheck/spellcheck.py deleted file mode 100644 index 0112fbf..0000000 --- a/uberwriter/gtkspellcheck/spellcheck.py +++ /dev/null @@ -1,660 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright (C) 2012, Maximilian Köhl -# Copyright (C) 2012, Carlos Jenkins -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -A simple but quite powerful spellchecking library written in pure Python for Gtk -based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with -automatic switching and binding detection. For automatic translation of the user -interface it can use Gedit’s translation files. -""" - -import enchant -import gettext -import logging -import re -import sys - -from uberwriter.pylocales import code_to_name as _code_to_name -from uberwriter.pylocales import LanguageNotFound, CountryNotFound - -# public objects -__all__ = ['SpellChecker', 'NoDictionariesFound', 'NoGtkBindingFound'] - -# logger -logger = logging.getLogger(__name__) - -class NoDictionariesFound(Exception): - """ - There aren't any dictionaries installed on the current system so - spellchecking could not work in any way. - """ - -class NoGtkBindingFound(Exception): - """ - Could not find any loaded Gtk binding. - """ - -if sys.version_info.major == 3: - _py3k = True -else: - _py3k = False - -if _py3k: - # there is only the gi binding for Python 3 - import gi - gi.require_version('Gtk', '3.0') - from gi.repository import Gtk as gtk - _pygobject = True -else: - # find any loaded gtk binding - if 'gi.repository.Gtk' in sys.modules: - gtk = sys.modules['gi.repository.Gtk'] - _pygobject = True - elif 'gtk' in sys.modules: - gtk = sys.modules['gtk'] - _pygobject = False - else: - raise NoGtkBindingFound('could not find any loaded Gtk binding') - -# select base list class -try: - from collections import UserList - _list = UserList -except ImportError: - _list = list - - - -# select base string -if _py3k: - basestring = str - -# map between Gedit's translation and PyGtkSpellcheck's -_GEDIT_MAP = {'Languages' : 'Languages', - 'Ignore All' : 'Ignore _All', - 'Suggestions' : 'Suggestions', - '(no suggestions)' : '(no suggested words)', - 'Add "{}" to Dictionary' : 'Add w_ord', - 'Unknown' : 'Unknown'} - -# translation -if gettext.find('gedit'): - _gedit = gettext.translation('gedit', fallback=True).gettext - _ = lambda message: _gedit(_GEDIT_MAP[message]).replace('_', '') -else: - locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major) - _ = gettext.translation(locale_name, fallback=True).gettext - -def code_to_name(code, separator='_'): - try: - return _code_to_name(code, separator) - except (LanguageNotFound, CountryNotFound): - return '{} ({})'.format(_('Unknown'), code) - -class SpellChecker(object): - """ - Main spellchecking class, everything important happens here. - - :param view: GtkTextView the SpellChecker should be attached to. - :param language: The language which should be used for spellchecking. - Use a combination of two letter lower-case ISO 639 language code with a - two letter upper-case ISO 3166 country code, for example en_US or de_DE. - :param prefix: A prefix for some internal GtkTextMarks. - :param collapse: Enclose suggestions in its own menu. - :param params: Dictionary with Enchant broker parameters that should be set - e.g. `enchant.myspell.dictionary.path`. - - .. attribute:: languages - - A list of supported languages. - - .. function:: exists(language) - - Checks if a language exists. - - :param language: language to check - """ - FILTER_WORD = 'word' - FILTER_LINE = 'line' - FILTER_TEXT = 'text' - - DEFAULT_FILTERS = {FILTER_WORD : [r'[0-9.,]+'], - FILTER_LINE : [(r'(https?|ftp|file):((//)|(\\\\))+[\w\d:' - r'#@%/;$()~_?+-=\\.&]+'), - r'[\w\d]+@[\w\d.]+'], - FILTER_TEXT : []} - - class _LanguageList(_list): - def __init__(self, *args, **kwargs): - if sys.version_info.major == 3: - super().__init__(*args, **kwargs) - else: - _list.__init__(self, *args, **kwargs) - self.mapping = dict(self) - - @classmethod - def from_broker(cls, broker): - return cls(sorted([(language, code_to_name(language)) - for language in broker.list_languages()], - key=lambda language: language[1])) - - def exists(self, language): - return language in self.mapping - - class _Mark(): - def __init__(self, buffer, name, start): - self._buffer = buffer - self._name = name - self._mark = self._buffer.create_mark(self._name, start, True) - - @property - def iter(self): - return self._buffer.get_iter_at_mark(self._mark) - - @property - def inside_word(self): - return self.iter.inside_word() - - @property - def word(self): - start = self.iter - if not start.starts_word(): - start.backward_word_start() - end = self.iter - if end.inside_word(): - end.forward_word_end() - return start, end - - def move(self, location): - self._buffer.move_mark(self._mark, location) - - def __init__(self, view, language='en', prefix='gtkspellchecker', - collapse=True, params={}): - self._view = view - self.collapse = collapse - self._view.connect('populate-popup', - lambda entry, menu:self._extend_menu(menu)) - self._view.connect('popup-menu', self._click_move_popup) - self._view.connect('button-press-event', self._click_move_button) - self._prefix = prefix - self._broker = enchant.Broker() - for param, value in params.items(): self._broker.set_param(param, value) - self.languages = SpellChecker._LanguageList.from_broker(self._broker) - if self.languages.exists(language): - self._language = language - elif self.languages.exists('en'): - logger.warning(('no installed dictionary for language "{}", ' - 'fallback to english'.format(language))) - self._language = 'en' - else: - if self.languages: - self._language = self.languages[0][0] - logger.warning(('no installed dictionary for language "{}" ' - 'and english, fallback to first language in' - 'language list ("{}")').format(language, - self._language)) - else: - logger.critical('no dictionaries found') - raise NoDictionariesFound() - self._dictionary = self._broker.request_dict(self._language) - self._deferred_check = False - self._filters = dict(SpellChecker.DEFAULT_FILTERS) - self._regexes = {SpellChecker.FILTER_WORD : re.compile('|'.join( - self._filters[SpellChecker.FILTER_WORD])), - SpellChecker.FILTER_LINE : re.compile('|'.join( - self._filters[SpellChecker.FILTER_LINE])), - SpellChecker.FILTER_TEXT : re.compile('|'.join( - self._filters[SpellChecker.FILTER_TEXT]), - re.MULTILINE)} - self._enabled = True - self.buffer_initialize() - - @property - def language(self): - """ - The language used for spellchecking. - """ - return self._language - - @language.setter - def language(self, language): - if language != self._language and self.languages.exists(language): - self._language = language - self._dictionary = self._broker.request_dict(language) - self.recheck() - - @property - def enabled(self): - """ - Enable or disable spellchecking. - """ - return self._enabled - - @enabled.setter - def enabled(self, enabled): - if enabled and not self._enabled: - self.enable() - elif not enabled and self._enabled: - self.disable() - - def buffer_initialize(self): - """ - Initialize the GtkTextBuffer associated with the GtkTextView. If you - have associated a new GtkTextBuffer with the GtkTextView call this - method. - """ - if _pygobject: - self._misspelled = gtk.TextTag.new('{}-misspelled'\ - .format(self._prefix)) - else: - self._misspelled = gtk.TextTag('{}-misspelled'.format(self._prefix)) - self._misspelled.set_property('underline', 4) - self._buffer = self._view.get_buffer() - self._buffer.connect('insert-text', self._before_text_insert) - self._buffer.connect_after('insert-text', self._after_text_insert) - self._buffer.connect_after('delete-range', self._range_delete) - self._buffer.connect_after('mark-set', self._mark_set) - start = self._buffer.get_bounds()[0] - self._marks = {'insert-start' : SpellChecker._Mark(self._buffer, - '{}-insert-start'.format(self._prefix), start), - 'insert-end' : SpellChecker._Mark(self._buffer, - '{}-insert-end'.format(self._prefix), start), - 'click' : SpellChecker._Mark(self._buffer, - '{}-click'.format(self._prefix), start)} - self._table = self._buffer.get_tag_table() - self._table.add(self._misspelled) - self.ignored_tags = [] - def tag_added(tag, *args): - if hasattr(tag, 'spell_check') and not getattr(tag, 'spell_check'): - self.ignored_tags.append(tag) - def tag_removed(tag, *args): - if tag in self.ignored_tags: - self.ignored_tags.remove(tag) - self._table.connect('tag-added', tag_added) - self._table.connect('tag-removed', tag_removed) - self._table.foreach(tag_added, None) - self.no_spell_check = self._table.lookup('no-spell-check') - if not self.no_spell_check: - if _pygobject: - self.no_spell_check = gtk.TextTag.new('no-spell-check') - else: - self.no_spell_check = gtk.TextTag('no-spell-check') - self._table.add(self.no_spell_check) - self.recheck() - - def recheck(self): - """ - Rechecks the spelling of the whole text. - """ - start, end = self._buffer.get_bounds() - self.check_range(start, end, True) - - def disable(self): - """ - Disable spellchecking. - """ - self._enabled = False - start, end = self._buffer.get_bounds() - self._buffer.remove_tag(self._misspelled, start, end) - - def enable(self): - """ - Enable spellchecking. - """ - self._enabled = True - self.recheck() - - def append_filter(self, regex, filter_type): - """ - Append a new filter to the filter list. Filters are useful to ignore - some misspelled words based on regular expressions. - - :param regex: The regex used for filtering. - :param filter_type: The type of the filter. - - Filter Types: - - :const:`SpellChecker.FILTER_WORD`: The regex must match the whole word - you want to filter. The word separation is done by Pango's word - separation algorithm so, for example, urls won't work here because - they are split in many words. - - :const:`SpellChecker.FILTER_LINE`: If the expression you want to match - is a single line expression use this type. It should not be an open - end expression because then the rest of the line with the text you - want to filter will become correct. - - :const:`SpellChecker.FILTER_TEXT`: Use this if you want to filter - multiline expressions. The regex will be compiled with the - `re.MULTILINE` flag. Same with open end expressions apply here. - """ - self._filters[filter_type].append(regex) - if filter_type == SpellChecker.FILTER_TEXT: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type]), re.MULTILINE) - else: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type])) - - def remove_filter(self, regex, filter_type): - """ - Remove a filter from the filter list. - - :param regex: The regex which used for filtering. - :param filter_type: The type of the filter. - """ - self._filters[filter_type].remove(regex) - if filter_type == SpellChecker.FILTER_TEXT: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type]), re.MULTILINE) - else: - self._regexes[filter_type] = re.compile('|'.join( - self._filters[filter_type])) - - def append_ignore_tag(self, tag): - """ - Appends a tag to the list of ignored tags. A string will be automatic - resolved into a tag object. - - :param tag: Tag object or tag name. - """ - if isinstance(tag, basestring): - tag = self._table.lookup(tag) - self.ignored_tags.append(tag) - - def remove_ignore_tag(self, tag): - """ - Removes a tag from the list of ignored tags. A string will be automatic - resolved into a tag object. - - :param tag: Tag object or tag name. - """ - if isinstance(tag, basestring): - tag = self._table.lookup(tag) - self.ignored_tags.remove(tag) - - def add_to_dictionary(self, word): - """ - Adds a word to user's dictionary. - - :param word: The word to add. - """ - self._dictionary.add_to_pwl(word) - self.recheck() - - def ignore_all(self, word): - """ - Ignores a word for the current session. - - :param word: The word to ignore. - """ - self._dictionary.add_to_session(word) - self.recheck() - - def check_range(self, start, end, force_all=False): - """ - Checks a specified range between two GtkTextIters. - - :param start: Start iter - checking starts here. - :param end: End iter - checking ends here. - """ - if not self._enabled: - return - if end.inside_word(): end.forward_word_end() - if not start.starts_word() and (start.inside_word() or - start.ends_word()): - start.backward_word_start() - self._buffer.remove_tag(self._misspelled, start, end) - cursor = self._buffer.get_iter_at_mark(self._buffer.get_insert()) - precursor = cursor.copy() - precursor.backward_char() - highlight = (cursor.has_tag(self._misspelled) or - precursor.has_tag(self._misspelled)) - if not start.get_offset(): - start.forward_word_end() - start.backward_word_start() - word_start = start.copy() - while word_start.compare(end) < 0: - word_end = word_start.copy() - word_end.forward_word_end() - in_word = ((word_start.compare(cursor) < 0) and - (cursor.compare(word_end) <= 0)) - if in_word and not force_all: - if highlight: - self._check_word(word_start, word_end) - else: - self._deferred_check = True - else: - self._check_word(word_start, word_end) - self._deferred_check = False - word_end.forward_word_end() - word_end.backward_word_start() - if word_start.equal(word_end): - break - word_start = word_end.copy() - - def _languages_menu(self): - def _set_language(item, code): - self.language = code - if _pygobject: - menu = gtk.Menu.new() - group = [] - else: - menu = gtk.Menu() - group = gtk.RadioMenuItem() - connect = [] - for code, name in self.languages: - if _pygobject: - item = gtk.RadioMenuItem.new_with_label(group, name) - group.append(item) - else: - item = gtk.RadioMenuItem(group, name) - if code == self.language: - item.set_active(True) - connect.append((item, code)) - menu.append(item) - for item, code in connect: - item.connect('activate', _set_language, code) - return menu - - def _suggestion_menu(self, word): - menu = [] - suggestions = self._dictionary.suggest(word) - if not suggestions: - if _pygobject: - item = gtk.MenuItem.new() - label = gtk.Label.new('') - else: - item = gtk.MenuItem() - label = gtk.Label() - try: - label.set_halign(gtk.Align.LEFT) - except AttributeError: - label.set_alignment(0.0, 0.5) - label.set_markup('{text}'.format(text=_('(no suggestions)'))) - item.add(label) - menu.append(item) - else: - for suggestion in suggestions: - if _pygobject: - item = gtk.MenuItem.new() - label = gtk.Label.new('') - else: - item = gtk.MenuItem() - label = gtk.Label() - label.set_markup('{text}'.format(text=suggestion)) - try: - label.set_halign(gtk.Align.LEFT) - except AttributeError: - label.set_alignment(0.0, 0.5) - item.add(label) - item.connect('activate', self._replace_word, word, suggestion) - menu.append(item) - if _pygobject: - menu.append(gtk.SeparatorMenuItem.new()) - item = gtk.MenuItem.new_with_label( - _('Add "{}" to Dictionary').format(word)) - else: - menu.append(gtk.SeparatorMenuItem()) - item = gtk.MenuItem(_('Add "{}" to Dictionary').format(word)) - item.connect('activate', lambda *args: self.add_to_dictionary(word)) - menu.append(item) - if _pygobject: - item = gtk.MenuItem.new_with_label(_('Ignore All')) - else: - item = gtk.MenuItem(_('Ignore All')) - item.connect('activate', lambda *args: self.ignore_all(word)) - menu.append(item) - return menu - - def _extend_menu(self, menu): - if not self._enabled: - return - if _pygobject: - separator = gtk.SeparatorMenuItem.new() - else: - separator = gtk.SeparatorMenuItem() - separator.show() - menu.prepend(separator) - if _pygobject: - languages = gtk.MenuItem.new_with_label(_('Languages')) - else: - languages = gtk.MenuItem(_('Languages')) - languages.set_submenu(self._languages_menu()) - languages.show_all() - menu.prepend(languages) - if self._marks['click'].inside_word: - start, end = self._marks['click'].word - if start.has_tag(self._misspelled): - if _py3k: - word = self._buffer.get_text(start, end, False) - else: - word = self._buffer.get_text(start, end, - False).decode('utf-8') - items = self._suggestion_menu(word) - if self.collapse: - if _pygobject: - suggestions = gtk.MenuItem.new_with_label( - _('Suggestions')) - submenu = gtk.Menu.new() - else: - suggestions = gtk.MenuItem(_('Suggestions')) - submenu = gtk.Menu() - for item in items: - submenu.append(item) - suggestions.set_submenu(submenu) - suggestions.show_all() - menu.prepend(suggestions) - else: - items.reverse() - for item in items: - menu.prepend(item) - menu.show_all() - - def _click_move_popup(self, *args): - self._marks['click'].move(self._buffer.get_iter_at_mark( - self._buffer.get_insert())) - return False - - def _click_move_button(self, widget, event): - if event.button == 3: - if self._deferred_check: self._check_deferred_range(True) - x, y = self._view.window_to_buffer_coords(2, int(event.x), - int(event.y)) - iter = self._view.get_iter_at_location(x, y) - if isinstance(iter, tuple): - iter = iter[1] - self._marks['click'].move(iter) - return False - - def _before_text_insert(self, textbuffer, location, text, length): - self._marks['insert-start'].move(location) - - def _after_text_insert(self, textbuffer, location, text, length): - start = self._marks['insert-start'].iter - self.check_range(start, location) - self._marks['insert-end'].move(location) - - def _range_delete(self, textbuffer, start, end): - self.check_range(start, end) - - def _mark_set(self, textbuffer, location, mark): - if mark == self._buffer.get_insert() and self._deferred_check: - self._check_deferred_range(False) - - def _replace_word(self, item, old_word, new_word): - start, end = self._marks['click'].word - offset = start.get_offset() - self._buffer.begin_user_action() - self._buffer.delete(start, end) - self._buffer.insert(self._buffer.get_iter_at_offset(offset), new_word) - self._buffer.end_user_action() - self._dictionary.store_replacement(old_word, new_word) - - def _check_deferred_range(self, force_all): - start = self._marks['insert-start'].iter - end = self._marks['insert-end'].iter - self.check_range(start, end, force_all) - - def _check_word(self, start, end): - if start.has_tag(self.no_spell_check): - return - for tag in self.ignored_tags: - if start.has_tag(tag): - return - if _py3k: - word = self._buffer.get_text(start, end, False).strip() - else: - word = self._buffer.get_text(start, end, False).decode('utf-8').strip() - if not word: - return - if len(self._filters[SpellChecker.FILTER_WORD]): - if self._regexes[SpellChecker.FILTER_WORD].match(word): - return - if len(self._filters[SpellChecker.FILTER_LINE]): - line_start = self._buffer.get_iter_at_line(start.get_line()) - line_end = end.copy() - line_end.forward_to_line_end() - if _py3k: - line = self._buffer.get_text(line_start, line_end, False) - else: - line = self._buffer.get_text(line_start, line_end, - False).decode('utf-8') - for match in self._regexes[SpellChecker.FILTER_LINE].finditer(line): - if match.start() <= start.get_line_offset() <= match.end(): - start = self._buffer.get_iter_at_line_offset( - start.get_line(), match.start()) - end = self._buffer.get_iter_at_line_offset(start.get_line(), - match.end()) - self._buffer.remove_tag(self._misspelled, start, end) - return - if len(self._filters[SpellChecker.FILTER_TEXT]): - text_start, text_end = self._buffer.get_bounds() - if _py3k: - text = self._buffer.get_text(text_start, text_end, False) - else: - text = self._buffer.get_text(text_start, text_end, - False).decode('utf-8') - for match in self._regexes[SpellChecker.FILTER_TEXT].finditer(text): - if match.start() <= start.get_offset() <= match.end(): - start = self._buffer.get_iter_at_offset(match.start()) - end = self._buffer.get_iter_at_offset(match.end()) - self._buffer.remove_tag(self._misspelled, start, end) - return - if not self._dictionary.check(word): - self._buffer.apply_tag(self._misspelled, start, end) diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py index e70b04e..110ba52 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 """ @@ -141,7 +141,7 @@ def buttons(app): btn.open_recent.pack_start(open_button, False, False, 0) btn.open_recent.pack_end(recents_button, False, False, 0) - btn.search.set_tooltip_text(_("Search and replace")) + btn.search.set_tooltip_text(_("Search and Replace")) btn.menu.set_tooltip_text(_("Menu")) btn.menu.set_image(Gtk.Image.new_from_icon_name("open-menu-symbolic", Gtk.IconSize.BUTTON)) @@ -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/stats_counter.py b/uberwriter/stats_counter.py new file mode 100644 index 0000000..fa4b103 --- /dev/null +++ b/uberwriter/stats_counter.py @@ -0,0 +1,75 @@ +import math +import re +from queue import Queue +from threading import Thread + +from gi.repository import GLib + +from uberwriter import helpers + + +class StatsCounter: + """Counts characters, words, sentences and read time using a background thread.""" + + # Regexp that matches any character, except for newlines and subsequent spaces. + CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))") + + # Regexp that matches Asian letters, general symbols and hieroglyphs, + # as well as sequences of word characters optionally containing non-word characters in-between. + WORDS = re.compile(r"[\u3040-\uffff]|(?:\w+\S?\w*)+", re.UNICODE) + + # Regexp that matches sentence-ending punctuation characters, ie. full stop, question mark, + # exclamation mark, paragraph, and variants. + SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+") + + # Regexp that matches paragraphs, ie. anything separated by newlines. + PARAGRAPHS = re.compile(r".+\n?") + + def __init__(self): + super().__init__() + + self.queue = Queue() + worker = Thread(target=self.__do_count, name="stats-counter") + worker.daemon = True + worker.start() + + def count(self, text, callback): + """Count stats for text, calling callback with a result when done. + + The callback argument contains the result, in the form: + + (characters, words, sentences, (hours, minutes, seconds))""" + + self.queue.put((text, callback)) + + def stop(self): + """Stops the background worker. StatsCounter shouldn't be used after this.""" + + self.queue.put((None, None)) + + def __do_count(self): + while True: + while True: + (text, callback) = self.queue.get() + if text is None and callback is None: + return + if self.queue.empty(): + break + + text = helpers.pandoc_convert(text, to="plain") + + character_count = len(re.findall(self.CHARACTERS, text)) + + word_count = len(re.findall(self.WORDS, text)) + + sentence_count = len(re.findall(self.SENTENCES, text)) + + paragraph_count = len(re.findall(self.PARAGRAPHS, text)) + + read_m, read_s = divmod(word_count / 200 * 60, 60) + read_h, read_m = divmod(read_m, 60) + read_time = (int(read_h), int(read_m), int(read_s)) + + GLib.idle_add( + callback, + (character_count, word_count, sentence_count, paragraph_count, read_time)) diff --git a/uberwriter/stats_handler.py b/uberwriter/stats_handler.py new file mode 100644 index 0000000..8fce782 --- /dev/null +++ b/uberwriter/stats_handler.py @@ -0,0 +1,98 @@ +import math +import re +from gettext import gettext as _ +from queue import Queue +from threading import Thread + +from gi.repository import GLib, Gio, Gtk + +from uberwriter import helpers +from uberwriter.helpers import get_builder +from uberwriter.settings import Settings +from uberwriter.stats_counter import StatsCounter + + +class StatsHandler: + """Shows a default statistic on the stats button, and allows the user to toggle which one.""" + + def __init__(self, stats_button, text_view): + super().__init__() + + self.stats_button = stats_button + self.stats_button.connect("clicked", self.on_stats_button_clicked) + self.stats_button.connect("destroy", self.on_destroy) + + self.text_view = text_view + self.text_view.get_buffer().connect("changed", self.on_text_changed) + + self.popover = None + + self.characters = 0 + self.words = 0 + self.sentences = 0 + self.paragraphs = 0 + self.read_time = (0, 0, 0) + + self.settings = Settings.new() + self.default_stat = self.settings.get_enum("stat-default") + + self.stats_counter = StatsCounter() + + self.update_default_stat() + + def on_stats_button_clicked(self, _button): + self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False) + + menu = Gio.Menu() + stats = self.settings.props.settings_schema.get_key("stat-default").get_range()[1] + for i, stat in enumerate(stats): + menu_item = Gio.MenuItem.new(self.get_text_for_stat(i), None) + menu_item.set_action_and_target_value("app.stat_default", GLib.Variant.new_string(stat)) + menu.append_item(menu_item) + self.popover = Gtk.Popover.new_from_model(self.stats_button, menu) + self.popover.connect('closed', self.on_popover_closed) + self.popover.popup() + + def on_popover_closed(self, _popover): + self.stats_button.unset_state_flags(Gtk.StateFlags.CHECKED) + + self.popover = None + self.text_view.grab_focus() + + def on_text_changed(self, buf): + self.stats_counter.count( + buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False), + self.update_stats) + + def get_text_for_stat(self, stat): + if stat == 0: + return _("{:n} Characters".format(self.characters)) + elif stat == 1: + return _("{:n} Words".format(self.words)) + elif stat == 2: + return _("{:n} Sentences".format(self.sentences)) + elif stat == 3: + return _("{:n} Paragraphs".format(self.paragraphs)) + elif stat == 4: + return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time)) + else: + raise ValueError("Unknown stat {}".format(stat)) + + def update_stats(self, stats): + (characters, words, sentences, paragraphs, read_time) = stats + self.characters = characters + self.words = words + self.sentences = sentences + self.paragraphs = paragraphs + self.read_time = read_time + self.update_default_stat(False) + + def update_default_stat(self, close_popover=True): + stat = self.settings.get_enum("stat-default") + text = self.get_text_for_stat(stat) + self.stats_button.set_label(text) + if close_popover and self.popover: + self.popover.popdown() + + def on_destroy(self, _widget): + self.stats_counter.stop() diff --git a/uberwriter/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..7436372 --- /dev/null +++ b/uberwriter/text_view.py @@ -0,0 +1,205 @@ +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') +gi.require_version('Gspell', '1') +from gi.repository import Gtk, Gdk, GObject, GLib, Gspell + +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) + + # Spell checking + self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self) + self.gspell_view.basic_setup() + + # Undo / redo + self.undo_redo = UndoRedoHandler() + self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text) + 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.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.gspell_view.set_inline_spell_checking(not 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 = 80 + self.props.bottom_margin = 64 + + def on_button_release_event(self, _widget, _event): + if self.focus_mode: + self.markup.apply() + return False + + def set_hemingway_mode(self, hemingway_mode): + """Toggle hemingway mode. + + 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 = 32 + scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__) + if not scrolled_window: + return + va = scrolled_window.get_vadjustment() + if va.props.page_size < margin * 2: + 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..db47156 100644 --- a/uberwriter/theme.py +++ b/uberwriter/theme.py @@ -11,11 +11,11 @@ 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): + def __init__(self, name, web_css_path, is_dark, inverse_name): self.name = name - self.gtk_css_path = gtk_css_path self.web_css_path = web_css_path self.is_dark = is_dark self.inverse_name = inverse_name @@ -29,33 +29,39 @@ 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.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 - Theme('Adwaita', get_css_path('gtk_adwaita.css'), - get_css_path('web_adwaita.css'), False, 'Adwaita-dark'), - Theme('Adwaita-dark', get_css_path('gtk_adwaita_dark.css'), - get_css_path('web_adwaita_dark.css'), True, 'Adwaita'), + Theme('Adwaita', get_css_path('web/adwaita.css'), False, 'Adwaita-dark'), + Theme('Adwaita-dark', get_css_path('web/adwaita_dark.css'), True, 'Adwaita'), # https://github.com/NicoHood/arc-theme/tree/master/common/gtk-3.0/3.20/sass - Theme('Arc', get_css_path('gtk_arc.css'), - get_css_path('web_arc.css'), False, 'Arc-Dark'), - Theme('Arc-Darker', get_css_path('gtk_arc_darker.css'), - get_css_path('web_arc_darker.css'), False, 'Arc-Dark'), - Theme('Arc-Dark', get_css_path('gtk_arc_dark.css'), - get_css_path('web_arc_dark.css'), True, 'Arc'), + Theme('Arc', get_css_path('web/arc.css'), False, 'Arc-Dark'), + Theme('Arc-Darker', get_css_path('web/arc_darker.css'), False, 'Arc-Dark'), + Theme('Arc-Dark', get_css_path('web/arc_dark.css'), True, 'Arc'), # https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/HighContrast - Theme('HighContrast', get_css_path('gtk_high_contrast.css'), - get_css_path('web_high_contrast.css'), False, 'HighContrastInverse'), - Theme('HighContrastInverse', get_css_path('gtk_high_contrast_inverse.css'), - get_css_path('web_high_contrast_inverse.css'), True, 'HighContrast'), + Theme('HighContrast', get_css_path('web/highcontrast.css'), False, 'HighContrastInverse'), + Theme('HighContrastInverse', get_css_path('web/highcontrast_inverse.css'), True, 'HighContrast') ] diff --git a/uberwriter/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 2d33c54..7d91e88 100644 --- a/uberwriter/window.py +++ b/uberwriter/window.py @@ -17,43 +17,34 @@ 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.stats_handler import StatsHandler +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,12 +53,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): - - WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE) + __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, ()) + } def __init__(self, app): """Set up the main window""" @@ -76,38 +72,26 @@ 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_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,117 +100,40 @@ class Window(Gtk.ApplicationWindow): self.add_accel_group(self.accel_group) # Setup text editor - self.text_editor = TextEditor() - self.text_editor.add_events(Gdk.ModifierType.BUTTON1_MASK) - self.text_editor.set_name('UberwriterEditor') - self.get_style_context().add_class('uberwriter_window') - - 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 = TextView() + self.text_view.props.halign = Gtk.Align.CENTER + self.text_view.connect('focus-out-event', self.focus_out) + self.text_view.get_buffer().connect('changed', self.on_text_changed) + self.text_view.show() + self.text_view.grab_focus() # 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') + # Stats counter + self.stats_counter_revealer = self.builder.get_object('stats_counter_revealer') + self.stats_button = self.builder.get_object('stats_counter') + self.stats_button.get_style_context().add_class('stats-counter') + self.stats_handler = StatsHandler(self.stats_button, self.text_view) + + # Setup header/stats bar hide after 3 seconds + self.top_bottom_bars_visible = True + self.was_motion = True + self.buffer_modified_for_status_bar = False + # some people seems to have performance problems with the overlay. # Let them disable it - - 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")) @@ -244,105 +151,42 @@ 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(helpers.get_css_path("gtk/base.css")) + Gtk.StyleContext.add_provider_for_screen( + self.get_screen(), style_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - # Reload preview if it exists, 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() - words = re.split(self.WORDCOUNT, text) - length = len(words) - # Last word a "space" - if not words[-1]: - length = length - 1 - # First word a "space" (happens in focus mode...) - if not words[0]: - length = length - 1 - if length == -1: - length = 0 - self.word_count.set_text(str(length)) - - 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 """ @@ -352,12 +196,7 @@ 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 @@ -375,186 +214,64 @@ 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) + 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 and (self.text_editor.get_events() != 4195088): - # print("pos: %i > %i" % (pos, ha.props.page_size * 0.5)) - - print(self.text_editor.get_events() == Gdk.ModifierType.BUTTON1_MASK) - 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 - 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") - self.get_style_context().add_class("small") + # Ensure the window receiving the event is the one we care about, ie. the main window. + # On Wayland (bug?), sub-windows such as the recents popover will also trigger this. + if event and event.window != window.get_window(): + return - 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 + # Adjust text editor width depending on window width, so that: + # - The number of characters per line is adequate (http://webtypography.net/2.1.2) + # - The number of characters stays constant while resizing the window / font + # - There is enough text margin for MarkupBuffer to apply indents / negative margins + # + # TODO: Avoid hard-coding. Font size is clearer than unclear dimensions, but not ideal. + w_width = event.width if event else window.get_allocation().width + if w_width < 900: + font_size = 14 + self.get_style_context().add_class("small") + self.get_style_context().remove_class("large") + + elif w_width < 1280: + font_size = 16 self.get_style_context().remove_class("small") self.get_style_context().remove_class("large") - 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): @@ -566,7 +283,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 @@ -577,7 +294,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, @@ -600,7 +317,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) @@ -641,7 +358,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) @@ -664,15 +381,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): @@ -682,19 +393,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() @@ -708,14 +424,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) @@ -742,99 +458,42 @@ 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() self.set_headerbar_title(_("New File") + self.title_end) + def update_default_stat(self): + self.stats_handler.update_default_stat() + def menu_toggle_sidebar(self, _widget=None): """WIP """ 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(): - try: - self.spell_checker.enable() - except: - try: - self.spell_checker = SpellChecker( - self.text_editor, locale.getdefaultlocale()[0], - collapse=False) - if self.auto_correct: - self.auto_correct.set_language(self.spell_checker.language) - self.spell_checker.connect_language_change( # pylint: disable=no-member - self.auto_correct.set_language) - except: - self.spell_checker = None - dialog = Gtk.MessageDialog(self, - Gtk.DialogFlags.MODAL \ - | Gtk.DialogFlags.DESTROY_WITH_PARENT, - Gtk.MessageType.INFO, - Gtk.ButtonsType.NONE, - _("You can not enable the Spell Checker.") - ) - dialog.format_secondary_text( - _("Please install 'hunspell' or 'aspell' dictionarys" - + " for your language from the software center.")) - _response = dialog.run() - return - return - else: - try: - self.spell_checker.disable() - except: - pass - return + self.text_view.gspell_view\ + .set_inline_spell_checking(state.get_boolean() + and not self.text_view.focus_mode) - def 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 @@ -852,8 +511,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() @@ -865,49 +524,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) @@ -915,7 +542,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.""" @@ -926,19 +557,14 @@ class Window(Gtk.ApplicationWindow): if filename.startswith('file://'): filename = filename[8:] 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, filename) - self.text_editor.undo_stack = [] - self.text_editor.redo_stack = [] + self.set_headerbar_title(os.path.basename(filename) + self.title_end, filename) self.set_filename(filename) except Exception: @@ -970,7 +596,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() @@ -992,13 +618,13 @@ class Window(Gtk.ApplicationWindow): """ if (self.was_motion is False - and self.status_bar_visible + and self.top_bottom_bars_visible and self.buffer_modified_for_status_bar - and self.text_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.stats_counter_revealer.set_reveal_child(False) self.headerbar.hb_revealer.set_reveal_child(False) - self.status_bar_visible = False + self.top_bottom_bars_visible = False self.buffer_modified_for_status_bar = False self.was_motion = False @@ -1017,26 +643,24 @@ class Window(Gtk.ApplicationWindow): return if now - self.timestamp_last_mouse_motion > 100: # react on motion by fading in headerbar and statusbar - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + if self.top_bottom_bars_visible is False: + self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 - self.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() # self.status_bar.set_state_flags(Gtk.StateFlags.NORMAL, True) self.was_motion = True def focus_out(self, _widget, _data=None): """events called when the window losses focus """ - if self.status_bar_visible is False: - self.statusbar_revealer.set_reveal_child(True) + if self.top_bottom_bars_visible is False: + self.stats_counter_revealer.set_reveal_child(True) self.headerbar.hb_revealer.set_reveal_child(True) self.headerbar.hb.props.opacity = 1 - self.status_bar_visible = True + self.top_bottom_bars_visible = True self.buffer_modified_for_status_bar = False - self.update_line_and_char_count() def draw_gradient(self, _widget, cr): """draw fading gradient over the top and the bottom of the @@ -1044,7 +668,7 @@ class Window(Gtk.ApplicationWindow): """ bg_color = self.get_style_context().get_background_color(Gtk.StateFlags.ACTIVE) - lg_top = cairo.LinearGradient(0, 0, 0, 35) # pylint: disable=no-member + lg_top = cairo.LinearGradient(0, 0, 0, 32) # pylint: disable=no-member lg_top.add_color_stop_rgba( 0, bg_color.red, bg_color.green, bg_color.blue, 1) lg_top.add_color_stop_rgba( @@ -1053,12 +677,12 @@ class Window(Gtk.ApplicationWindow): width = self.scrolled_window.get_allocation().width height = self.scrolled_window.get_allocation().height - cr.rectangle(0, 0, width, 35) + cr.rectangle(0, 0, width, 32) cr.set_source(lg_top) cr.fill() - cr.rectangle(0, height - 35, width, height) + cr.rectangle(0, height - 32, width, height) - lg_btm = cairo.LinearGradient(0, height - 35, 0, height) # pylint: disable=no-member + lg_btm = cairo.LinearGradient(0, height - 32, 0, height) # pylint: disable=no-member lg_btm.add_color_stop_rgba( 1, bg_color.red, bg_color.green, bg_color.blue, 1) lg_btm.add_color_stop_rgba( @@ -1067,24 +691,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. """ @@ -1094,7 +700,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? @@ -1102,12 +708,6 @@ class Window(Gtk.ApplicationWindow): self.destroy() return - def on_destroy(self, _widget, _data=None): - """Called when the TexteditorWindow is closed. - """ - # Clean up code for saving application state should be added here. - Gtk.main_quit() - def set_headerbar_title(self, title, subtitle=""): """set the desired headerbar title """