diff --git a/data/de.wolfvollprecht.UberWriter.gschema.xml b/data/de.wolfvollprecht.UberWriter.gschema.xml
index e7fdb28..7a3b93b 100644
--- a/data/de.wolfvollprecht.UberWriter.gschema.xml
+++ b/data/de.wolfvollprecht.UberWriter.gschema.xml
@@ -4,35 +4,47 @@
+
+ true
+ Set dark mode automatically
+
+ Whether dark mode depends on the system theme, or is set to what the user specifies.
+
+ false
- Dark mode
+ Force dark mode
- If enabled, the window will be dark themed
- If disabled, the window will be light themed
- asked to install them manually.
+ Enable or disable the dark mode.
true
- Spellcheck
+ Check spelling while typing
- Enable/disable spellchecking in the application.
+ Enable or disable spellchecking.
false
- Show gradient overlay
+ Draw scroll gradient
Show a gradient overlay over the text at the top anf bottom of the window.
- It can cause performance problems to some users
+ It can cause performance problems to some users.
+
+
+
+ "markdown"
+ Input format
+
+ Input format to use when previewing and exporting using Pandoc.
trueAllow Uberwriter to poll cursor motion
- Used for hidding the headerbar after 3 seconds if the cursor is not moving.
+ Hide the header and status bars if the cursor is not moving.
diff --git a/data/media/css/_gtk_base.css b/data/media/css/_gtk_base.css
index fdbf0cf..84261ab 100644
--- a/data/media/css/_gtk_base.css
+++ b/data/media/css/_gtk_base.css
@@ -8,42 +8,39 @@
bind "i" { "insert-italic" () };
bind "b" { "insert-bold" () };
bind "r" { "insert-hrule" () };
- bind "u" { "insert-ulistitem" () };
- bind "h" { "insert-heading" () };
+ bind "u" { "insert-listitem" () };
+ bind "h" { "insert-header" () };
bind "z" { "undo" () };
bind "y" { "redo" () };
- bind "d" { "insert-strikeout" () };
+ bind "d" { "insert-strikethrough" () };
/*bind "t" { "insert-at-cursor" ('[ ] ') };*/
bind "z" { "redo" () };
}
/* Main window and text colors */
-.uberwriter_window {
+.uberwriter-window {
/*border-radius: 7px 7px 3px 3px;*/
background: @background_color;
caret-color: @foreground_color;
}
-.uberwriter_window.small .uberwriter-editor {
+.uberwriter-window .uberwriter-editor {
font-family: 'Fira Mono', monospace;
- font-size: 12px;
-}
-.uberwriter_window grid {
- background-color: @background_color;
+ font-size: 16px;
+ padding-top: 80px;
+ padding-bottom: 16px;
}
-#UberwriterWindow.medium .uberwriter-editor {
- font-family: 'Fira Mono', monospace;
- font-size: 15px;
+.uberwriter-window.small .uberwriter-editor {
+ font-size: 14px;
}
-#UberwriterWindow.large .uberwriter-editor {
- font-family: 'Fira Mono', monospace;
+.uberwriter-window.large .uberwriter-editor {
font-size: 18px;
}
-#titlebar_revealer {
+#titlebar-revealer {
padding: 0;
}
@@ -52,8 +49,8 @@
background: transparent;
}
-#titlebar_container {
- background: @background_color;
+#titlebar-container {
+ background: @background_color;
}
.uberwriter-editor {
@@ -89,11 +86,11 @@
}
-.status_bar_box label {
+.status-bar-box label {
color: #666;
}
-.status_bar_box button {
+.status-bar-box button {
/* finding reset */
background-color: @background_color;
text-shadow: inherit;
@@ -118,26 +115,26 @@
transition: 100ms ease-in;
}
-.status_bar_box button:hover,
-.status_bar_box button:checked {
+.status-bar-box button:hover,
+.status-bar-box button:checked {
transition: 0s ease-in;
color: @background_color;
background-color: #666;
}
-.status_bar_box button:hover label,
-.status_bar_box button:checked label {
+.status-bar-box button:hover label,
+.status-bar-box button:checked label {
color: @background_color;
}
-.status_bar_box button:active {
+.status-bar-box button:active {
color: #EEE;
background-color: #EEE;
background-image: none;
box-shadow: 0 0 2px rgba(0,0,0,0.4)
}
-.status_bar_box separator {
+.status-bar-box separator {
border-color: #999;
border-right: none;
}
@@ -150,7 +147,7 @@
background: #FFF;
}
-#UberwriterWindow treeview {
+.uberwriter-window treeview {
padding: 3px 3px 3px 3px;
}
@@ -165,7 +162,7 @@
padding: 5px;
}
-/* .QuickPreviewPopup {
+/* .quick-preview-popup {
padding: 5px;
margin: 5px;
border: 1px solid #333;
@@ -183,8 +180,7 @@
border: 5px solid @background_color;
}
-#LexikonBubble .lexikon_heading {
- /*font: serif 12;*/
+#LexikonBubble .lexikon-heading {
font-family: serif;
font-size: 12px;
padding-bottom: 5px;
@@ -193,21 +189,21 @@
padding-left: 10px;
}
-#LexikonBubble .lexikon_num {
+#LexikonBubble .lexikon-num {
padding-right: 5px;
padding-left: 20px;
}
-.QuickPreviewPopup {
+.quick-preview-popup {
background-color: @background_color;
}
-.QuickPreviewPopup grid {
+.quick-preview-popup grid {
background-color: @background_color;
color: @foreground_color;
border-color: @background_color;
}
-.QuickPreviewPopup label {
+.quick-preview-popup label {
color: @foreground_color;
}
\ No newline at end of file
diff --git a/data/ui/Export.ui b/data/ui/Export.ui
index 011d433..3ea4419 100644
--- a/data/ui/Export.ui
+++ b/data/ui/Export.ui
@@ -6,7 +6,12 @@
TrueFalsecenter
+ 2
+ 2
+ 2
+ 2True
+ 2
diff --git a/help/stump/help.md b/help/stump/help.md
index c63bf5b..e4a98a9 100644
--- a/help/stump/help.md
+++ b/help/stump/help.md
@@ -619,9 +619,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte
feas*ible*, not feas*able*.
-#### Strikeout
+#### Strikethrough
-To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
+To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
This ~~is deleted text.~~
@@ -1183,7 +1183,7 @@ Sergey Astanin.
[Slidy]: http://www.w3.org/Talks/Tools/Slidy/
[Slideous]: http://goessner.net/articles/slideous/
[HTML]: http://www.w3.org/TR/html40/
-[HTML 5]: http://www.w3.org/TR/html5/
+[HTML5]: http://www.w3.org/TR/html5/
[XHTML]: http://www.w3.org/TR/xhtml1/
[LaTeX]: http://www.latex-project.org/
[beamer]: http://www.tex.ac.uk/CTAN/macros/latex/contrib/beamer
diff --git a/help/stump/help_backup.html b/help/stump/help_backup.html
index 6c05d16..dc82ab5 100644
--- a/help/stump/help_backup.html
+++ b/help/stump/help_backup.html
@@ -47,8 +47,8 @@ is *emphasized with asterisks*.
This is * not emphasized *, and \*neither is this\*.
Because _ is sometimes used inside words and identifiers, pandoc does not interpret a _ surrounded by alphanumeric characters as an emphasis marker. If you want to emphasize just part of a word, use *:
feas*ible*, not feas*able*.
-
Strikeout
-
To strikeout a section of text with a horizontal line, begin and end it with ~~. Thus, for example,
+
Strikethrough
+
To strikethrough a section of text with a horizontal line, begin and end it with ~~. Thus, for example,
This ~~is deleted text.~~
Block quotations
Markdown uses email conventions for quoting blocks of text. A block quotation is one or more paragraphs or other block elements (such as lists or headers), with each line preceded by a > character and a space.
diff --git a/help/stump/help_backup.md b/help/stump/help_backup.md
index 6395e5c..17518d1 100644
--- a/help/stump/help_backup.md
+++ b/help/stump/help_backup.md
@@ -67,9 +67,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte
feas*ible*, not feas*able*.
-### Strikeout
+### Strikethrough
-To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
+To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
This ~~is deleted text.~~
diff --git a/requirements.txt b/requirements.txt
index 340d2e0..971e047 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
regex
enchant
python-gtkspellcheck
-pandoc
+pypandoc==1.4
diff --git a/uberwriter/__init__.py b/uberwriter/__init__.py
index f062c1e..8632281 100644
--- a/uberwriter/__init__.py
+++ b/uberwriter/__init__.py
@@ -15,36 +15,21 @@
### END LICENSE
import sys
-import locale
-import os
-
-import gettext
-from gettext import gettext as _
-
import gi
+
gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk # pylint: disable=E0611
from uberwriter import window
from uberwriter import application
from uberwriter.helpers import set_up_logging
-from uberwriter.uberwriterconfig import get_version
+from uberwriter.config import get_version
def main():
'constructor for your class instances'
# (options, args) = parse_options()
-
+
# Run the application.
app = application.Application()
-
- # ~ if args:
- # ~ for arg in args:
- # ~ pass
- # ~ else:
- # ~ pass
- # ~ if options.experimental_features:
- # ~ window.use_experimental_features(True)
-
+
app.run(sys.argv)
-
diff --git a/uberwriter/application.py b/uberwriter/application.py
index 304fed1..39038c0 100644
--- a/uberwriter/application.py
+++ b/uberwriter/application.py
@@ -11,17 +11,14 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
-import webbrowser
from gettext import gettext as _
import gi
-
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
-from gi.repository import GLib, Gio, Gtk, Gdk, GdkPixbuf
+from gi.repository import GLib, Gio, Gtk, GdkPixbuf
from uberwriter import window
-from uberwriter.theme import Theme
from uberwriter.settings import Settings
from uberwriter.helpers import set_up_logging
from uberwriter.preferences_dialog import PreferencesDialog
@@ -41,74 +38,7 @@ class Application(Gtk.Application):
Gtk.Application.do_startup(self)
- # Actions
-
- action = Gio.SimpleAction.new("help", None)
- action.connect("activate", self.on_help)
- self.add_action(action)
-
- action = Gio.SimpleAction.new("shortcuts", None)
- action.connect("activate", self.on_shortcuts)
- self.add_action(action)
-
- action = Gio.SimpleAction.new("about", None)
- action.connect("activate", self.on_about)
- self.add_action(action)
-
- action = Gio.SimpleAction.new("quit", None)
- action.connect("activate", self.on_quit)
- self.add_action(action)
-
- set_dark_mode = self.settings.get_value("dark-mode")
- action = Gio.SimpleAction.new_stateful("dark_mode",
- None,
- GLib.Variant.new_boolean(set_dark_mode))
- action.connect("change-state", self.on_dark_mode)
- self.add_action(action)
-
- action = Gio.SimpleAction.new_stateful("focus_mode",
- None,
- GLib.Variant.new_boolean(False))
- action.connect("change-state", self.on_focus_mode)
- self.add_action(action)
-
- action = Gio.SimpleAction.new_stateful("hemingway_mode",
- None,
- GLib.Variant.new_boolean(False))
- action.connect("change-state", self.on_hemingway_mode)
- self.add_action(action)
-
- action = Gio.SimpleAction.new_stateful("fullscreen",
- None,
- GLib.Variant.new_boolean(False))
- action.connect("change-state", self.on_fullscreen)
- self.add_action(action)
-
- action = Gio.SimpleAction.new_stateful("preview",
- None,
- GLib.Variant.new_boolean(False))
- action.connect("change-state", self.on_preview)
- self.add_action(action)
-
- action = Gio.SimpleAction.new("search", None)
- action.connect("activate", self.on_search)
- self.add_action(action)
-
- set_spellcheck = self.settings.get_value("spellcheck")
- action = Gio.SimpleAction.new_stateful("spellcheck",
- None,
- GLib.Variant.new_boolean(set_spellcheck))
- action.connect("change-state", self.on_spellcheck)
- self.add_action(action)
-
- set_gradient_overlay = self.settings.get_value("gradient-overlay")
- action = Gio.SimpleAction.new_stateful("draw_gradient",
- None,
- GLib.Variant.new_boolean(set_gradient_overlay))
- action.connect("change-state", self.on_draw_gradient)
- self.add_action(action)
-
- # Menu Actions
+ self.settings.connect("changed", self.on_settings_changed)
action = Gio.SimpleAction.new("new", None)
action.connect("activate", self.on_new)
@@ -122,14 +52,34 @@ class Application(Gtk.Application):
action.connect("activate", self.on_open_recent)
self.add_action(action)
- action = Gio.SimpleAction.new("open_examples", None)
- action.connect("activate", self.on_example)
- self.add_action(action)
-
action = Gio.SimpleAction.new("save", None)
action.connect("activate", self.on_save)
self.add_action(action)
+ action = Gio.SimpleAction.new("search", None)
+ action.connect("activate", self.on_search)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ "focus_mode", None, GLib.Variant.new_boolean(False))
+ action.connect("change-state", self.on_focus_mode)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ "hemingway_mode", None, GLib.Variant.new_boolean(False))
+ action.connect("change-state", self.on_hemingway_mode)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ "preview", None, GLib.Variant.new_boolean(False))
+ action.connect("change-state", self.on_preview)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new_stateful(
+ "fullscreen", None, GLib.Variant.new_boolean(False))
+ action.connect("change-state", self.on_fullscreen)
+ self.add_action(action)
+
action = Gio.SimpleAction.new("save_as", None)
action.connect("activate", self.on_save_as)
self.add_action(action)
@@ -138,14 +88,30 @@ class Application(Gtk.Application):
action.connect("activate", self.on_export)
self.add_action(action)
- action = Gio.SimpleAction.new("HTML_copy", None)
- action.connect("activate", self.on_html_copy)
+ action = Gio.SimpleAction.new("copy_html", None)
+ action.connect("activate", self.on_copy_html)
self.add_action(action)
action = Gio.SimpleAction.new("preferences", None)
action.connect("activate", self.on_preferences)
self.add_action(action)
+ action = Gio.SimpleAction.new("shortcuts", None)
+ action.connect("activate", self.on_shortcuts)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new("open_tutorial", None)
+ action.connect("activate", self.on_open_tutorial)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new("about", None)
+ action.connect("activate", self.on_about)
+ self.add_action(action)
+
+ action = Gio.SimpleAction.new("quit", None)
+ action.connect("activate", self.on_quit)
+ self.add_action(action)
+
# Shortcuts
# TODO: be aware that a couple of shortcuts are defined in _gtk_base.css
@@ -163,8 +129,6 @@ class Application(Gtk.Application):
self.set_accels_for_action("app.save_as", ["s"])
self.set_accels_for_action("app.quit", ["w", "q"])
- self.apply_current_theme()
-
def do_activate(self, *args, **kwargs):
# We only allow a single window and raise any existing ones
if not self.window:
@@ -174,8 +138,6 @@ class Application(Gtk.Application):
self.window = window.Window(self)
if self.args:
self.window.load_file(self.args[0])
- if self.options.experimental_features:
- self.window.use_experimental_features(True)
self.window.present()
@@ -187,8 +149,7 @@ class Application(Gtk.Application):
help=_("Show debug messages (-vv debugs uberwriter also)"))
parser.add_argument(
"-e", "--experimental-features", help=_("Use experimental features"),
- action='store_true'
- )
+ action='store_true')
(self.options, self.args) = parser.parse_known_args()
set_up_logging(self.options)
@@ -196,90 +157,15 @@ class Application(Gtk.Application):
self.activate()
return 0
- def apply_current_theme(self):
- # get current theme
- theme = Theme.get_current()
-
- # set theme variant (dark/light)
- Gtk.Settings.get_default().set_property(
- "gtk-application-prefer-dark-theme",
- GLib.Variant("b", theme.is_dark))
-
- # set theme css
- style_provider = Gtk.CssProvider()
- style_provider.load_from_path(theme.gtk_css_path)
- Gtk.StyleContext.add_provider_for_screen(
- Gdk.Screen.get_default(), style_provider,
- Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
- )
-
- def on_about(self, _action, _param):
- builder = get_builder('About')
- about_dialog = builder.get_object("AboutDialog")
- about_dialog.set_transient_for(self.window)
-
- logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg")
- logo = GdkPixbuf.Pixbuf.new_from_file(logo_file)
-
- about_dialog.set_logo(logo)
-
- about_dialog.present()
-
- def on_help(self, _action, _param):
- """open pandoc markdown web
- """
- webbrowser.open(
- "http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown")
-
- def on_shortcuts(self, _action, _param):
- builder = get_builder('Shortcuts')
- builder.get_object("shortcuts").set_transient_for(self.window)
- builder.get_object("shortcuts").show()
-
- def on_dark_mode(self, action, value):
- action.set_state(value)
- self.settings.set_value("dark-mode", GLib.Variant("b", value))
-
- # this changes the headerbar theme accordingly
- self.apply_current_theme()
-
- # adjust window for theme
- self.window.apply_current_theme()
-
- def on_focus_mode(self, action, value):
- action.set_state(value)
- self.window.set_focus_mode(value)
-
- def on_hemingway_mode(self, action, value):
- action.set_state(value)
- self.window.set_hemingway_mode(value)
-
- def on_fullscreen(self, action, value):
- action.set_state(value)
- self.window.set_fullscreen(value)
-
- def on_preview(self, action, value):
- action.set_state(value)
- self.window.toggle_preview(value)
-
- def on_search(self, _action, _value):
- self.window.open_search_and_replace()
-
- def on_spellcheck(self, action, value):
- action.set_state(value)
- self.settings.set_value("spellcheck",
- GLib.Variant("b", value))
- self.window.toggle_spellcheck(value)
-
- def on_draw_gradient(self, action, value):
- action.set_state(value)
- self.settings.set_value("gradient-overlay",
- GLib.Variant("b", value))
- if value:
- self.window.overlay = self.window.scrolled_window.connect_after(
- "draw", self.window.draw_gradient)
- else:
- self.window.scrolled_window.disconnect(self.window.overlay)
+ def on_settings_changed(self, settings, key):
+ if key == "dark-mode-auto" or key == "dark-mode":
+ self.window.apply_current_theme()
+ elif key == "spellcheck":
+ self.window.toggle_spellcheck(settings.get_value(key))
+ elif key == "gradient-overlay":
+ self.window.toggle_gradient_overlay(settings.get_value(key))
+ elif key == "input-format":
+ self.window.reload_preview()
def on_new(self, _action, _value):
self.window.new_document()
@@ -290,26 +176,58 @@ class Application(Gtk.Application):
def on_open_recent(self, file):
self.window.load_file(file.get_current_uri())
- def on_example(self, _action, _value):
- self.window.open_uberwriter_markdown()
-
def on_save(self, _action, _value):
self.window.save_document()
+ def on_search(self, _action, _value):
+ self.window.open_search_and_replace()
+
+ def on_focus_mode(self, action, value):
+ action.set_state(value)
+ self.window.set_focus_mode(value)
+
+ def on_hemingway_mode(self, action, value):
+ action.set_state(value)
+ self.window.set_hemingway_mode(value)
+
+ def on_preview(self, action, value):
+ action.set_state(value)
+ self.window.toggle_preview(value)
+
+ def on_fullscreen(self, action, value):
+ action.set_state(value)
+ self.window.set_fullscreen(value)
+
def on_save_as(self, _action, _value):
self.window.save_document_as()
def on_export(self, _action, _value):
self.window.open_advanced_export()
- def on_html_copy(self, _action, _value):
+ def on_copy_html(self, _action, _value):
self.window.copy_html_to_clipboard()
def on_preferences(self, _action, _value):
- preferences_window = PreferencesDialog()
- preferences_window.set_application(self)
- preferences_window.set_transient_for(self.window)
- preferences_window.show()
+ PreferencesDialog(self.settings).show(self.window)
+
+ def on_shortcuts(self, _action, _param):
+ builder = get_builder('Shortcuts')
+ builder.get_object("shortcuts").set_transient_for(self.window)
+ builder.get_object("shortcuts").show()
+
+ def on_open_tutorial(self, _action, _value):
+ self.window.open_uberwriter_markdown()
+
+ def on_about(self, _action, _param):
+ builder = get_builder('About')
+ about_dialog = builder.get_object("AboutDialog")
+ about_dialog.set_transient_for(self.window)
+
+ logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg")
+ logo = GdkPixbuf.Pixbuf.new_from_file(logo_file)
+
+ about_dialog.set_logo(logo)
+ about_dialog.present()
def on_quit(self, _action, _param):
self.quit()
diff --git a/uberwriter/uberwriterconfig.py b/uberwriter/config.py
similarity index 100%
rename from uberwriter/uberwriterconfig.py
rename to uberwriter/config.py
diff --git a/uberwriter/export_dialog.py b/uberwriter/export_dialog.py
index 7a2362f..d65b583 100644
--- a/uberwriter/export_dialog.py
+++ b/uberwriter/export_dialog.py
@@ -17,14 +17,11 @@
"""
-import os
-import subprocess
import logging
-# import gettext
-
+import os
from gettext import gettext as _
-import gi
+import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
@@ -35,6 +32,7 @@ from uberwriter.helpers import get_builder
LOGGER = logging.getLogger('uberwriter')
+
class Export:
"""
Manages all the export operations and dialogs
@@ -42,6 +40,114 @@ class Export:
__gtype_name__ = "export_dialog"
+ formats = [
+ {
+ "name": "LaTeX (pdf)",
+ "ext": "pdf",
+ "to": "pdf"
+ },
+ {
+ "name": "LaTeX Beamer Slideshow (pdf)",
+ "ext": "pdf",
+ "to": "beamer"
+ },
+ {
+ "name": "LaTeX (tex)",
+ "ext": "tex",
+ "to": "latex"
+ },
+ {
+ "name": "LaTeX Beamer Slideshow (tex)",
+ "ext": "tex",
+ "to": "beamer"
+ },
+ {
+ "name": "ConTeXt",
+ "ext": "tex",
+ "to": "context"
+ },
+ {
+ "name": "HTML",
+ "ext": "html",
+ "to": "html"
+ },
+ {
+ "name": "HTML and JavaScript Slideshow (Slidy)",
+ "ext": "html",
+ "to": "slidy"
+ },
+ {
+ "name": "HTML and JavaScript Slideshow (Slideous)",
+ "ext": "html",
+ "to": "slideous"
+ },
+ {
+ "name": "HTML5 and JavaScript Slideshow (DZSlides)",
+ "ext": "html",
+ "to": "dzslides"
+ },
+ {
+ "name": "HTML5 and JavaScript Slideshow (reveal.js)",
+ "ext": "html",
+ "to": "revealjs"
+ },
+ {
+ "name": "HTML and JavaScript Slideshow (S5)",
+ "ext": "html",
+ "to": "s5"
+ },
+ {
+ "name": "Textile",
+ "ext": "txt",
+ "to": "textile"
+ },
+ {
+ "name": "reStructuredText",
+ "ext": "txt",
+ "to": "rst"
+ },
+ {
+ "name": "MediaWiki Markup",
+ "ext": "txt",
+ "to": "mediawiki"
+ },
+ {
+ "name": "OpenDocument (xml)",
+ "ext": "xml",
+ "to": "opendocument"
+ },
+ {
+ "name": "OpenDocument (texi)",
+ "ext": "texi",
+ "to": "texinfo"
+ },
+ {
+ "name": "OpenOffice Text Document",
+ "ext": "odt",
+ "to": "odt"
+ },
+ {
+ "name": "Microsoft Word (docx)",
+ "ext": "docx",
+ "to": "docx"
+ },
+ {
+ "name": "Rich Text Format",
+ "ext": "rtf",
+ "to": "rtf"
+ },
+ {
+ "name": "Groff Man",
+ "ext": "man",
+ "to": "man"
+ },
+ {
+ "name": "EPUB v3",
+ "ext": "epub",
+ "to": "epub"
+ }
+ ]
+
def __init__(self, filename):
"""Set up the about dialog"""
self.builder = get_builder('Export')
@@ -52,8 +158,8 @@ class Export:
stack_pdf_disabled = self.builder.get_object("pdf_disabled")
filename = filename or _("Untitled document.md")
- self.filechoosers = {export_format:self.stack.get_child_by_name(export_format)\
- for export_format in ["pdf", "html", "odt", "advanced"]}
+ self.filechoosers = {export_format: self.stack.get_child_by_name(export_format)
+ for export_format in ["pdf", "html", "advanced"]}
for export_format, filechooser in self.filechoosers.items():
filechooser.set_do_overwrite_confirmation(True)
filechooser.set_current_folder(os.path.dirname(filename))
@@ -75,9 +181,12 @@ class Export:
self.builder.get_object("highlight_style").set_active(0)
+ self.builder.get_object("css_filechooser").set_uri(
+ helpers.path_to_file(Theme.get_current().web_css_path))
+
format_store = Gtk.ListStore(int, str)
- for fmt_id in self.formats_dict:
- format_store.append([fmt_id, self.formats_dict[fmt_id]["name"]])
+ for i, fmt in enumerate(self.formats):
+ format_store.append([i, fmt["name"]])
self.format_field = self.builder.get_object('choose_format')
self.format_field.set_model(format_store)
@@ -86,171 +195,56 @@ class Export:
self.format_field.add_attribute(format_renderer, "text", 1)
self.format_field.set_active(0)
- formats_dict = {
- 1: {
- "name": "LaTeX Source",
- "ext": "tex",
- "to": "latex"
- },
- 2: {
- "name": "LaTeX PDF",
- "ext": "pdf",
- "to": "pdf"
- },
- 3: {
- "name": "LaTeX beamer slide show Source .tex",
- "ext": "tex",
- "to": "beamer"
- },
- 4: {
- "name": "LaTeX beamer slide show PDF",
- "ext": "pdf",
- "to": "beamer"
- },
- 5: {
- "name": "HTML",
- "ext": "html",
- "to": "html"
- },
- 6: {
- "name": "Textile",
- "ext": "txt",
- "to": "textile"
- },
- 7: {
- "name": "OpenOffice text document",
- "ext": "odt",
- "to": "odt"
- },
- 8: {
- "name": "Word docx",
- "ext": "docx",
- "to": "docx"
- },
- 9: {
- "name": "reStructuredText txt",
- "ext": "txt",
- "to": "rst"
- },
- 10: {
- "name": "ConTeXt tex",
- "ext": "tex",
- "to": "context"
- },
- 11: {
- "name": "groff man",
- "ext": "man",
- "to": "man"
- },
- 12: {
- "name": "MediaWiki markup",
- "ext": "txt",
- "to": "mediawiki"
- },
- 13: {
- "name": "OpenDocument XML",
- "ext": "xml",
- "to": "opendocument"
- },
- 14: {
- "name": "OpenDocument XML",
- "ext": "texi",
- "to": "texinfo"
- },
- 15: {
- "name": "Slidy HTML and javascript slide show",
- "ext": "html",
- "to": "slidy"
- },
- 16: {
- "name": "Slideous HTML and javascript slide show",
- "ext": "html",
- "to": "slideous"
- },
- 17: {
- "name": "HTML5 + javascript slide show",
- "ext": "html",
- "to": "dzslides"
- },
- 18: {
- "name": "S5 HTML and javascript slide show",
- "ext": "html",
- "to": "s5"
- },
- 19: {
- "name": "EPub electronic publication",
- "ext": "epub",
- "to": "epub"
- },
- 20: {
- "name": "RTF Rich Text Format",
- "ext": "rtf",
- "to": "rtf"
- }
-
- }
-
def export(self, text=""):
- """Export to pdf, html or odt the given text
+ """Export the given text using the specified format.
+ For advanced export, this includes special flags for the enabled options.
Keyword Arguments:
text {str} -- Text to export (default: {""})
"""
- export_format = self.stack.get_visible_child_name()
+ export_type = self.stack.get_visible_child_name()
+ args = []
+ if export_type == "advanced":
+ filename = self.adv_export_name.get_text()
+ output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder())
+ basename = os.path.basename(filename)
+
+ fmt = self.formats[self.format_field.get_active()]
+ to = fmt["to"]
+ ext = fmt["ext"]
+
+ if self.builder.get_object("html5").get_active() and to == "html":
+ to = "html5"
+ if self.builder.get_object("smart").get_active():
+ to += "+smart"
+
+ args.extend(self.get_advanced_arguments())
- if export_format == "advanced":
- self.advanced_export(text)
else:
- filename = self.filechoosers[export_format].get_filename()
- if filename.endswith("." + export_format):
- filename = filename[:-len(export_format)-1]
-
+ filename = self.filechoosers[export_type].get_filename()
+ if filename.endswith("." + export_type):
+ filename = filename[:-len(export_type)-1]
output_dir = os.path.abspath(os.path.join(filename, os.path.pardir))
basename = os.path.basename(filename)
- args = ['pandoc', '--from=markdown', '-s']
+ to = export_type
+ ext = export_type
- if export_format == "pdf":
- args.append("-o%s.pdf" % basename)
-
- elif export_format == "odt":
- args.append("-o%s.odt" % basename)
-
- elif export_format == "html":
- css = Theme.ADWAITA.get_gtk_css_file()
- relativize = helpers.get_script_path('relative_to_absolute.lua')
- task_list = helpers.get_script_path('task-list.lua')
- args.append("-c%s" % css)
- args.append("-o%s.html" % basename)
+ if export_type == "html":
+ to = "html5"
+ args.append("--standalone")
+ args.append("--css=%s" % Theme.get_current().web_css_path)
args.append("--mathjax")
- args.append("--lua-filter=" + relativize)
- args.append("--lua-filter=" + task_list)
+ args.append("--lua-filter=%s" % helpers.get_script_path('relative_to_absolute.lua'))
+ args.append("--lua-filter=%s" % helpers.get_script_path('task-list.lua'))
- proc = subprocess.Popen(args, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, cwd=output_dir)
- _ = proc.communicate(text)[0]
+ helpers.pandoc_convert(
+ text, to=to, args=args,
+ outputfile="%s/%s.%s" % (output_dir, basename, ext))
- def advanced_export(self, text=""):
- """Export the given text to special formats with the enabled flags
-
- Keyword Arguments:
- text {str} -- The text to export (default: {""})
- """
-
- filename = self.adv_export_name.get_text()
- output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder())
- basename = os.path.basename(filename)
- args = self.set_arguments(basename)
-
- LOGGER.info(args)
-
- proc = subprocess.Popen(args, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE, cwd=output_dir)
- _ = proc.communicate(text)[0]
-
- def set_arguments(self, basename):
- """Retrieve a list of the selected arguments
+ def get_advanced_arguments(self):
+ """Retrieve a list of the selected advanced arguments
For most of the advanced option checkboxes, returns a list
of the related pandoc flags
@@ -264,78 +258,49 @@ class Export:
highlight_style = self.builder.get_object("highlight_style").get_active_text()
- conditions_dict = {
- 1: {
+ conditions = [
+ {
"condition": self.builder.get_object("toc").get_active(),
"yes": "--toc",
"no": None
},
- 2: {
+ {
"condition": self.builder.get_object("highlight").get_active(),
"yes": "--highlight-style=%s" % highlight_style,
"no": "--no-highlight"
},
- 3: {
+ {
"condition": self.builder.get_object("standalone").get_active(),
"yes": "--standalone",
"no": None
},
- 4: {
+ {
"condition": self.builder.get_object("number_sections").get_active(),
"yes": "--number-sections",
"no": None
},
- 5: {
+ {
"condition": self.builder.get_object("strict").get_active(),
"yes": "--strict",
"no": None
},
- 6: {
+ {
"condition": self.builder.get_object("incremental").get_active(),
"yes": "--incremental",
"no": None
},
- 7: {
+ {
"condition": self.builder.get_object("self_contained").get_active(),
"yes": "--self-contained",
"no": None
}
- }
+ ]
- tree_iter = self.format_field.get_active_iter()
- if tree_iter is not None:
- model = self.format_field.get_model()
- row_id, _ = model[tree_iter][:2]
+ args = []
- fmt = self.formats_dict[row_id]
+ args.extend([c["yes"] if c["condition"] else c["no"] for c in conditions])
- args = ['pandoc', '--from=markdown']
-
- extension = "--to=%s" % fmt["to"]
-
- if basename.endswith("." + fmt["ext"]):
- output_file = "--output=%s" % basename
- else:
- output_file = "--output=%s.%s" % (basename, fmt["ext"])
-
- args.extend([conditions_dict[c_id]["yes"]\
- if conditions_dict[c_id]["condition"]\
- else conditions_dict[c_id]["no"]\
- for c_id in conditions_dict])
-
- args = list(filter(None, args))
-
- if self.builder.get_object("html5").get_active():
- if fmt["to"] == "html":
- extension = "--to=%s" % "html5"
-
- if self.builder.get_object("smart").get_active():
- extension += '+smart'
- else:
- extension += '-smart'
-
- if fmt["to"] != "pdf":
- args.append(extension)
+ args = list(filter(lambda arg: arg is not None, args))
css_uri = self.builder.get_object("css_filechooser").get_uri()
if css_uri:
@@ -349,8 +314,6 @@ class Export:
bib_uri = bib_uri[7:]
args.append("--bibliography=%s" % bib_uri)
- args.append(output_file)
-
return args
def allow_export(self, widget, data, signal):
diff --git a/uberwriter/format_shortcuts.py b/uberwriter/format_shortcuts.py
index 0d0b030..19e0a91 100644
--- a/uberwriter/format_shortcuts.py
+++ b/uberwriter/format_shortcuts.py
@@ -17,18 +17,14 @@
from gettext import gettext as _
-from uberwriter.markup_buffer import MarkupBuffer
-
class FormatShortcuts():
"""Manage the insertion of formatting for insert them using shortcuts
"""
-
def __init__(self, textbuffer, texteditor):
self.text_buffer = textbuffer
self.text_editor = texteditor
- self.regex = MarkupBuffer.regex
def rule(self):
"""insert ruler at cursor
diff --git a/uberwriter/headerbars.py b/uberwriter/headerbars.py
index e70b04e..6ee4ed8 100644
--- a/uberwriter/headerbars.py
+++ b/uberwriter/headerbars.py
@@ -20,12 +20,12 @@ from collections import namedtuple
from gettext import gettext as _
import gi
+
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from uberwriter.helpers import get_builder
from uberwriter.helpers import get_descendant
-from uberwriter.application import Application as app
class MainHeaderbar: #pylint: disable=too-few-public-methods
"""Sets up the main application headerbar
@@ -36,14 +36,14 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
self.hb.props.show_close_button = True
self.hb.get_style_context().add_class("titlebar")
- self.hb_revealer = Gtk.Revealer(name='titlebar_revealer')
+ self.hb_revealer = Gtk.Revealer(name='titlebar-revealer')
self.hb_revealer.add(self.hb)
- self.hb_revealer.props.transition_duration = 1000
+ self.hb_revealer.props.transition_duration = 750
self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
self.hb_revealer.show()
self.hb_revealer.set_reveal_child(True)
- self.hb_container = Gtk.Frame(name='titlebar_container')
+ self.hb_container = Gtk.Frame(name='titlebar-container')
self.hb_container.set_shadow_type(Gtk.ShadowType.NONE)
self.hb_container.add(self.hb_revealer)
self.hb_container.show()
@@ -54,7 +54,7 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
self.hb.show_all()
-class FsHeaderbar:
+class FullscreenHeaderbar:
"""Sets up and manages the fullscreen headerbar and his events
"""
@@ -153,6 +153,7 @@ def buttons(app):
return btn
+
def pack_buttons(headerbar, btn, btn_exit=None):
"""Pack the given buttons in the given headerbar
diff --git a/uberwriter/helpers.py b/uberwriter/helpers.py
index efd0ef5..e7f2c03 100644
--- a/uberwriter/helpers.py
+++ b/uberwriter/helpers.py
@@ -21,12 +21,16 @@ import logging
import os
import shutil
-
import gi
+import pypandoc
+from gi.overrides.Pango import Pango
+
+from uberwriter.settings import Settings
+
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # pylint: disable=E0611
-from uberwriter.uberwriterconfig import get_data_file
+from uberwriter.config import get_data_file
from uberwriter.builder import Builder
@@ -48,13 +52,18 @@ def get_builder(builder_file_name):
return builder
-# Owais Lone : To get quick access to icons and stuff.
+def path_to_file(path):
+ """Return a file path (file:///) for the given path"""
+
+ return "file:///" + path
+
+
def get_media_file(media_file_path):
"""Return the full path of a given filename under the media dir
(starts with file:///)
"""
- return "file:///" + get_media_path(media_file_path)
+ return path_to_file(get_media_path(media_file_path))
def get_media_path(media_file_name):
@@ -160,6 +169,7 @@ def exist_executable(command):
return shutil.which(command) is not None
+
def get_descendant(widget, child_name, level, doPrint=False):
if widget is not None:
if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) +
@@ -188,3 +198,14 @@ def get_descendant(widget, child_name, level, doPrint=False):
if child is not None:
found = get_descendant(child, child_name, level+1, doPrint) # //search the child
if found: return found
+
+
+def get_char_width(widget):
+ return Pango.units_to_double(
+ widget.get_pango_context().get_metrics().get_approximate_char_width())
+
+
+def pandoc_convert(text, to="html5", args=[], outputfile=None):
+ fr = Settings.new().get_value('input-format').get_string() or "markdown"
+ args.extend(["--quiet"])
+ return pypandoc.convert_text(text, to, fr, extra_args=args, outputfile=outputfile)
diff --git a/uberwriter/inline_preview.py b/uberwriter/inline_preview.py
index 6396411..ae5f9d9 100644
--- a/uberwriter/inline_preview.py
+++ b/uberwriter/inline_preview.py
@@ -14,32 +14,29 @@
# with this program. If not, see .
# END LICENSE
-import re
-import urllib
-from urllib.error import URLError
-import webbrowser
-import subprocess
-import tempfile
import logging
-import threading
+import re
+import subprocess
import telnetlib
-
+import tempfile
+import threading
+import urllib
+import webbrowser
from gettext import gettext as _
+from urllib.error import URLError
+from urllib.parse import unquote
import gi
+
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
-from uberwriter import latex_to_PNG
+from uberwriter import latex_to_PNG, text_view_markup_handler
from uberwriter.settings import Settings
from uberwriter.fix_table import FixTable
-from uberwriter.markup_buffer import MarkupBuffer
-
LOGGER = logging.getLogger('uberwriter')
-GObject.threads_init() # Still needed?
-
# TODO:
# - Don't insert a span with id, it breaks the text to often
# Would be better to search for the nearest title and generate
@@ -240,7 +237,7 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
if lexikon_dict:
for entry in lexikon_dict:
vocab_label = Gtk.Label.new(vocab + ' ~ ' + entry['class'])
- vocab_label.get_style_context().add_class('lexikon_heading')
+ vocab_label.get_style_context().add_class('lexikon-heading')
vocab_label.set_halign(Gtk.Align.START)
vocab_label.set_justify(Gtk.Justification.LEFT)
grid.attach(vocab_label, 0, i, 3, 1)
@@ -248,14 +245,14 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
for definition in entry['defs']:
i = i + 1
num_label = Gtk.Label.new(definition['num'])
- num_label.get_style_context().add_class('lexikon_num')
+ num_label.get_style_context().add_class('lexikon-num')
num_label.set_justify(Gtk.Justification.RIGHT)
grid.attach(num_label, 0, i, 1, 1)
def_label = Gtk.Label.new(' '.join(definition['description']))
def_label.set_halign(Gtk.Align.START)
def_label.set_justify(Gtk.Justification.LEFT)
- def_label.get_style_context().add_class('lexikon_definition')
+ def_label.get_style_context().add_class('lexikon-definition')
def_label.props.wrap = True
grid.attach(def_label, 1, i, 1, 1)
i = i + 1
@@ -264,11 +261,11 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
return None
-class InlinePreview():
+class InlinePreview:
- def __init__(self, view, text_buffer):
- self.text_view = view
- self.text_buffer = text_buffer
+ def __init__(self, text_view):
+ self.text_view = text_view
+ self.text_buffer = text_view.get_buffer()
self.latex_converter = latex_to_PNG.LatexToPNG()
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
@@ -307,7 +304,7 @@ class InlinePreview():
# b.show_all()
# a.show_all()
self.popover = Gtk.Popover.new(lbl)
- self.popover.get_style_context().add_class("QuickPreviewPopup")
+ self.popover.get_style_context().add_class("quick-preview-popup")
self.popover.add(alignment)
# a.add(alignment)
_dismiss, rect = self.popover.get_pointing_to()
@@ -363,8 +360,8 @@ class InlinePreview():
text = self.text_buffer.get_text(start_iter, end_iter, False)
- math = MarkupBuffer.regex["MATH"]
- link = MarkupBuffer.regex["LINK"]
+ math = text_view_markup_handler.regex["MATH"]
+ link = text_view_markup_handler.regex["LINK"]
footnote = re.compile(r'\[\^([^\s]+?)\]')
image = re.compile(r"!\[(.*?)\]\((.+?)\)")
diff --git a/uberwriter/markup_buffer.py b/uberwriter/markup_buffer.py
deleted file mode 100644
index c413482..0000000
--- a/uberwriter/markup_buffer.py
+++ /dev/null
@@ -1,321 +0,0 @@
-# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
-### BEGIN LICENSE
-# Copyright (C) 2012, Wolf Vollprecht
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License version 3, as published
-# by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranties of
-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
-# PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program. If not, see .
-### END LICENSE
-
-import re
-import gi
-
-gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk
-from gi.repository import Pango
-
-
-class MarkupBuffer():
-
- def __init__(self, Parent, TextBuffer, base_leftmargin):
- self.multiplier = 10
- self.parent = Parent
- self.text_buffer = TextBuffer
-
- # Styles
- self.italic = self.text_buffer.create_tag("italic",
- style=Pango.Style.ITALIC)
-
- self.emph = self.text_buffer.create_tag("emph",
- weight=Pango.Weight.BOLD,
- style=Pango.Style.NORMAL)
-
- self.bolditalic = self.text_buffer.create_tag("bolditalic",
- weight=Pango.Weight.BOLD,
- style=Pango.Style.ITALIC)
-
- self.headline_two = self.text_buffer.create_tag("headline_two",
- weight=Pango.Weight.BOLD,
- style=Pango.Style.NORMAL)
-
- self.normal_indent = self.text_buffer.create_tag('normal_indent', indent=100)
-
- self.math_text = self.text_buffer.create_tag('math_text')
-
- self.unfocused_text = self.text_buffer.create_tag('graytag',
- foreground="gray")
-
- self.underline = self.text_buffer.create_tag("underline",
- underline=Pango.Underline.SINGLE)
-
- self.underline.set_property('weight', Pango.Weight.BOLD)
-
- self.strikethrough = self.text_buffer.create_tag("strikethrough",
- strikethrough=True)
-
- self.centertext = self.text_buffer.create_tag("centertext",
- justification=Gtk.Justification.CENTER)
-
- self.text_buffer.apply_tag(
- self.normal_indent,
- self.text_buffer.get_start_iter(),
- self.text_buffer.get_end_iter()
- )
-
- self.rev_leftmargin = []
- for i in range(0, 6):
- name = "rev_marg_indent_left" + str(i)
- self.rev_leftmargin.append(self.text_buffer.create_tag(name))
- self.rev_leftmargin[i].set_property("left-margin", 90 - 10 * (i + 1))
- self.rev_leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
- #self.leftmargin[i].set_property("background", "gray")
-
- self.leftmargin = []
-
- for i in range(0, 6):
- name = "marg_indent_left" + str(i)
- self.leftmargin.append(self.text_buffer.create_tag(name))
- self.leftmargin[i].set_property("left-margin", base_leftmargin + 10 + 10 * (i + 1))
- self.leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
-
- self.leftindent = []
-
- for i in range(0, 15):
- name = "indent_left" + str(i)
- self.leftindent.append(self.text_buffer.create_tag(name))
- self.leftindent[i].set_property("indent", - 10 * (i + 1) - 20)
-
- self.table_env = self.text_buffer.create_tag('table_env')
- self.table_env.set_property('wrap-mode', Gtk.WrapMode.NONE)
- # self.table_env.set_property('font', 'Ubuntu Mono 13px')
- self.table_env.set_property('pixels-above-lines', 0)
- self.table_env.set_property('pixels-below-lines', 0)
-
- self.update_style()
-
- regex = {
- "ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_
- "STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__
- "STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"),
- "BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE),
- "STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"),
- "LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE),
- "NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE),
- "INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE),
- "HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE),
- "HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE),
- "HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE),
- "MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"),
- "HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"),
- "TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL),
- "LINK": re.compile(r"\(http(.+?)\)")
- }
-
- def update_style(self):
- (found, color) = self.parent.get_style_context().lookup_color('math_text_color')
- if not found:
- (_, color) = self.parent.get_style_context().lookup_color('foreground_color')
-
- self.math_text.set_property("foreground", color.to_string())
-
- def markup_buffer(self, mode=0):
- buf = self.text_buffer
-
- # Test for shifting first line
- # bbs = buf.get_start_iter()
- # bbb = buf.get_iter_at_offset(3)
-
- # buf.apply_tag(self.ftag, bbs, bbb)
-
- # Modes:
- # 0 -> start to end
- # 1 -> around the cursor
- # 2 -> n.d.
-
- if mode == 0:
- context_start = buf.get_start_iter()
- context_end = buf.get_end_iter()
- context_offset = 0
- elif mode == 1:
- cursor_mark = buf.get_insert()
- context_start = buf.get_iter_at_mark(cursor_mark)
- context_start.backward_lines(3)
- context_end = buf.get_iter_at_mark(cursor_mark)
- context_end.forward_lines(2)
- context_offset = context_start.get_offset()
-
- text = buf.get_slice(context_start, context_end, False)
-
- self.text_buffer.remove_tag(self.italic, context_start, context_end)
-
- matches = re.finditer(self.regex["ITALIC"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.italic, start_iter, end_iter)
-
- self.text_buffer.remove_tag(self.emph, context_start, context_end)
-
- matches = re.finditer(self.regex["STRONG"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
-
- matches = re.finditer(self.regex["STRONGITALIC"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.bolditalic, start_iter, end_iter)
-
- self.text_buffer.remove_tag(self.strikethrough, context_start, context_end)
-
- matches = re.finditer(self.regex["STRIKETHROUGH"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.strikethrough, start_iter, end_iter)
-
- self.text_buffer.remove_tag(self.math_text, context_start, context_end)
-
- matches = re.finditer(self.regex["MATH"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.math_text, start_iter, end_iter)
-
- for margin in self.rev_leftmargin:
- self.text_buffer.remove_tag(margin, context_start, context_end)
-
- matches = re.finditer(self.regex["LIST"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.rev_leftmargin[0], start_iter, end_iter)
-
- matches = re.finditer(self.regex["NUMERICLIST"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- index = len(match.group(1)) - 1
- if index < len(self.rev_leftmargin):
- margin = self.rev_leftmargin[index]
- self.text_buffer.apply_tag(margin, start_iter, end_iter)
-
- matches = re.finditer(self.regex["BLOCKQUOTE"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- index = len(match.group(1)) - 2
- if index < len(self.leftmargin):
- self.text_buffer.apply_tag(self.leftmargin[index], start_iter, end_iter)
-
- for leftindent in self.leftindent:
- self.text_buffer.remove_tag(leftindent, context_start, context_end)
-
- matches = re.finditer(self.regex["INDENTEDLIST"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- index = (len(match.group(1)) - 1) * 2 + len(match.group(2))
- if index < len(self.leftindent):
- self.text_buffer.apply_tag(self.leftindent[index], start_iter, end_iter)
-
- matches = re.finditer(self.regex["HEADINDICATOR"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- index = len(match.group(1)) - 1
- if index < len(self.rev_leftmargin):
- margin = self.rev_leftmargin[index]
- self.text_buffer.apply_tag(margin, start_iter, end_iter)
-
- matches = re.finditer(self.regex["HORIZONTALRULE"], text)
- rulecontext = context_start.copy()
- rulecontext.forward_lines(3)
- self.text_buffer.remove_tag(self.centertext, rulecontext, context_end)
-
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- start_iter.forward_chars(2)
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.centertext, start_iter, end_iter)
-
- matches = re.finditer(self.regex["HEADLINE"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
-
- matches = re.finditer(self.regex["HEADLINE_TWO"], text)
- self.text_buffer.remove_tag(self.headline_two, rulecontext, context_end)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.headline_two, start_iter, end_iter)
-
- matches = re.finditer(self.regex["TABLE"], text)
- for match in matches:
- start_iter = buf.get_iter_at_offset(context_offset + match.start())
- end_iter = buf.get_iter_at_offset(context_offset + match.end())
- self.text_buffer.apply_tag(self.table_env, start_iter, end_iter)
-
- if self.parent.focusmode:
- self.focusmode_highlight()
-
- def focusmode_highlight(self):
- start_document = self.text_buffer.get_start_iter()
- end_document = self.text_buffer.get_end_iter()
-
- self.text_buffer.remove_tag(
- self.unfocused_text,
- start_document,
- end_document)
-
- cursor = self.text_buffer.get_mark("insert")
- cursor_iter = self.text_buffer.get_iter_at_mark(cursor)
-
- end_sentence = cursor_iter.copy()
- end_sentence.forward_sentence_end()
-
- end_line = cursor_iter.copy()
- end_line.forward_to_line_end()
-
- comp = end_line.compare(end_sentence)
- # if comp < 0, end_line is BEFORE end_sentence
- if comp <= 0:
- end_sentence = end_line
-
- start_sentence = cursor_iter.copy()
- start_sentence.backward_sentence_start()
-
- # grey out everything before
- self.text_buffer.apply_tag(
- self.unfocused_text,
- self.text_buffer.get_start_iter(), start_sentence)
-
- self.text_buffer.apply_tag(
- self.unfocused_text,
- end_sentence, self.text_buffer.get_end_iter())
-
- def set_multiplier(self, multiplier):
- self.multiplier = multiplier
-
- def recalculate(self, lm):
- multiplier = self.multiplier
- for i in range(0, 6):
- new_margin = (lm - multiplier) - multiplier * (i + 1)
- self.rev_leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
- self.rev_leftmargin[i].set_property("indent", - multiplier * (i + 1) - multiplier)
-
- for i in range(0, 6):
- new_margin = (lm - multiplier) + multiplier + multiplier * (i + 1)
- self.leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
- self.leftmargin[i].set_property("indent", - (multiplier - 1) * (i + 1) - multiplier)
diff --git a/uberwriter/preferences_dialog.py b/uberwriter/preferences_dialog.py
index 937dcf4..fffda4b 100644
--- a/uberwriter/preferences_dialog.py
+++ b/uberwriter/preferences_dialog.py
@@ -18,23 +18,119 @@
"""this dialog adjusts values in gsettings
"""
+import webbrowser
+
import gi
+
gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk # pylint: disable=E0611
+from gi.repository import Gtk, Pango, GLib # pylint: disable=E0611
import logging
logger = logging.getLogger('uberwriter')
-from uberwriter.helpers import get_builder, show_uri, get_help_uri
+from uberwriter.helpers import get_builder
+
+
+class PreferencesDialog:
-class PreferencesDialog(Gtk.Window):
__gtype_name__ = "PreferencesDialog"
- def __new__(cls):
- """Special static method that's automatically called by Python when
- constructing a new instance of this class.
-
- Returns a fully instantiated PreferencesDialog object.
- """
- builder = get_builder('Preferences')
- new_object = builder.get_object("PreferencesWindow")
- return new_object
+ formats = [
+ {
+ "name": "Pandoc's Markdown",
+ "format": "markdown",
+ "help": "https://pandoc.org/MANUAL.html#pandocs-markdown"
+ },
+ {
+ "name": "CommonMark",
+ "format": "commonmark",
+ "help": "https://commonmark.org"
+ },
+ {
+ "name": "GitHub Flavored Markdown",
+ "format": "gfm",
+ "help": "https://help.github.com/en/categories/writing-on-github"
+ },
+ {
+ "name": "MultiMarkdown",
+ "format": "markdown_mmd",
+ "help": "https://fletcherpenney.net/multimarkdown"
+ },
+ {
+ "name": "Plain Markdown",
+ "format": "markdown_strict",
+ "help": "https://daringfireball.net/projects/markdown"
+ }
+ ]
+
+ def __init__(self, settings):
+ self.settings = settings
+ self.builder = get_builder("Preferences")
+
+ self.dark_mode_auto_switch = self.builder.get_object("dark_mode_auto_switch")
+ self.dark_mode_auto_switch.set_active(self.settings.get_value("dark-mode-auto"))
+ self.dark_mode_auto_switch.connect("state-set", self.on_dark_mode_auto)
+
+ self.dark_mode_switch = self.builder.get_object("dark_mode_switch")
+ self.dark_mode_switch.set_active(self.settings.get_value("dark-mode"))
+ self.dark_mode_switch.connect("state-set", self.on_dark_mode)
+
+ self.spellcheck_switch = self.builder.get_object("spellcheck_switch")
+ self.spellcheck_switch.set_active(self.settings.get_value("spellcheck"))
+ self.spellcheck_switch.connect("state-set", self.on_spellcheck)
+
+ self.gradient_overlay_switch = self.builder.get_object("gradient_overlay_switch")
+ self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay"))
+ self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay)
+
+ input_format_store = Gtk.ListStore(int, str)
+ input_format = self.settings.get_value("input-format").get_string()
+ input_format_active = 0
+ for i, fmt in enumerate(self.formats):
+ input_format_store.append([i, fmt["name"]])
+ if fmt["format"] == input_format:
+ input_format_active = i
+ self.input_format_combobox = self.builder.get_object("input_format_combobox")
+ self.input_format_combobox.set_model(input_format_store)
+ input_format_renderer = Gtk.CellRendererText()
+ self.input_format_combobox.pack_start(input_format_renderer, True)
+ self.input_format_combobox.add_attribute(input_format_renderer, "text", 1)
+ self.input_format_combobox.set_active(input_format_active)
+ self.input_format_combobox.connect("changed", self.on_input_format)
+
+ self.input_format_help_button = self.builder.get_object("input_format_help_button")
+ self.input_format_help_button.connect('clicked', self.on_input_format_help)
+
+ def show(self, window):
+ preferences_window = self.builder.get_object("PreferencesWindow")
+ preferences_window.set_application(window.get_application())
+ preferences_window.set_transient_for(window)
+ preferences_window.show()
+
+ def on_dark_mode_auto(self, _, state):
+ self.settings.set_value("dark-mode-auto", GLib.Variant.new_boolean(state))
+ if state and self.dark_mode_switch.get_active():
+ self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False))
+ return False
+
+ def on_dark_mode(self, _, state):
+ self.settings.set_value("dark-mode", GLib.Variant.new_boolean(state))
+ if state and self.dark_mode_auto_switch.get_active():
+ self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False))
+ return False
+
+ def on_spellcheck(self, _, state):
+ self.settings.set_value("spellcheck", GLib.Variant.new_boolean(state))
+ return False
+
+ def on_gradient_overlay(self, _, state):
+ self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(state))
+ return False
+
+ def on_input_format(self, combobox):
+ fmt = self.formats[combobox.get_active()]
+ self.settings.set_value("input-format", GLib.Variant.new_string(fmt["format"]))
+
+ def on_input_format_help(self, _):
+ fmt = self.formats[self.input_format_combobox.get_active()]
+ webbrowser.open(fmt["help"])
+
diff --git a/uberwriter/scroller.py b/uberwriter/scroller.py
new file mode 100644
index 0000000..518167f
--- /dev/null
+++ b/uberwriter/scroller.py
@@ -0,0 +1,48 @@
+class Scroller:
+ def __init__(self, scrolled_window, source_pos, target_pos):
+ super().__init__()
+
+ self.scrolled_window = scrolled_window
+ self.source_pos = source_pos
+ self.target_pos = target_pos
+ self.duration = max(200, (target_pos - source_pos) / 50) * 1000
+
+ self.is_started = False
+ self.is_setup = False
+ self.start_time = 0
+ self.end_time = 0
+ self.tick_callback_id = 0
+
+ def start(self):
+ self.is_started = True
+ self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick)
+
+ def end(self):
+ self.scrolled_window.remove_tick_callback(self.tick_callback_id)
+ self.is_started = False
+
+ def setup(self, time):
+ self.start_time = time
+ self.end_time = time + self.duration
+ self.is_setup = True
+
+ def on_tick(self, widget, frame_clock):
+ def ease_out_cubic(value):
+ return pow(value - 1, 3) + 1
+
+ now = frame_clock.get_frame_time()
+ if not self.is_setup:
+ self.setup(now)
+
+ if now < self.end_time:
+ time = float(now - self.start_time) / float(self.end_time - self.start_time)
+ else:
+ time = 1
+ self.end()
+
+ time = ease_out_cubic(time)
+ pos = self.source_pos + (time * (self.target_pos - self.source_pos))
+ widget.get_vadjustment().props.value = pos
+ return True
+
+
diff --git a/uberwriter/search_and_replace.py b/uberwriter/search_and_replace.py
index 63c40a0..24fb1d9 100644
--- a/uberwriter/search_and_replace.py
+++ b/uberwriter/search_and_replace.py
@@ -14,22 +14,30 @@
# with this program. If not, see .
### END LICENSE
-import re
import logging
+import re
+
import gi
+
gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk, Gdk
+from gi.repository import Gdk
+
# from plugins import plugins
LOGGER = logging.getLogger('uberwriter')
-class SearchAndReplace():
+
+class SearchAndReplace:
"""
Adds (regex) search and replace functionality to
uberwriter
"""
- def __init__(self, parentwindow):
+
+ def __init__(self, parentwindow, textview):
self.parentwindow = parentwindow
+ self.textview = textview
+ self.textbuffer = textview.get_buffer()
+
self.box = parentwindow.builder.get_object("searchbar_placeholder")
self.box.set_reveal_child(False)
self.searchbar = parentwindow.builder.get_object("searchbar")
@@ -41,9 +49,6 @@ class SearchAndReplace():
self.open_replace_button = parentwindow.builder.get_object("replace")
self.open_replace_button.connect("toggled", self.toggle_replace)
- self.textbuffer = parentwindow.text_buffer
- self.texteditor = parentwindow.text_editor
-
self.nextbutton = parentwindow.builder.get_object("next_result")
self.prevbutton = parentwindow.builder.get_object("previous_result")
self.regexbutton = parentwindow.builder.get_object("regex")
@@ -63,18 +68,17 @@ class SearchAndReplace():
self.prevbutton.connect('clicked', self.scrolltoprev)
self.regexbutton.connect('toggled', self.search)
self.casesensitivebutton.connect('toggled', self.search)
- self.highlight = self.textbuffer.create_tag('search_highlight',
- background="yellow")
+ self.highlight = self.textbuffer.create_tag('search_highlight', background="yellow")
- self.texteditor.connect("focus-in-event", self.focused_texteditor)
+ self.textview.connect("focus-in-event", self.focused_texteditor)
+
+ self.matches = []
+ self.active = 0
def toggle_replace(self, widget, _data=None):
"""toggle the replace box
"""
- if widget.get_active():
- self.replacebox.set_reveal_child(True)
- else:
- self.replacebox.set_reveal_child(False)
+ self.replacebox.set_reveal_child(widget.get_active())
# TODO: refactorize!
def key_pressed(self, _widget, event, _data=None):
@@ -100,13 +104,11 @@ class SearchAndReplace():
self.hide()
self.open_replace_button.set_active(False)
-
def search(self, _widget=None, _data=None, scroll=True):
searchtext = self.searchentry.get_text()
- buf = self.textbuffer
- context_start = buf.get_start_iter()
- context_end = buf.get_end_iter()
- text = buf.get_slice(context_start, context_end, False)
+ context_start = self.textbuffer.get_start_iter()
+ context_end = self.textbuffer.get_end_iter()
+ text = self.textbuffer.get_slice(context_start, context_end, False)
self.textbuffer.remove_tag(self.highlight, context_start, context_end)
@@ -121,12 +123,12 @@ class SearchAndReplace():
matches = re.finditer(searchtext, text, flags)
- self.matchiters = []
+ self.matches = []
self.active = 0
for match in matches:
- start_iter = buf.get_iter_at_offset(match.start())
- end_iter = buf.get_iter_at_offset(match.end())
- self.matchiters.append((start_iter, end_iter))
+ self.matches.append((match.start(), match.end()))
+ start_iter = self.textbuffer.get_iter_at_offset(match.start())
+ end_iter = self.textbuffer.get_iter_at_offset(match.end())
self.textbuffer.apply_tag(self.highlight, start_iter, end_iter)
if scroll:
self.scrollto(self.active)
@@ -139,43 +141,40 @@ class SearchAndReplace():
self.scrollto(self.active - 1)
def scrollto(self, index):
- if not self.matchiters:
+ if not self.matches:
return
- if index < len(self.matchiters):
- self.active = index
- else:
- self.active = 0
+ self.active = index % len(self.matches)
- matchiter = self.matchiters[self.active]
- self.texteditor.get_buffer().select_range(matchiter[0], matchiter[1])
-
- # self.texteditor.scroll_to_iter(matchiter[0], 0.0, True, 0.0, 0.5)
+ match = self.matches[self.active]
+ start_iter = self.textbuffer.get_iter_at_offset(match[0])
+ end_iter = self.textbuffer.get_iter_at_offset(match[1])
+ self.textbuffer.select_range(start_iter, end_iter)
def hide(self):
- self.replacebox.set_reveal_child(False)
self.box.set_reveal_child(False)
self.textbuffer.remove_tag(self.highlight,
self.textbuffer.get_start_iter(),
self.textbuffer.get_end_iter())
- self.texteditor.grab_focus()
-
+ self.textview.grab_focus()
def replace_clicked(self, _widget, _data=None):
self.replace(self.active)
def replace_all(self, _widget=None, _data=None):
- while self.matchiters:
- match = self.matchiters[0]
- self.textbuffer.delete(match[0], match[1])
- self.textbuffer.insert(match[0], self.replaceentry.get_text())
- self.search(scroll=False)
+ for match in reversed(self.matches):
+ self.do_replace(match)
+ self.search(scroll=False)
def replace(self, searchindex, _inloop=False):
- match = self.matchiters[searchindex]
- self.textbuffer.delete(match[0], match[1])
- self.textbuffer.insert(match[0], self.replaceentry.get_text())
+ self.do_replace(self.matches[searchindex])
active = self.active
self.search(scroll=False)
self.active = active
- self.parentwindow.MarkupBuffer.markup_buffer()
self.scrollto(self.active)
+
+ def do_replace(self, match):
+ start_iter = self.textbuffer.get_iter_at_offset(match[0])
+ end_iter = self.textbuffer.get_iter_at_offset(match[1])
+ self.textbuffer.delete(start_iter, end_iter)
+ start_iter = self.textbuffer.get_iter_at_offset(match[0])
+ self.textbuffer.insert(start_iter, self.replaceentry.get_text())
diff --git a/uberwriter/settings.py b/uberwriter/settings.py
index 58c44b0..08bfe2b 100644
--- a/uberwriter/settings.py
+++ b/uberwriter/settings.py
@@ -27,7 +27,8 @@ class Settings(Gio.Settings):
"""
Gio.Settings.__init__(self)
- def new():
+ @classmethod
+ def new(cls):
"""
Return a new Settings object
"""
diff --git a/uberwriter/sidebar.py b/uberwriter/sidebar.py
index ddc8536..16b7104 100644
--- a/uberwriter/sidebar.py
+++ b/uberwriter/sidebar.py
@@ -58,7 +58,7 @@ class Sidebar():
Presentational class for shelves and files managed by the "sidebar"
parentwindow:
- Reference to UberwriterWindow
+ Reference to Window
"""
def __init__(self, parentwindow):
"""
diff --git a/uberwriter/text_editor.py b/uberwriter/text_editor.py
deleted file mode 100644
index a94a1b2..0000000
--- a/uberwriter/text_editor.py
+++ /dev/null
@@ -1,487 +0,0 @@
-### BEGIN LICENSE
-# Copyright (C) 2012, Wolf Vollprecht
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License version 3, as published
-# by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranties of
-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
-# PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program. If not, see .
-### END LICENSE
-"""Module for the TextView widgth wich encapsulates management of TextBuffer
-and TextIter for common functionality, such as cut, copy, paste, undo, redo,
-and highlighting of text.
-
-Using
-#create the TextEditor and set the text
-editor = TextEditor()
-editor.text = "Text to add to the editor"
-
-#use cut, works the same for copy, paste, undo, and redo
-def __handle_on_cut(self, widget, data=None):
- self.editor.cut()
-
-#add string to highlight
-self.editor.add_highlight("Ubuntu")
-self.editor.add_highlight("Quickly")
-
-#remove highlights
-self.editor.clear_highlight("Ubuntu")
-self.editor.clear_all_highlight()
-
-Configuring
-#Configure as a TextView
-self.editor.set_wrap_mode(Gtk.WRAP_CHAR)
-
-#Access the Gtk.TextBuffer if needed
-buffer = self.editor.get_buffer()
-
-Extending
-A TextEditor is Gtk.TextView
-
-"""
-
-import gi
-gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk, Gdk, GObject
-from uberwriter.format_shortcuts import FormatShortcuts
-
-import logging
-LOGGER = logging.getLogger('uberwriter')
-
-
-class UndoableInsert:
- """something that has been inserted into our textbuffer"""
- def __init__(self, text_iter, text, length):
- self.offset = text_iter.get_offset()
- self.text = text
- self.length = length
- if self.length > 1 or self.text in ("\r", "\n", " "):
- self.mergeable = False
- else:
- self.mergeable = True
-
-
-class UndoableDelete:
- """something that has ben deleted from our textbuffer"""
- def __init__(self, text_buffer, start_iter, end_iter):
- self.text = text_buffer.get_text(start_iter, end_iter, False)
- self.start = start_iter.get_offset()
- self.end = end_iter.get_offset()
- # need to find out if backspace or delete key has been used
- # so we don't mess up during redo
- insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
-
- self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
- self.mergeable = not bool(self.end - self.start > 1
- or self.text in ("\r", "\n", " "))
-
-
-class TextEditor(Gtk.TextView):
- """TextEditor encapsulates management of TextBuffer and TextIter for
- common functionality, such as cut, copy, paste, undo, redo, and
- highlighting of text.
- """
-
- __gsignals__ = {
- 'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
- 'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
- 'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
- 'insert-ulistitem': (GObject.SignalFlags.ACTION, None, ()),
- 'insert-heading': (GObject.SignalFlags.ACTION, None, ()),
- 'insert-strikeout': (GObject.SignalFlags.ACTION, None, ()),
- 'undo': (GObject.SignalFlags.ACTION, None, ()),
- 'redo': (GObject.SignalFlags.ACTION, None, ())
- }
-
- def scroll_to_iter(self, iterable, *args):
- self.get_buffer().place_cursor(iterable)
-
- def __init__(self):
- """
- Create a TextEditor
- """
-
- Gtk.TextView.__init__(self)
- self.undo_max = None
-
- self.insert_event = self.get_buffer().connect("insert-text",
- self.on_insert_text)
- self.delete_event = self.get_buffer().connect("delete-range",
- self.on_delete_range)
- display = self.get_display()
- self.clipboard = Gtk.Clipboard.get_for_display(display,
- Gdk.SELECTION_CLIPBOARD)
-
- self.undo_stack = []
- self.redo_stack = []
- self.not_undoable_action = False
- self.undo_in_progress = False
-
- self.can_delete = True
- self.connect('key-press-event', self.on_key_press_event)
-
- self.format_shortcuts = FormatShortcuts(self.get_buffer(), self)
-
- self.connect('insert-italic', self.set_italic)
- self.connect('insert-bold', self.set_bold)
- self.connect('insert-strikeout', self.set_strikeout)
- self.connect('insert-hrule', self.insert_horizontal_rule)
- self.connect('insert-ulistitem', self.insert_unordered_list_item)
- self.connect('insert-heading', self.insert_heading)
- self.connect('redo', self.redo)
- self.connect('undo', self.undo)
-
- self.get_style_context().add_class("uberwriter-editor")
-
- @property
- def text(self):
- """
- text - a string specifying all the text currently
- in the TextEditor's buffer.
-
- This property is read/write.
- """
- start_iter = self.get_buffer().get_iter_at_offset(0)
- end_iter = self.get_buffer().get_iter_at_offset(-1)
- return self.get_buffer().get_text(start_iter, end_iter, False)
-
- @property
- def can_undo(self):
- return bool(self.undo_stack)
-
- @property
- def can_redo(self):
- return bool(self.redo_stack)
-
- @text.setter
- def text(self, text):
- self.get_buffer().set_text(text)
-
- def append(self, text):
- """append: appends text to the end of the textbuffer.
-
- arguments:
- text - a string to add to the buffer. The text will be the
- last text in the buffer. The insertion cursor will not be moved.
-
- """
-
- end_iter = self.get_buffer().get_iter_at_offset(-1)
- self.get_buffer().insert(end_iter, text)
-
- def prepend(self, text):
- """prepend: appends text to the start of the textbuffer.
-
- arguments:
- text - a string to add to the buffer. The text will be the
- first text in the buffer. The insertion cursor will not be moved.
-
- """
-
- start_iter = self.get_buffer().get_iter_at_offset(0)
- self.get_buffer().insert(start_iter, text)
- insert_iter = self.get_buffer().get_iter_at_offset(len(text)-1)
- self.get_buffer().place_cursor(insert_iter)
-
- def cursor_to_end(self):
- """cursor_to_end: moves the insertion curson to the last position
- in the buffer.
-
- """
-
- end_iter = self.get_buffer().get_iter_at_offset(-1)
- self.get_buffer().place_cursor(end_iter)
-
- def cursor_to_start(self):
- """cursor_to_start: moves the insertion curson to the first position
- in the buffer.
-
- """
-
- start_iter = self.get_buffer().get_iter_at_offset(0)
- self.get_buffer().place_cursor(start_iter)
-
- def cut(self, _widget=None, _data=None):
- """cut: cut currently selected text and put it on the clipboard.
- This function can be called as a function, or assigned as a signal
- handler.
-
- """
-
- self.get_buffer().cut_clipboard(self.clipboard, True)
-
- def copy(self, _widget=None, _data=None):
- """copy: copy currently selected text to the clipboard.
- This function can be called as a function, or assigned as a signal
- handler.
- """
- self.get_buffer().copy_clipboard(self.clipboard)
-
- def paste(self, _widget=None, _data=None):
- """paste: Insert any text currently on the clipboard into the
- buffer.
- This function can be called as a function, or assigned as a signal
- handler.
-
- """
-
- self.get_buffer().paste_clipboard(self.clipboard, None, True)
-
- def undo(self, _widget=None, _data=None):
- """undo inserts or deletions
- undone actions are being moved to redo stack"""
- if not self.undo_stack:
- return
- self.begin_not_undoable_action()
- self.undo_in_progress = True
- undo_action = self.undo_stack.pop()
- self.redo_stack.append(undo_action)
- buf = self.get_buffer()
- if isinstance(undo_action, UndoableInsert):
- offset = undo_action.offset
- start = buf.get_iter_at_offset(offset)
- stop = buf.get_iter_at_offset(
- offset + undo_action.length
- )
- buf.place_cursor(start)
- buf.delete(start, stop)
- else:
- start = buf.get_iter_at_offset(undo_action.start)
- buf.insert(start, undo_action.text)
- if undo_action.delete_key_used:
- buf.place_cursor(start)
- else:
- stop = buf.get_iter_at_offset(undo_action.end)
- buf.place_cursor(stop)
- self.end_not_undoable_action()
- self.undo_in_progress = False
-
- def redo(self, _widget=None, _data=None):
- """redo inserts or deletions
-
- redone actions are moved to undo stack"""
- if not self.redo_stack:
- return
- self.begin_not_undoable_action()
- self.undo_in_progress = True
- redo_action = self.redo_stack.pop()
- self.undo_stack.append(redo_action)
- buf = self.get_buffer()
- if isinstance(redo_action, UndoableInsert):
- start = buf.get_iter_at_offset(redo_action.offset)
- buf.insert(start, redo_action.text)
- new_cursor_pos = buf.get_iter_at_offset(
- redo_action.offset + redo_action.length
- )
- buf.place_cursor(new_cursor_pos)
- else:
- start = buf.get_iter_at_offset(redo_action.start)
- stop = buf.get_iter_at_offset(redo_action.end)
- buf.delete(start, stop)
- buf.place_cursor(start)
- self.end_not_undoable_action()
- self.undo_in_progress = False
-
- def on_insert_text(self, _textbuffer, text_iter, text, _length):
- """
- _on_insert: internal function to handle programatically inserted
- text. Do not call directly.
- """
- def can_be_merged(prev, cur):
- """see if we can merge multiple inserts here
-
- will try to merge words or whitespace
- can't merge if prev and cur are not mergeable in the first place
- can't merge when user set the input bar somewhere else
- can't merge across word boundaries"""
- whitespace = (' ', '\t')
- if not cur.mergeable or not prev.mergeable:
- return False
- if cur.offset != (prev.offset + prev.length):
- return False
- if cur.text in whitespace and not prev.text in whitespace:
- return False
- if prev.text in whitespace and not cur.text in whitespace:
- return False
- return True
-
- if not self.undo_in_progress:
- self.redo_stack = []
- if self.not_undoable_action:
- return
-
- undo_action = UndoableInsert(text_iter, text, len(text))
- try:
- prev_insert = self.undo_stack.pop()
- except IndexError:
- self.undo_stack.append(undo_action)
- return
- if not isinstance(prev_insert, UndoableInsert):
- self.undo_stack.append(prev_insert)
- self.undo_stack.append(undo_action)
- return
- if can_be_merged(prev_insert, undo_action):
- prev_insert.length += undo_action.length
- prev_insert.text += undo_action.text
- self.undo_stack.append(prev_insert)
- else:
- self.undo_stack.append(prev_insert)
- self.undo_stack.append(undo_action)
-
- def on_delete_range(self, text_buffer, start_iter, end_iter):
- """On delete
- """
- def can_be_merged(prev, cur):
- """see if we can merge multiple deletions here
-
- will try to merge words or whitespace
- can't merge if prev and cur are not mergeable in the first place
- can't merge if delete and backspace key were both used
- can't merge across word boundaries"""
-
- whitespace = (' ', '\t')
- if not cur.mergeable or not prev.mergeable:
- return False
- if prev.delete_key_used != cur.delete_key_used:
- return False
- if prev.start != cur.start and prev.start != cur.end:
- return False
- if cur.text not in whitespace and \
- prev.text in whitespace:
- return False
- if cur.text in whitespace and \
- prev.text not in whitespace:
- return False
- return True
-
- if not self.undo_in_progress:
- self.redo_stack = []
- if self.not_undoable_action:
- return
- undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
- try:
- prev_delete = self.undo_stack.pop()
- except IndexError:
- self.undo_stack.append(undo_action)
- return
- if not isinstance(prev_delete, UndoableDelete):
- self.undo_stack.append(prev_delete)
- self.undo_stack.append(undo_action)
- return
- if can_be_merged(prev_delete, undo_action):
- if prev_delete.start == undo_action.start: # delete key used
- prev_delete.text += undo_action.text
- prev_delete.end += (undo_action.end - undo_action.start)
- else: # Backspace used
- prev_delete.text = "%s%s" % (undo_action.text,
- prev_delete.text)
- prev_delete.start = undo_action.start
- self.undo_stack.append(prev_delete)
- else:
- self.undo_stack.append(prev_delete)
- self.undo_stack.append(undo_action)
-
- def begin_not_undoable_action(self):
- """don't record the next actions
- toggles self.not_undoable_action"""
- self.not_undoable_action = True
-
- def end_not_undoable_action(self):
- """record next actions
- toggles self.not_undoable_action"""
- self.not_undoable_action = False
-
- def on_key_press_event(self, widget, event):
- if widget == self and not self.can_delete:
- return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
- else:
- return False
-
- def set_italic(self, _widget, _data=None):
- """Ctrl + I"""
- self.format_shortcuts.italic()
-
- def set_bold(self, _widget, _data=None):
- """Ctrl + Shift + D"""
- self.format_shortcuts.bold()
-
- def set_strikeout(self, _widget, _data=None):
- """Ctrl + B"""
- self.format_shortcuts.strikeout()
-
- def insert_horizontal_rule(self, _widget, _data=None):
- """Ctrl + R"""
- self.format_shortcuts.rule()
-
- def insert_unordered_list_item(self, _widget, _data=None):
- """Ctrl + U"""
- self.format_shortcuts.unordered_list_item()
-
- def insert_ordered_list(self, _widget, _data=None):
- """CTRL + O"""
- self.format_shortcuts.ordered_list_item()
-
- def insert_heading(self, _widget, _data=None):
- """CTRL + H"""
- self.format_shortcuts.heading()
-
-
-class TestWindow(Gtk.Window):
- """For testing and demonstrating AsycnTaskProgressBox.
-
- """
- def __init__(self):
- # create a window a VBox to hold the controls
- Gtk.Window.__init__(self)
- self.set_title("TextEditor Test Window")
- windowbox = Gtk.VBox(homogeneous=False, spacing=2)
- windowbox.show()
- self.add(windowbox)
- self.editor = TextEditor()
- self.editor.show()
- windowbox.pack_end(self.editor, True, True, 0)
- self.set_size_request(200, 200)
- self.show()
- self.maximize()
-
- self.connect("destroy", Gtk.main_quit)
- self.editor.text = "this is some inserted text"
- self.editor.append("\nLine 3")
- self.editor.prepend("Line1\n")
- self.editor.cursor_to_end()
- self.editor.cursor_to_start()
- self.editor.undo_max = 100
- cut_button = Gtk.Button(label="Cut")
- cut_button.connect("clicked", self.editor.cut)
- cut_button.show()
- windowbox.pack_start(cut_button, False, False, 0)
-
- copy_button = Gtk.Button(label="Copy")
- copy_button.connect("clicked", self.editor.copy)
- copy_button.show()
- windowbox.pack_start(copy_button, False, False, 0)
-
- paste_button = Gtk.Button(label="Paste")
- paste_button.connect("clicked", self.editor.paste)
- paste_button.show()
- windowbox.pack_start(paste_button, False, False, 0)
-
- undo_button = Gtk.Button(label="Undo")
- undo_button.connect("clicked", self.editor.undo)
- undo_button.show()
- windowbox.pack_start(undo_button, False, False, 0)
-
- redo_button = Gtk.Button(label="Redo")
- redo_button.connect("clicked", self.editor.redo)
- redo_button.show()
- windowbox.pack_start(redo_button, False, False, 0)
-
-
-if __name__ == "__main__":
- TEST = TestWindow()
- Gtk.main()
diff --git a/uberwriter/text_view.py b/uberwriter/text_view.py
new file mode 100644
index 0000000..df945df
--- /dev/null
+++ b/uberwriter/text_view.py
@@ -0,0 +1,201 @@
+import gi
+
+from uberwriter.inline_preview import InlinePreview
+from uberwriter.text_view_format_inserter import FormatInserter
+from uberwriter.text_view_markup_handler import MarkupHandler
+from uberwriter.text_view_undo_redo_handler import UndoRedoHandler
+from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
+from uberwriter.scroller import Scroller
+
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk, Gdk, GObject, GLib
+
+import logging
+LOGGER = logging.getLogger('uberwriter')
+
+
+class TextView(Gtk.TextView):
+ """UberwriterTextView encapsulates all the features around the editor.
+
+ It combines the following:
+ - Undo / redo (via TextBufferUndoRedoHandler)
+ - Format shortcuts (via TextBufferShortcutInserter)
+ - Markup (via TextBufferMarkupHandler)
+ - Preview popover (via TextBufferMarkupHandler)
+ - Drag and drop (via TextViewDragDropHandler)
+ - Scrolling (via TextViewScroller)
+ - The various modes supported by UberWriter (eg. Focus Mode, Hemingway Mode)
+ """
+
+ __gsignals__ = {
+ 'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
+ 'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
+ 'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
+ 'insert-listitem': (GObject.SignalFlags.ACTION, None, ()),
+ 'insert-header': (GObject.SignalFlags.ACTION, None, ()),
+ 'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
+ 'undo': (GObject.SignalFlags.ACTION, None, ()),
+ 'redo': (GObject.SignalFlags.ACTION, None, ())
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ # Appearance
+ self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
+ self.set_pixels_above_lines(4)
+ self.set_pixels_below_lines(4)
+ self.set_pixels_inside_wrap(8)
+ self.get_style_context().add_class('uberwriter-editor')
+
+ # General behavior
+ self.get_buffer().connect('changed', self.on_text_changed)
+ self.connect('size-allocate', self.on_size_allocate)
+
+ # Undo / redo
+ self.undo_redo = UndoRedoHandler()
+ self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text)
+ self.get_buffer().connect('delete-range', self.undo_redo.on_delete_range)
+ self.connect('undo', self.undo_redo.undo)
+ self.connect('redo', self.undo_redo.redo)
+
+ # Format shortcuts
+ self.shortcut = FormatInserter()
+ self.connect('insert-italic', self.shortcut.insert_italic)
+ self.connect('insert-bold', self.shortcut.insert_bold)
+ self.connect('insert-strikethrough', self.shortcut.insert_strikethrough)
+ self.connect('insert-hrule', self.shortcut.insert_horizontal_rule)
+ self.connect('insert-listitem', self.shortcut.insert_list_item)
+ self.connect('insert-header', self.shortcut.insert_header)
+
+ # Markup
+ self.markup = MarkupHandler(self)
+ self.connect('style-updated', self.markup.on_style_updated)
+
+ # Preview popover
+ self.preview_popover = InlinePreview(self)
+
+ # Drag and drop
+ self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT)
+
+ # Scrolling
+ self.scroller = None
+ self.get_buffer().connect('mark-set', self.on_mark_set)
+
+ # Focus mode
+ self.focus_mode = False
+ self.original_top_margin = self.props.top_margin
+ self.original_bottom_margin = self.props.bottom_margin
+ self.connect('button-release-event', self.on_button_release_event)
+
+ # Hemingway mode
+ self.hemingway_mode = False
+ self.connect('key-press-event', self.on_key_press_event)
+
+ def get_text(self):
+ text_buffer = self.get_buffer()
+ start_iter = text_buffer.get_start_iter()
+ end_iter = text_buffer.get_end_iter()
+ return text_buffer.get_text(start_iter, end_iter, False)
+
+ def set_text(self, text):
+ text_buffer = self.get_buffer()
+ text_buffer.set_text(text)
+
+ def on_text_changed(self, *_):
+ self.markup.apply()
+ GLib.idle_add(self.scroll_to)
+
+ def on_size_allocate(self, *_):
+ self.update_vertical_margin()
+ self.markup.update_margins_indents()
+
+ def set_focus_mode(self, focus_mode):
+ """Toggle focus mode.
+
+ When in focus mode, the cursor sits in the middle of the text view,
+ and the surrounding text is greyed out."""
+
+ self.focus_mode = focus_mode
+ self.update_vertical_margin()
+ self.markup.apply()
+ self.scroll_to()
+
+ def update_vertical_margin(self):
+ if self.focus_mode:
+ height = self.get_allocation().height
+ self.props.top_margin = height / 2
+ self.props.bottom_margin = height / 2
+ else:
+ self.props.top_margin = self.original_top_margin
+ self.props.bottom_margin = self.original_bottom_margin
+
+ def on_button_release_event(self, _widget, _event):
+ if self.focus_mode:
+ self.markup.apply()
+ return False
+
+ def set_hemingway_mode(self, hemingway_mode):
+ """Toggle hemingway mode.
+
+ When in hemingway mode, the backspace and delete keys are ignored."""
+
+ self.hemingway_mode = hemingway_mode
+
+ def on_key_press_event(self, _widget, event):
+ if self.hemingway_mode:
+ return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
+ else:
+ return False
+
+ def clear(self):
+ """Clear text and undo history"""
+
+ self.get_buffer().set_text('')
+ self.undo_redo.clear()
+
+ def scroll_to(self, mark=None):
+ """Scrolls if needed to ensure mark is visible.
+
+ If mark is unspecified, the cursor is used."""
+
+ margin = 80
+ scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__)
+ if not scrolled_window:
+ return
+ va = scrolled_window.get_vadjustment()
+ if va.props.page_size < margin * 2:
+ return
+
+ text_buffer = self.get_buffer()
+ if mark:
+ mark_iter = text_buffer.get_iter_at_mark(mark)
+ else:
+ mark_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
+ mark_rect = self.get_iter_location(mark_iter)
+
+ pos_y = mark_rect.y + mark_rect.height + self.props.top_margin
+ pos = pos_y - va.props.value
+ target_pos = None
+ if self.focus_mode:
+ if pos != (va.props.page_size * 0.5):
+ target_pos = pos_y - (va.props.page_size * 0.5)
+ elif pos > va.props.page_size - margin:
+ target_pos = pos_y - va.props.page_size + margin
+ elif pos < margin:
+ target_pos = pos_y - margin
+
+ if self.scroller and self.scroller.is_started:
+ self.scroller.end()
+ if target_pos:
+ self.scroller = Scroller(scrolled_window, va.props.value, target_pos)
+ self.scroller.start()
+
+ def on_mark_set(self, _text_buffer, _location, mark, _data=None):
+ if mark.get_name() == 'insert':
+ self.markup.apply()
+ if self.focus_mode:
+ self.scroll_to(mark)
+ elif mark.get_name() == 'gtk_drag_target':
+ self.scroll_to(mark)
+ return True
diff --git a/uberwriter/text_view_drag_drop_handler.py b/uberwriter/text_view_drag_drop_handler.py
new file mode 100644
index 0000000..1b6c593
--- /dev/null
+++ b/uberwriter/text_view_drag_drop_handler.py
@@ -0,0 +1,62 @@
+import mimetypes
+import urllib
+
+from gi.repository import Gtk
+
+(TARGET_URI, TARGET_TEXT) = range(2)
+
+
+class DragDropHandler:
+ TARGET_URI = None
+
+ def __init__(self, text_view, *targets):
+ super().__init__()
+
+ self.target_list = Gtk.TargetList.new([])
+ if TARGET_URI in targets:
+ self.target_list.add_uri_targets(TARGET_URI)
+ if TARGET_TEXT in targets:
+ self.target_list.add_text_targets(TARGET_TEXT)
+
+ text_view.drag_dest_set_target_list(self.target_list)
+ text_view.connect_after('drag-data-received', self.on_drag_data_received)
+
+ def on_drag_data_received(self, text_view, drag_context, _x, _y, data, info, time):
+ """Handle drag and drop events"""
+
+ text_buffer = text_view.get_buffer()
+ if info == TARGET_URI:
+ uris = data.get_uris()
+ for uri in uris:
+ uri = urllib.parse.unquote_plus(uri)
+ mime = mimetypes.guess_type(uri)
+
+ if mime[0] is not None and mime[0].startswith('image'):
+ if uri.startswith("file://"):
+ uri = uri[7:]
+ text = "![Image caption](%s)" % uri
+ limit_left = 2
+ limit_right = 23
+ else:
+ text = "[Link description](%s)" % uri
+ limit_left = 1
+ limit_right = 22
+ text_buffer.place_cursor(text_buffer.get_iter_at_mark(
+ text_buffer.get_mark('gtk_drag_target')))
+ text_buffer.insert_at_cursor(text)
+ insert_mark = text_buffer.get_insert()
+ selection_bound = text_buffer.get_selection_bound()
+ cursor_iter = text_buffer.get_iter_at_mark(insert_mark)
+ cursor_iter.backward_chars(len(text) - limit_left)
+ text_buffer.move_mark(insert_mark, cursor_iter)
+ cursor_iter.forward_chars(limit_right)
+ text_buffer.move_mark(selection_bound, cursor_iter)
+
+ elif info == TARGET_TEXT:
+ text_buffer.place_cursor(text_buffer.get_iter_at_mark(
+ text_buffer.get_mark('gtk_drag_target')))
+ text_buffer.insert_at_cursor(data.get_text())
+
+ Gtk.drag_finish(drag_context, True, True, time)
+ text_view.get_window().present_with_time(time)
+ return False
diff --git a/uberwriter/text_view_format_inserter.py b/uberwriter/text_view_format_inserter.py
new file mode 100644
index 0000000..5cbaf61
--- /dev/null
+++ b/uberwriter/text_view_format_inserter.py
@@ -0,0 +1,154 @@
+from gettext import gettext as _
+
+
+class FormatInserter:
+ """Manages insertion of formatting.
+
+ Methods can be called directly, as well as be used as signal callbacks."""
+
+ def insert_italic(self, text_view, _data=None):
+ """Insert italic or mark a selection as bold"""
+
+ self.__wrap(text_view, "_", _("italic text"))
+
+ def insert_bold(self, text_view, _data=None):
+ """Insert bold or mark a selection as bold"""
+
+ self.__wrap(text_view, "**", _("bold text"))
+
+ def insert_strikethrough(self, text_view, _data=None):
+ """Insert strikethrough or mark a selection as strikethrough"""
+
+ self.__wrap(text_view, "~~", _("strikethrough text"))
+
+ def insert_horizontal_rule(self, text_view, _data=None):
+ """Insert horizontal rule"""
+
+ text_buffer = text_view.get_buffer()
+ text_buffer.insert_at_cursor("\n\n---\n")
+ text_view.scroll_mark_onscreen(text_buffer.get_insert())
+
+ def insert_list_item(self, text_view, _data=None):
+ """Insert list item or mark a selection as list item"""
+
+ text_buffer = text_view.get_buffer()
+ if text_buffer.get_has_selection():
+ (start, end) = text_buffer.get_selection_bounds()
+ if start.starts_line():
+ text = text_buffer.get_text(start, end, False)
+ if text.startswith(("- ", "* ", "+ ")):
+ delete_end = start.forward_chars(2)
+ text_buffer.delete(start, delete_end)
+ else:
+ text_buffer.insert(start, "- ")
+ else:
+ helptext = _("Item")
+ text_length = len(helptext)
+
+ cursor_mark = text_buffer.get_insert()
+ cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
+
+ start_ext = cursor_iter.copy()
+ start_ext.backward_lines(3)
+ text = text_buffer.get_text(cursor_iter, start_ext, False)
+ lines = text.splitlines()
+
+ for line in reversed(lines):
+ if line and line.startswith(("- ", "* ", "+ ")):
+ if cursor_iter.starts_line():
+ text_buffer.insert_at_cursor(line[:2] + helptext)
+ else:
+ text_buffer.insert_at_cursor(
+ "\n" + line[:2] + helptext)
+ break
+ else:
+ if not lines[-1] and not lines[-2]:
+ text_buffer.insert_at_cursor("- " + helptext)
+ elif not lines[-1]:
+ if cursor_iter.starts_line():
+ text_buffer.insert_at_cursor("- " + helptext)
+ else:
+ text_buffer.insert_at_cursor("\n- " + helptext)
+ else:
+ text_buffer.insert_at_cursor("\n\n- " + helptext)
+ break
+
+ self.__select_text(text_view, 0, text_length)
+
+ def insert_ordered_list_item(self, _text_view, _data=None):
+ # TODO: implement ordered lists
+ pass
+
+ def insert_header(self, text_view, _data=None):
+ """Insert header or mark a selection as a list header"""
+
+ text_buffer = text_view.get_buffer()
+ if text_buffer.get_has_selection():
+ (start, end) = text_buffer.get_selection_bounds()
+ text = text_buffer.get_text(start, end, False)
+ text_buffer.delete(start, end)
+ else:
+ text = _("Header")
+
+ text_buffer.insert_at_cursor("#" + " " + text)
+ self.__select_text(text_view, 0, len(text))
+
+ @staticmethod
+ def __wrap(text_view, wrap, helptext=""):
+ """Inserts wrap format to the selected text (helper text when nothing selected)"""
+ text_buffer = text_view.get_buffer()
+ if text_buffer.get_has_selection():
+ # Find current highlighting
+ (start, end) = text_buffer.get_selection_bounds()
+ moved = False
+ if (start.get_offset() >= len(wrap) and
+ end.get_offset() <= text_buffer.get_char_count() - len(wrap)):
+ moved = True
+ ext_start = start.copy()
+ ext_start.backward_chars(len(wrap))
+ ext_end = end.copy()
+ ext_end.forward_chars(len(wrap))
+ text = text_buffer.get_text(ext_start, ext_end, True)
+ else:
+ text = text_buffer.get_text(start, end, True)
+
+ if moved and text.startswith(wrap) and text.endswith(wrap):
+ text = text[len(wrap):-len(wrap)]
+ new_text = text
+ text_buffer.delete(ext_start, ext_end)
+ move_back = 0
+ else:
+ if moved:
+ text = text[len(wrap):-len(wrap)]
+ new_text = text.lstrip().rstrip()
+ text = text.replace(new_text, wrap + new_text + wrap)
+
+ text_buffer.delete(start, end)
+ move_back = len(wrap)
+
+ text_buffer.insert_at_cursor(text)
+ text_length = len(new_text)
+
+ else:
+ text_buffer.insert_at_cursor(wrap + helptext + wrap)
+ text_length = len(helptext)
+ move_back = len(wrap)
+
+ cursor_mark = text_buffer.get_insert()
+ cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
+ cursor_iter.backward_chars(move_back)
+ text_buffer.move_mark_by_name('selection_bound', cursor_iter)
+ cursor_iter.backward_chars(text_length)
+ text_buffer.move_mark_by_name('insert', cursor_iter)
+
+ @staticmethod
+ def __select_text(text_view, offset, length):
+ """Selects text starting at the current cursor minus offset, length characters."""
+
+ text_buffer = text_view.get_buffer()
+ cursor_mark = text_buffer.get_insert()
+ cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
+ cursor_iter.backward_chars(offset)
+ text_buffer.move_mark_by_name('selection_bound', cursor_iter)
+ cursor_iter.backward_chars(length)
+ text_buffer.move_mark_by_name('insert', cursor_iter)
diff --git a/uberwriter/text_view_markup_handler.py b/uberwriter/text_view_markup_handler.py
new file mode 100644
index 0000000..029871a
--- /dev/null
+++ b/uberwriter/text_view_markup_handler.py
@@ -0,0 +1,307 @@
+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
+### BEGIN LICENSE
+# Copyright (C) 2012, Wolf Vollprecht
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see .
+### END LICENSE
+
+import re
+
+import gi
+from gi.overrides import GLib
+
+from uberwriter import helpers
+
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+from gi.repository import Pango
+
+
+class MarkupHandler:
+ # Maximum number of characters for which to markup synchronously.
+ max_char_sync = 100000
+
+ # Regular expressions for various markdown constructs.
+ regex = {
+ "ITALIC": re.compile(r"(\*|_)(.+?)\1"),
+ "BOLD": re.compile(r"(\*\*|__)(.+?)\1"),
+ "BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"),
+ "STRIKETHROUGH": re.compile(r"~~.+?~~"),
+ "LINK": re.compile(r"(\[).*(\]\(.+?\))"),
+ "HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n", re.MULTILINE),
+ "LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE),
+ "NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", re.MULTILINE),
+ "NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[.)]) .+", re.MULTILINE),
+ "BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:>|(?:> )+).+", re.MULTILINE),
+ "HEADER": re.compile(r"^[ ]{0,3}(#{1,6}) [^\n]+", re.MULTILINE),
+ "HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE),
+ "CODE": re.compile(r"(?:^|\n)[ ]{0,3}(([`~]{3}).+?[ ]{0,3}\2)(?:\n|$)", re.DOTALL),
+ "TABLE": re.compile(r"^[\-+]{5,}\n(.+?)\n[\-+]{5,}\n", re.DOTALL),
+ "MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"),
+ }
+
+ def __init__(self, text_view):
+ self.text_view = text_view
+ self.text_buffer = text_view.get_buffer()
+
+ # Styles
+ buffer = self.text_buffer
+
+ self.italic = buffer.create_tag('italic',
+ weight=Pango.Weight.NORMAL,
+ style=Pango.Style.ITALIC)
+
+ self.bold = buffer.create_tag('bold',
+ weight=Pango.Weight.BOLD,
+ style=Pango.Style.NORMAL)
+
+ self.bolditalic = buffer.create_tag('bolditalic',
+ weight=Pango.Weight.BOLD,
+ style=Pango.Style.ITALIC)
+
+ self.strikethrough = buffer.create_tag('strikethrough', strikethrough=True)
+
+ self.horizontalrule = buffer.create_tag('centertext',
+ justification=Gtk.Justification.CENTER)
+
+ self.plaintext = buffer.create_tag('plaintext',
+ weight=Pango.Weight.NORMAL,
+ style=Pango.Style.NORMAL,
+ strikethrough=False,
+ justification=Gtk.Justification.LEFT)
+
+ self.table = buffer.create_tag('table')
+ self.table.set_property('wrap-mode', Gtk.WrapMode.NONE)
+ self.table.set_property('pixels-above-lines', 0)
+ self.table.set_property('pixels-below-lines', 0)
+
+ self.mathtext = buffer.create_tag('mathtext')
+
+ self.graytext = buffer.create_tag('graytext',
+ foreground='gray',
+ weight=Pango.Weight.NORMAL,
+ style=Pango.Style.NORMAL)
+
+ # Margin and indents
+ # A baseline margin is set to allow negative offsets for formatting headers, lists, etc
+ self.margins_indents = {}
+ self.update_margins_indents()
+
+ # Style
+ self.on_style_updated()
+
+ self.version = 0
+
+ def on_style_updated(self, *_):
+ (found, color) = self.text_view.get_style_context().lookup_color('math_text_color')
+ if not found:
+ (_, color) = self.text_view.get_style_context().lookup_color('foreground_color')
+ self.mathtext.set_property("foreground", color.to_string())
+
+ def apply(self):
+ self.version = self.version + 1
+ if self.text_buffer.get_char_count() < self.max_char_sync:
+ self.do_apply()
+ else:
+ GLib.idle_add(self.do_apply, self.version)
+
+ def do_apply(self, version=None):
+ if version is not None and version != self.version:
+ return
+
+ buffer = self.text_buffer
+ start = buffer.get_start_iter()
+ end = buffer.get_end_iter()
+ offset = 0
+
+ text = buffer.get_slice(start, end, False)
+
+ # Remove tags
+ buffer.remove_tag(self.italic, start, end)
+ buffer.remove_tag(self.bold, start, end)
+ buffer.remove_tag(self.bolditalic, start, end)
+ buffer.remove_tag(self.strikethrough, start, end)
+ buffer.remove_tag(self.horizontalrule, start, end)
+ buffer.remove_tag(self.plaintext, start, end)
+ buffer.remove_tag(self.table, start, end)
+ buffer.remove_tag(self.mathtext, start, end)
+ for tag in self.margins_indents.values():
+ buffer.remove_tag(tag, start, end)
+ buffer.remove_tag(self.graytext, start, end)
+ buffer.remove_tag(self.graytext, start, end)
+
+ # Apply "_italic_" tag (italic)
+ matches = re.finditer(self.regex["ITALIC"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.italic, start_iter, end_iter)
+
+ # Apply "**bold**" tag (bold)
+ matches = re.finditer(self.regex["BOLD"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.bold, start_iter, end_iter)
+
+ # Apply "***bolditalic***" tag (bold/italic)
+ matches = re.finditer(self.regex["BOLDITALIC"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.bolditalic, start_iter, end_iter)
+
+ # Apply "~~strikethrough~~" tag (strikethrough)
+ matches = re.finditer(self.regex["STRIKETHROUGH"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.strikethrough, start_iter, end_iter)
+
+ matches = re.finditer(self.regex["LINK"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start(1))
+ end_iter = buffer.get_iter_at_offset(offset + match.end(1))
+ buffer.apply_tag(self.graytext, start_iter, end_iter)
+ start_iter = buffer.get_iter_at_offset(offset + match.start(2))
+ end_iter = buffer.get_iter_at_offset(offset + match.end(2))
+ buffer.apply_tag(self.graytext, start_iter, end_iter)
+
+ # Apply "---" horizontal rule tag (center)
+ matches = re.finditer(self.regex["HORIZONTALRULE"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start(1))
+ end_iter = buffer.get_iter_at_offset(offset + match.end(1))
+ buffer.apply_tag(self.horizontalrule, start_iter, end_iter)
+
+ # Apply "* list" tag (offset)
+ matches = re.finditer(self.regex["LIST"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ # Lists use character+space (eg. "* ")
+ length = 2
+ nest = len(match.group(1).replace(" ", "\t"))
+ margin = -length - 2 * nest
+ indent = -length - 2 * length * nest
+ buffer.apply_tag(self.get_margin_indent_tag(margin, indent), start_iter, end_iter)
+
+ # Apply "1. numbered list" tag (offset)
+ matches = re.finditer(self.regex["NUMBEREDLIST"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ # Numeric lists use numbers/letters+dot/parens+space (eg. "123. ")
+ length = len(match.group(2)) + 1
+ nest = len(match.group(1).replace(" ", "\t"))
+ margin = -length - 2 * nest
+ indent = -length - 2 * length * nest
+ buffer.apply_tag(self.get_margin_indent_tag(margin, indent), start_iter, end_iter)
+
+ # Apply "> blockquote" tag (offset)
+ matches = re.finditer(self.regex["BLOCKQUOTE"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.get_margin_indent_tag(2, -2), start_iter, end_iter)
+
+ # Apply "#" tag (offset + bold)
+ matches = re.finditer(self.regex["HEADER"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ margin = -len(match.group(1)) - 1
+ buffer.apply_tag(self.get_margin_indent_tag(margin, 0), start_iter, end_iter)
+ buffer.apply_tag(self.bold, start_iter, end_iter)
+
+ # Apply "======" header underline tag (bold)
+ matches = re.finditer(self.regex["HEADER_UNDER"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.bold, start_iter, end_iter)
+
+ # Apply "```" code tag (offset)
+ matches = re.finditer(self.regex["CODE"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start(1))
+ end_iter = buffer.get_iter_at_offset(offset + match.end(1))
+ buffer.apply_tag(self.get_margin_indent_tag(0, 2), start_iter, end_iter)
+ buffer.apply_tag(self.plaintext, start_iter, end_iter)
+
+ # Apply "---" table tag (wrap/pixels)
+ matches = re.finditer(self.regex["TABLE"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.table, start_iter, end_iter)
+
+ # Apply "$math$" tag (colorize)
+ matches = re.finditer(self.regex["MATH"], text)
+ for match in matches:
+ start_iter = buffer.get_iter_at_offset(offset + match.start())
+ end_iter = buffer.get_iter_at_offset(offset + match.end())
+ buffer.apply_tag(self.mathtext, start_iter, end_iter)
+
+ # Apply focus mode tag (grey out before/after current sentence)
+ if self.text_view.focus_mode:
+ cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
+ start_sentence = cursor_iter.copy()
+ start_sentence.backward_sentence_start()
+ end_sentence = cursor_iter.copy()
+ end_sentence.forward_sentence_end()
+ if start.compare(start_sentence) <= 0:
+ buffer.apply_tag(self.graytext, start, start_sentence)
+ if end.compare(end_sentence) >= 0:
+ buffer.apply_tag(self.graytext, end_sentence, end)
+
+ # Margin and indent are cumulative. They differ in two ways:
+ # * Margin is always in the beginning, which means it effectively only affects the first line
+ # of multi-line text. Indent is applied to every line.
+ # * Margin level can be negative, as a baseline margin exists from which it can be subtracted.
+ # Indent is always positive, or 0.
+ def get_margin_indent_tag(self, margin_level, indent_level):
+ level = (margin_level, indent_level)
+ if level not in self.margins_indents:
+ tag = self.text_buffer.create_tag(
+ "margin_indent_" + str(margin_level) + "_" + str(indent_level))
+ margin, indent = self.get_margin_indent(margin_level, indent_level)
+ tag.set_property("left-margin", margin)
+ tag.set_property("indent", indent)
+ self.margins_indents[level] = tag
+ return tag
+ else:
+ return self.margins_indents[level]
+
+ def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
+ if baseline_margin is None:
+ baseline_margin = self.text_view.get_left_margin()
+ if char_width is None:
+ char_width = helpers.get_char_width(self.text_view)
+ margin = max(baseline_margin + char_width * margin_level, 0)
+ indent = char_width * indent_level
+ return margin, indent
+
+ def update_margins_indents(self):
+ baseline_margin = self.text_view.get_left_margin()
+ char_width = helpers.get_char_width(self.text_view)
+
+ # Adjust tab size, as character width can change
+ tab_array = Pango.TabArray.new(1, True)
+ tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
+ self.text_view.set_tabs(tab_array)
+
+ # Adjust margins and indents, as character width can change
+ for level, tag in self.margins_indents.items():
+ margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
+ tag.set_property("left-margin", margin)
+ tag.set_property("indent", indent)
diff --git a/uberwriter/text_view_undo_redo_handler.py b/uberwriter/text_view_undo_redo_handler.py
new file mode 100644
index 0000000..f91903c
--- /dev/null
+++ b/uberwriter/text_view_undo_redo_handler.py
@@ -0,0 +1,204 @@
+class UndoableInsert:
+ """Something has been inserted into text_buffer"""
+
+ def __init__(self, text_iter, text, length):
+ self.offset = text_iter.get_offset()
+ self.text = text
+ self.length = length
+ self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " "))
+
+
+class UndoableDelete:
+ """Something has been deleted from text_buffer"""
+
+ def __init__(self, text_buffer, start_iter, end_iter):
+ self.text = text_buffer.get_text(start_iter, end_iter, False)
+ self.start = start_iter.get_offset()
+ self.end = end_iter.get_offset()
+ # Find out if backspace or delete were used to not mess up redo
+ insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
+ self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
+ self.mergeable = not bool(self.end - self.start > 1 or self.text in ("\r", "\n", " "))
+
+
+class UndoRedoHandler:
+ """Manages undo/redo for a given text_buffer.
+
+ Methods can be called directly, as well as be used as signal callbacks."""
+
+ def __init__(self):
+ self.undo_stack = []
+ self.redo_stack = []
+ self.not_undoable_action = False
+ self.undo_in_progress = False
+
+ def undo(self, text_view, _data=None):
+ """Undo insertions or deletions. Undone actions are moved to redo stack.
+
+ This method can be registered to a custom undo signal, or used independently."""
+
+ if not self.undo_stack:
+ return
+ self.__begin_not_undoable_action()
+ self.undo_in_progress = True
+ undo_action = self.undo_stack.pop()
+ self.redo_stack.append(undo_action)
+ text_buffer = text_view.get_buffer()
+ if isinstance(undo_action, UndoableInsert):
+ offset = undo_action.offset
+ start = text_buffer.get_iter_at_offset(offset)
+ stop = text_buffer.get_iter_at_offset(
+ offset + undo_action.length
+ )
+ text_buffer.place_cursor(start)
+ text_buffer.delete(start, stop)
+ else:
+ start = text_buffer.get_iter_at_offset(undo_action.start)
+ text_buffer.insert(start, undo_action.text)
+ if undo_action.delete_key_used:
+ text_buffer.place_cursor(start)
+ else:
+ stop = text_buffer.get_iter_at_offset(undo_action.end)
+ text_buffer.place_cursor(stop)
+ self.__end_not_undoable_action()
+ self.undo_in_progress = False
+
+ def redo(self, text_view, _data=None):
+ """Redo insertions or deletions. Redone actions are moved to undo stack
+
+ This method can be registered to a custom redo signal, or used independently."""
+
+ if not self.redo_stack:
+ return
+ self.__begin_not_undoable_action()
+ self.undo_in_progress = True
+ redo_action = self.redo_stack.pop()
+ self.undo_stack.append(redo_action)
+ text_buffer = text_view.get_buffer()
+ if isinstance(redo_action, UndoableInsert):
+ start = text_buffer.get_iter_at_offset(redo_action.offset)
+ text_buffer.insert(start, redo_action.text)
+ new_cursor_pos = text_buffer.get_iter_at_offset(
+ redo_action.offset + redo_action.length)
+ text_buffer.place_cursor(new_cursor_pos)
+ else:
+ start = text_buffer.get_iter_at_offset(redo_action.start)
+ stop = text_buffer.get_iter_at_offset(redo_action.end)
+ text_buffer.delete(start, stop)
+ text_buffer.place_cursor(start)
+ self.__end_not_undoable_action()
+ self.undo_in_progress = False
+
+ def clear(self):
+ self.undo_stack = []
+ self.redo_stack = []
+
+ def on_insert_text(self, _text_buffer, text_iter, text, _length):
+ """Registers a text insert. Refer to TextBuffer's "insert-text" signal.
+
+ This method must be registered to TextBuffer's "insert-text" signal, or called manually."""
+
+ def can_be_merged(prev, cur):
+ """Check if multiple insertions can be merged
+
+ can't merge if prev and cur are not mergeable in the first place
+ can't merge when user set the input bar somewhere else
+ can't merge across word boundaries"""
+
+ whitespace = (' ', '\t')
+ if not cur.mergeable or not prev.mergeable:
+ return False
+ if cur.offset != (prev.offset + prev.length):
+ return False
+ if cur.text in whitespace and prev.text not in whitespace:
+ return False
+ if prev.text in whitespace and cur.text not in whitespace:
+ return False
+ return True
+
+ if not self.undo_in_progress:
+ self.redo_stack = []
+ if self.not_undoable_action:
+ return
+
+ undo_action = UndoableInsert(text_iter, text, len(text))
+ try:
+ prev_insert = self.undo_stack.pop()
+ except IndexError:
+ self.undo_stack.append(undo_action)
+ return
+ if not isinstance(prev_insert, UndoableInsert):
+ self.undo_stack.append(prev_insert)
+ self.undo_stack.append(undo_action)
+ return
+ if can_be_merged(prev_insert, undo_action):
+ prev_insert.length += undo_action.length
+ prev_insert.text += undo_action.text
+ self.undo_stack.append(prev_insert)
+ else:
+ self.undo_stack.append(prev_insert)
+ self.undo_stack.append(undo_action)
+
+ def on_delete_range(self, text_buffer, start_iter, end_iter):
+ """Registers a range deletion. Refer to TextBuffer's "delete-range" signal.
+
+ This method must be registered to TextBuffer's "delete-range" signal, or called manually."""
+
+ def can_be_merged(prev, cur):
+ """Check if multiple deletions can be merged
+
+ can't merge if prev and cur are not mergeable in the first place
+ can't merge if delete and backspace key were both used
+ can't merge across word boundaries"""
+
+ whitespace = (' ', '\t')
+ if not cur.mergeable or not prev.mergeable:
+ return False
+ if prev.delete_key_used != cur.delete_key_used:
+ return False
+ if prev.start != cur.start and prev.start != cur.end:
+ return False
+ if cur.text not in whitespace and \
+ prev.text in whitespace:
+ return False
+ if cur.text in whitespace and \
+ prev.text not in whitespace:
+ return False
+ return True
+
+ if not self.undo_in_progress:
+ self.redo_stack = []
+ if self.not_undoable_action:
+ return
+ undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
+ try:
+ prev_delete = self.undo_stack.pop()
+ except IndexError:
+ self.undo_stack.append(undo_action)
+ return
+ if not isinstance(prev_delete, UndoableDelete):
+ self.undo_stack.append(prev_delete)
+ self.undo_stack.append(undo_action)
+ return
+ if can_be_merged(prev_delete, undo_action):
+ if prev_delete.start == undo_action.start: # delete key used
+ prev_delete.text += undo_action.text
+ prev_delete.end += (undo_action.end - undo_action.start)
+ else: # Backspace used
+ prev_delete.text = "%s%s" % (undo_action.text,
+ prev_delete.text)
+ prev_delete.start = undo_action.start
+ self.undo_stack.append(prev_delete)
+ else:
+ self.undo_stack.append(prev_delete)
+ self.undo_stack.append(undo_action)
+
+ def __begin_not_undoable_action(self):
+ """Toggle to stop recording actions"""
+
+ self.not_undoable_action = True
+
+ def __end_not_undoable_action(self):
+ """Toggle to start recording actions"""
+
+ self.not_undoable_action = False
diff --git a/uberwriter/theme.py b/uberwriter/theme.py
index e43ec57..8139c90 100644
--- a/uberwriter/theme.py
+++ b/uberwriter/theme.py
@@ -11,6 +11,7 @@ class Theme:
The light variant is listed first, followed by the dark variant, if any.
"""
+ previous = None
settings = Settings.new()
def __init__(self, name, gtk_css_path, web_css_path, is_dark, inverse_name):
@@ -29,16 +30,30 @@ class Theme:
return current_theme
@classmethod
- def get_current(cls):
+ def get_current_changed(cls):
theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name')
+ dark_mode_auto = cls.settings.get_value('dark-mode-auto').get_boolean()
dark_mode = cls.settings.get_value('dark-mode').get_boolean()
current_theme = cls.get_for_name(theme_name)
- # Technically, we could very easily allow the user to force the light ui on a dark theme.
- # However, as there is no inverse of "gtk-application-prefer-dark-theme", we shouldn't do that.
- if dark_mode and not current_theme.is_dark and current_theme.inverse_name:
+ if not dark_mode_auto and dark_mode != current_theme.is_dark and current_theme.inverse_name:
current_theme = cls.get_for_name(current_theme.inverse_name, current_theme.name)
+ changed = current_theme != cls.previous
+ cls.previous = current_theme
+ return current_theme, changed
+
+ @classmethod
+ def get_current(cls):
+ current_theme, _ = cls.get_current_changed()
return current_theme
+ def __eq__(self, other):
+ return isinstance(other, self.__class__) and \
+ self.name == other.name and \
+ self.gtk_css_path == other.gtk_css_path and \
+ self.web_css_path == other.web_css_path and \
+ self.is_dark == other.is_dark and \
+ self.inverse_name == other.inverse_name
+
defaultThemes = [
# https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita
diff --git a/uberwriter/webkit2png/webkit2png.py b/uberwriter/webkit2png/webkit2png.py
index ffc2345..ba9f22b 100755
--- a/uberwriter/webkit2png/webkit2png.py
+++ b/uberwriter/webkit2png/webkit2png.py
@@ -101,7 +101,6 @@ class PyGTKBrowser:
if options.delay:
print("--delay is only supported on Mac OS X (for now). Sorry!")
- gobject.threads_init()
window = gtk.Window()
window.resize(int(options.initWidth),int(options.initHeight))
self.view = webkit.WebView()
diff --git a/uberwriter/window.py b/uberwriter/window.py
index ba784c7..f541773 100644
--- a/uberwriter/window.py
+++ b/uberwriter/window.py
@@ -17,43 +17,35 @@
import codecs
import locale
import logging
-import mimetypes
import os
import re
-import subprocess
import urllib
import webbrowser
from gettext import gettext as _
import gi
-from gi.repository.GObject import param_spec_string
+from uberwriter.export_dialog import Export
+from uberwriter.text_view import TextView
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0') # pylint: disable=wrong-import-position
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
from gi.repository import WebKit2 as WebKit
-from gi.repository import Pango # pylint: disable=E0611
import cairo
-# import cairo.Pattern, cairo.SolidPattern
-from uberwriter import headerbars
from uberwriter import helpers
from uberwriter.theme import Theme
from uberwriter.helpers import get_builder
from uberwriter.gtkspellcheck import SpellChecker
-from uberwriter.markup_buffer import MarkupBuffer
-from uberwriter.text_editor import TextEditor
-from uberwriter.inline_preview import InlinePreview
from uberwriter.sidebar import Sidebar
from uberwriter.search_and_replace import SearchAndReplace
from uberwriter.settings import Settings
-# from .auto_correct import AutoCorrect
-from uberwriter.export_dialog import Export
-# from .plugins.bibtex import BibTex
+from . import headerbars
+
# Some Globals
# TODO move them somewhere for better
# accesibility from other files
@@ -62,10 +54,17 @@ LOGGER = logging.getLogger('uberwriter')
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
-# See texteditor_lib.Window.py for more details about how this class works
-
class Window(Gtk.ApplicationWindow):
+ __gsignals__ = {
+ 'save-file': (GObject.SIGNAL_ACTION, None, ()),
+ 'open-file': (GObject.SIGNAL_ACTION, None, ()),
+ 'save-file-as': (GObject.SIGNAL_ACTION, None, ()),
+ 'new-file': (GObject.SIGNAL_ACTION, None, ()),
+ 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()),
+ 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()),
+ 'close-window': (GObject.SIGNAL_ACTION, None, ())
+ }
WORDCOUNT = re.compile(r"(?!\-\w)[\s#*\+\-]+", re.UNICODE)
@@ -76,38 +75,37 @@ class Window(Gtk.ApplicationWindow):
application=Gio.Application.get_default(),
title="Uberwriter")
- self.builder = get_builder('UberwriterWindow')
- self.add(self.builder.get_object("FullscreenOverlay"))
+ # Set UI
+ self.builder = get_builder('Window')
+ root = self.builder.get_object("FullscreenOverlay")
+ root.connect('style-updated', self.apply_current_theme)
+ self.add(root)
- self.set_default_size(850, 500)
+ self.set_default_size(900, 500)
- # preferences
+ # Preferences
self.settings = Settings.new()
- self.set_name('UberwriterWindow')
-
# Headerbars
self.headerbar = headerbars.MainHeaderbar(app)
self.set_titlebar(self.headerbar.hb_container)
- self.fs_headerbar = headerbars.FsHeaderbar(self.builder, app)
+ self.fs_headerbar = headerbars.FullscreenHeaderbar(self.builder, app)
self.title_end = " – UberWriter"
self.set_headerbar_title("New File" + self.title_end)
- self.focusmode = False
-
self.word_count = self.builder.get_object('word_count')
self.char_count = self.builder.get_object('char_count')
# Setup status bar hide after 3 seconds
-
self.status_bar = self.builder.get_object('status_bar_box')
self.statusbar_revealer = self.builder.get_object('status_bar_revealer')
- self.status_bar.get_style_context().add_class('status_bar_box')
+ self.status_bar.get_style_context().add_class('status-bar-box')
self.status_bar_visible = True
self.was_motion = True
self.buffer_modified_for_status_bar = False
+ self.timestamp_last_mouse_motion = 0
if self.settings.get_value("poll-motion"):
self.connect("motion-notify-event", self.on_motion_notify)
GObject.timeout_add(3000, self.poll_for_motion)
@@ -116,116 +114,30 @@ class Window(Gtk.ApplicationWindow):
self.add_accel_group(self.accel_group)
# Setup text editor
- self.text_editor = TextEditor()
- self.text_editor.set_name('UberwriterEditor')
- self.get_style_context().add_class('uberwriter_window')
+ self.text_view = TextView()
+ self.text_view.props.halign = Gtk.Align.CENTER
+ self.text_view.connect('focus-out-event', self.focus_out)
+ self.text_view.show()
+ self.text_view.grab_focus()
- base_leftmargin = 100
- self.text_editor.set_left_margin(base_leftmargin)
- self.text_editor.set_left_margin(40)
- self.text_editor.set_top_margin(80)
- self.text_editor.props.width_request = 600
- self.text_editor.props.halign = Gtk.Align.CENTER
- self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1'))
- self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
- self.text_editor.connect('focus-out-event', self.focus_out)
- self.text_editor.get_style_context().connect('changed', self.style_changed)
-
- self.text_editor.set_top_margin(80)
- self.text_editor.set_bottom_margin(16)
-
- self.text_editor.set_pixels_above_lines(4)
- self.text_editor.set_pixels_below_lines(4)
- self.text_editor.set_pixels_inside_wrap(8)
-
- tab_array = Pango.TabArray.new(1, True)
- tab_array.set_tab(0, Pango.TabAlign.LEFT, 20)
- self.text_editor.set_tabs(tab_array)
-
- self.text_editor.show()
- self.text_editor.grab_focus()
+ self.text_view.get_buffer().connect('changed', self.on_text_changed)
# Setup preview webview
self.preview_webview = None
- self.editor_alignment = self.builder.get_object('editor_alignment')
self.scrolled_window = self.builder.get_object('editor_scrolledwindow')
- self.scrolled_window.props.width_request = 600
- self.scrolled_window.add(self.text_editor)
- self.alignment_padding = 40
+ self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window')
+ self.scrolled_window.add(self.text_view)
self.editor_viewport = self.builder.get_object('editor_viewport')
# some people seems to have performance problems with the overlay.
# Let them disable it
-
- if self.settings.get_value("gradient-overlay"):
- self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient)
-
- self.smooth_scroll_starttime = 0
- self.smooth_scroll_endtime = 0
- self.smooth_scroll_acttarget = 0
- self.smooth_scroll_data = {
- 'target_pos': -1,
- 'source_pos': -1,
- 'duration': 0
- }
- self.smooth_scroll_tickid = -1
-
- self.text_buffer = self.text_editor.get_buffer()
- self.text_buffer.set_text('')
-
- # Init Window height for top/bottom padding
- self.window_height = self.get_size()[1]
-
- self.text_change_event = self.text_buffer.connect(
- 'changed', self.text_changed)
+ self.overlay_id = None
+ self.toggle_gradient_overlay(self.settings.get_value("gradient-overlay"))
# Init file name with None
self.set_filename()
- # Markup and Shortcuts for the TextBuffer
- self.markup_buffer = MarkupBuffer(
- self, self.text_buffer, base_leftmargin)
- self.markup_buffer.markup_buffer()
-
- # Set current theme
- self.apply_current_theme()
-
- # Scrolling -> Dark or not?
- self.textchange = False
- self.scroll_count = 0
- self.timestamp_last_mouse_motion = 0
- self.text_buffer.connect_after('mark-set', self.mark_set)
-
- # Drag and drop
-
- # self.TextEditor.drag_dest_unset()
- # self.TextEditor.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
- self.target_list = Gtk.TargetList.new([])
- self.target_list.add_uri_targets(1)
- self.target_list.add_text_targets(2)
-
- self.text_editor.drag_dest_set_target_list(self.target_list)
- self.text_editor.connect_after(
- 'drag-data-received', self.on_drag_data_received)
-
- def on_drop(_widget, *_args):
- print("drop")
- self.text_editor.connect('drag-drop', on_drop)
-
- self.text_buffer.connect('paste-done', self.paste_done)
- # self.connect('key-press-event', self.alt_mod)
-
- # Events for Typewriter mode
-
- # Setting up inline preview
- self.inline_preview = InlinePreview(
- self.text_editor, self.text_buffer)
-
- # Vertical scrolling
- self.vadjustment = self.scrolled_window.get_vadjustment()
- self.vadjustment.connect('value-changed', self.scrolled)
-
# Setting up spellcheck
self.auto_correct = None
self.toggle_spellcheck(self.settings.get_value("spellcheck"))
@@ -243,87 +155,49 @@ class Window(Gtk.ApplicationWindow):
# Search and replace initialization
# Same interface as Sidebar ;)
###
- self.searchreplace = SearchAndReplace(self)
+ self.searchreplace = SearchAndReplace(self, self.text_view)
# Window resize
self.window_resize(self)
self.connect("configure-event", self.window_resize)
self.connect("delete-event", self.on_delete_called)
- __gsignals__ = {
- 'save-file': (GObject.SIGNAL_ACTION, None, ()),
- 'open-file': (GObject.SIGNAL_ACTION, None, ()),
- 'save-file-as': (GObject.SIGNAL_ACTION, None, ()),
- 'new-file': (GObject.SIGNAL_ACTION, None, ()),
- 'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()),
- 'toggle-preview': (GObject.SIGNAL_ACTION, None, ()),
- 'close-window': (GObject.SIGNAL_ACTION, None, ())
- }
+ # Set current theme
+ self.apply_current_theme()
+ self.get_style_context().add_class('uberwriter-window')
- def apply_current_theme(self):
- """Adjusts both the window and the CSD for the current theme.
+ def apply_current_theme(self, *_):
+ """Adjusts the window, CSD and preview for the current theme.
"""
+ # Get current theme
+ theme, changed = Theme.get_current_changed()
+ if changed:
+ # Set theme variant (dark/light)
+ Gtk.Settings.get_default().set_property(
+ "gtk-application-prefer-dark-theme",
+ GLib.Variant("b", theme.is_dark))
- self.markup_buffer.update_style()
+ # Set theme css
+ style_provider = Gtk.CssProvider()
+ style_provider.load_from_path(theme.gtk_css_path)
+ Gtk.StyleContext.add_provider_for_screen(
+ self.get_screen(), style_provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
- # Reload preview if it exists, otherwise redraw contents of window (self)
- if self.preview_webview:
- self.show_preview()
- else:
+ # Reload preview if it exists
+ self.reload_preview()
+
+ # Redraw contents of window
self.queue_draw()
- def scrolled(self, widget):
- """if window scrolled + focusmode make font black again"""
- # if self.focusmode:
- # if self.textchange == False:
- # if self.scroll_count >= 4:
- # self.TextBuffer.apply_tag(
- # self.MarkupBuffer.blackfont,
- # self.TextBuffer.get_start_iter(),
- # self.TextBuffer.get_end_iter())
- # else:
- # self.scroll_count += 1
- # else:
- # self.scroll_count = 0
- # self.textchange = False
-
- def paste_done(self, *_):
- self.markup_buffer.markup_buffer(0)
-
- def init_typewriter(self):
- """put the cursor at the center of the screen by setting top and
- bottom margins to height/2
- """
-
- editor_height = self.text_editor.get_allocation().height
- self.text_editor.props.top_margin = editor_height / 2
- self.text_editor.props.bottom_margin = editor_height / 2
-
- def remove_typewriter(self):
- """set margins to default values
- """
-
- self.text_editor.props.top_margin = 80
- self.text_editor.props.bottom_margin = 16
- self.text_change_event = self.text_buffer.connect(
- 'changed', self.text_changed)
-
- def get_text(self):
- """get text from self.text_buffer
- """
-
- start_iter = self.text_buffer.get_start_iter()
- end_iter = self.text_buffer.get_end_iter()
- return self.text_buffer.get_text(start_iter, end_iter, False)
-
def update_line_and_char_count(self):
"""it... it updates line and characters count
"""
if self.status_bar_visible is False:
return
- self.char_count.set_text(str(self.text_buffer.get_char_count()))
- text = self.get_text()
+ text = self.text_view.get_text()
+ self.char_count.set_text(str(len(text)))
words = re.split(self.WORDCOUNT, text)
length = len(words)
# Last word a "space"
@@ -336,12 +210,7 @@ class Window(Gtk.ApplicationWindow):
length = 0
self.word_count.set_text(str(length))
- def mark_set(self, _buffer, _location, mark, _data=None):
- if mark.get_name() in ['insert', 'gtk_drag_target']:
- self.check_scroll(mark)
- return True
-
- def text_changed(self, *_args):
+ def on_text_changed(self, *_args):
"""called when the text changes, sets the self.did_change to true and
updates the title and the counters to reflect that
"""
@@ -351,12 +220,8 @@ class Window(Gtk.ApplicationWindow):
title = self.get_title()
self.set_headerbar_title("* " + title)
- self.markup_buffer.markup_buffer(1)
- self.textchange = True
-
self.buffer_modified_for_status_bar = True
self.update_line_and_char_count()
- self.check_scroll(self.text_buffer.get_insert())
def set_fullscreen(self, state):
"""Puts the application in fullscreen mode and show/hides
@@ -374,184 +239,61 @@ class Window(Gtk.ApplicationWindow):
self.unfullscreen()
self.fs_headerbar.events.hide()
- self.text_editor.grab_focus()
+ self.text_view.grab_focus()
def set_focus_mode(self, state):
"""toggle focusmode
"""
- if state.get_boolean():
- self.init_typewriter()
- self.markup_buffer.focusmode_highlight()
- self.focusmode = True
- self.text_editor.grab_focus()
- self.check_scroll(self.text_buffer.get_insert())
- if self.spell_checker:
- self.spell_checker._misspelled.set_property('underline', 0)
- self.click_event = self.text_editor.connect("button-release-event",
- self.on_focusmode_click)
- else:
- self.remove_typewriter()
- self.focusmode = False
- self.text_buffer.remove_tag(self.markup_buffer.unfocused_text,
- self.text_buffer.get_start_iter(),
- self.text_buffer.get_end_iter())
- self.text_buffer.remove_tag(self.markup_buffer.blackfont,
- self.text_buffer.get_start_iter(),
- self.text_buffer.get_end_iter())
-
- self.markup_buffer.markup_buffer(1)
- self.text_editor.grab_focus()
- self.update_line_and_char_count()
- self.check_scroll()
- if self.spell_checker:
- self.spell_checker._misspelled.set_property('underline', 4)
- _click_event = self.text_editor.disconnect(self.click_event)
+ focus_mode = state.get_boolean()
+ self.text_view.set_focus_mode(focus_mode)
+ if self.spell_checker:
+ self.spell_checker._misspelled.set_property('underline', 0 if focus_mode else 4)
+ self.text_view.grab_focus()
def set_hemingway_mode(self, state):
"""toggle hemingwaymode
"""
- self.text_editor.can_delete = not state.get_boolean()
- self.text_editor.grab_focus()
- def on_focusmode_click(self, *_args):
- """call MarkupBuffer to mark as bold the line where the cursor is
- """
+ self.text_view.set_hemingway_mode(state.get_boolean())
+ self.text_view.grab_focus()
- self.markup_buffer.markup_buffer(1)
-
- def scroll_smoothly(self, widget, frame_clock, _data=None):
- if self.smooth_scroll_data['target_pos'] == -1:
- return True
-
- def ease_out_cubic(time):
- time = time - 1
- return pow(time, 3) + 1
-
- now = frame_clock.get_frame_time()
- if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']:
- self.smooth_scroll_starttime = now
- self.smooth_scroll_endtime = now + \
- self.smooth_scroll_data['duration'] * 100
- self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos']
-
- if now < self.smooth_scroll_endtime:
- time = float(now - self.smooth_scroll_starttime) / float(
- self.smooth_scroll_endtime - self.smooth_scroll_starttime)
- else:
- time = 1
- pos = self.smooth_scroll_data['source_pos'] \
- + (time * (self.smooth_scroll_data['target_pos']
- - self.smooth_scroll_data['source_pos']))
- widget.get_vadjustment().props.value = pos
- self.smooth_scroll_data['target_pos'] = -1
- return True
-
- time = ease_out_cubic(time)
- pos = self.smooth_scroll_data['source_pos'] \
- + (time * (self.smooth_scroll_data['target_pos']
- - self.smooth_scroll_data['source_pos']))
- widget.get_vadjustment().props.value = pos
- return True # continue ticking
-
- def check_scroll(self, mark=None):
- gradient_offset = 80
- buf = self.text_editor.get_buffer()
- if mark:
- ins_it = buf.get_iter_at_mark(mark)
- else:
- ins_it = buf.get_iter_at_mark(buf.get_insert())
- loc_rect = self.text_editor.get_iter_location(ins_it)
-
- # alignment offset added from top
- pos_y = loc_rect.y + loc_rect.height + self.text_editor.props.top_margin # pylint: disable=no-member
-
- ha = self.scrolled_window.get_vadjustment()
- if ha.props.page_size < gradient_offset:
- return
- pos = pos_y - ha.props.value
- # print("pos: %i, pos_y %i, page_sz: %i, val: %i" % (pos, pos_y, ha.props.page_size
- # - gradient_offset, ha.props.value))
- # global t, amount, initvadjustment
- target_pos = -1
- if self.focusmode:
- # print("pos: %i > %i" % (pos, ha.props.page_size * 0.5))
- if pos != (ha.props.page_size * 0.5):
- target_pos = pos_y - (ha.props.page_size * 0.5)
- elif pos > ha.props.page_size - gradient_offset - 60:
- target_pos = pos_y - ha.props.page_size + gradient_offset + 40
- elif pos < gradient_offset:
- target_pos = pos_y - gradient_offset
- self.smooth_scroll_data = {
- 'target_pos': target_pos,
- 'source_pos': ha.props.value,
- 'duration': 2000
- }
- if self.smooth_scroll_tickid == -1:
- self.smooth_scroll_tickid = self.scrolled_window.add_tick_callback(
- self.scroll_smoothly)
-
- def window_resize(self, widget, _data=None):
+ def window_resize(self, window, event=None):
"""set paddings dependant of the window size
"""
- # To calc padding top / bottom
- self.window_height = widget.get_allocation().height
- w_width = widget.get_allocation().width
- # Calculate left / right margin
+ # Adjust text editor width depending on window width, so that:
+ # - The number of characters per line is adequate (http://webtypography.net/2.1.2)
+ # - The number of characters stays constant while resizing the window / font
+ # - There is enough text margin for MarkupBuffer to apply indents / negative margins
+ #
+ # TODO: Avoid hard-coding. Font size is clearer than unclear dimensions, but not ideal.
+ w_width = event.width if event else window.get_allocation().width
if w_width < 900:
- width_request = 600
- self.markup_buffer.set_multiplier(8)
- self.current_font_size = 12
- self.alignment_padding = 30
- lm = 7 * 8
- self.get_style_context().remove_class("medium")
- self.get_style_context().remove_class("large")
+ font_size = 14
self.get_style_context().add_class("small")
+ self.get_style_context().remove_class("large")
- elif w_width < 1400:
- width_request = 800
- self.markup_buffer.set_multiplier(10)
- self.current_font_size = 15
- self.alignment_padding = 40
- lm = 7 * 10
+ elif w_width < 1280:
+ font_size = 16
self.get_style_context().remove_class("small")
self.get_style_context().remove_class("large")
- self.get_style_context().add_class("medium")
else:
- width_request = 1000
- self.markup_buffer.set_multiplier(13)
- self.current_font_size = 17
- self.alignment_padding = 60
- lm = 7 * 13
- self.get_style_context().remove_class("medium")
+ font_size = 18
self.get_style_context().remove_class("small")
self.get_style_context().add_class("large")
- self.editor_alignment.props.margin_bottom = 0
- self.editor_alignment.props.margin_top = 0
- self.text_editor.set_left_margin(lm)
- self.text_editor.set_right_margin(lm)
+ font_width = int(font_size * 1/1.6) # Ratio specific to Fira Mono
+ width = 67 * font_width - 1 # 66 characters
+ horizontal_margin = 8 * font_width # 8 characters
+ width_request = width + horizontal_margin * 2
- self.markup_buffer.recalculate(lm)
-
- if self.focusmode:
- self.remove_typewriter()
- self.init_typewriter()
-
- if self.text_editor.props.width_request != width_request: # pylint: disable=no-member
- self.text_editor.props.width_request = width_request
+ if self.text_view.props.width_request != width_request:
+ self.text_view.props.width_request = width_request
+ self.text_view.set_left_margin(horizontal_margin)
+ self.text_view.set_right_margin(horizontal_margin)
self.scrolled_window.props.width_request = width_request
- alloc = self.text_editor.get_allocation()
- alloc.width = width_request
- self.text_editor.size_allocate(alloc)
-
- def style_changed(self, _widget, _data=None):
- pgc = self.text_editor.get_pango_context()
- mets = pgc.get_metrics()
- self.markup_buffer.set_multiplier(
- Pango.units_to_double(mets.get_approximate_char_width()) + 1)
# TODO: refactorizable
def save_document(self, _widget=None, _data=None):
@@ -563,7 +305,7 @@ class Window(Gtk.ApplicationWindow):
LOGGER.info("saving")
filename = self.filename
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
- file_to_save.write(self.get_text())
+ file_to_save.write(self.text_view.get_text())
file_to_save.close()
if self.did_change:
self.did_change = False
@@ -574,7 +316,7 @@ class Window(Gtk.ApplicationWindow):
filefilter = Gtk.FileFilter.new()
filefilter.add_mime_type('text/x-markdown')
filefilter.add_mime_type('text/plain')
- filefilter.set_name('MarkDown (.md)')
+ filefilter.set_name('Markdown (.md)')
filechooser = Gtk.FileChooserDialog(
_("Save your File"),
self,
@@ -597,7 +339,7 @@ class Window(Gtk.ApplicationWindow):
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
- file_to_save.write(self.get_text())
+ file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
@@ -638,7 +380,7 @@ class Window(Gtk.ApplicationWindow):
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
- file_to_save.write(self.get_text())
+ file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
@@ -661,15 +403,9 @@ class Window(Gtk.ApplicationWindow):
"""Copies only html without headers etc. to Clipboard
"""
- args = ['pandoc', '--from=markdown', '--to=html5']
- proc = subprocess.Popen(args, stdin=subprocess.PIPE,
- stdout=subprocess.PIPE)
-
- text = bytes(self.get_text(), "utf-8")
- output = proc.communicate(text)[0]
-
+ output = helpers.pandoc_convert(self.text_view.get_text())
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- clipboard.set_text(output.decode("utf-8"), -1)
+ clipboard.set_text(output, -1)
clipboard.store()
def open_document(self, _widget=None):
@@ -679,19 +415,24 @@ class Window(Gtk.ApplicationWindow):
if self.check_change() == Gtk.ResponseType.CANCEL:
return
- filefilter = Gtk.FileFilter.new()
- filefilter.add_mime_type('text/x-markdown')
- filefilter.add_mime_type('text/plain')
- filefilter.set_name(_('MarkDown or Plain Text'))
+ markdown_filter = Gtk.FileFilter.new()
+ markdown_filter.add_mime_type('text/markdown')
+ markdown_filter.add_mime_type('text/x-markdown')
+ markdown_filter.set_name(_('Markdown Files'))
+
+ plaintext_filter = Gtk.FileFilter.new()
+ plaintext_filter.add_mime_type('text/plain')
+ plaintext_filter.set_name(_('Plain Text Files'))
filechooser = Gtk.FileChooserDialog(
- _("Open a .md-File"),
+ _("Open a .md file"),
self,
Gtk.FileChooserAction.OPEN,
("_Cancel", Gtk.ResponseType.CANCEL,
"_Open", Gtk.ResponseType.OK)
)
- filechooser.add_filter(filefilter)
+ filechooser.add_filter(markdown_filter)
+ filechooser.add_filter(plaintext_filter)
response = filechooser.run()
if response == Gtk.ResponseType.OK:
filename = filechooser.get_filename()
@@ -705,14 +446,14 @@ class Window(Gtk.ApplicationWindow):
"""Show dialog to prevent loss of unsaved changes
"""
- if self.did_change and self.get_text():
+ if self.did_change and self.text_view.get_text():
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.NONE,
_("You have not saved your changes.")
)
- dialog.add_button(_("Close without Saving"), Gtk.ResponseType.NO)
+ dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO)
dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("Save now"), Gtk.ResponseType.YES)
# dialog.set_default_size(200, 60)
@@ -739,9 +480,7 @@ class Window(Gtk.ApplicationWindow):
if self.check_change() == Gtk.ResponseType.CANCEL:
return
- self.text_buffer.set_text('')
- self.text_editor.undos = []
- self.text_editor.redos = []
+ self.text_view.clear()
self.did_change = False
self.set_filename()
@@ -752,20 +491,20 @@ class Window(Gtk.ApplicationWindow):
"""
self.sidebar.toggle_sidebar()
- def toggle_spellcheck(self, status):
+ def toggle_spellcheck(self, state):
"""Enable/disable the autospellchecking
Arguments:
status {gtk bool} -- Desired status of the spellchecking
"""
- if status.get_boolean():
+ if state.get_boolean():
try:
self.spell_checker.enable()
except:
try:
self.spell_checker = SpellChecker(
- self.text_editor, locale.getdefaultlocale()[0],
+ self.text_view, locale.getdefaultlocale()[0],
collapse=False)
if self.auto_correct:
self.auto_correct.set_language(self.spell_checker.language)
@@ -781,7 +520,7 @@ class Window(Gtk.ApplicationWindow):
_("You can not enable the Spell Checker.")
)
dialog.format_secondary_text(
- _("Please install 'hunspell' or 'aspell' dictionarys"
+ _("Please install 'hunspell' or 'aspell' dictionaries"
+ " for your language from the software center."))
_response = dialog.run()
return
@@ -793,45 +532,17 @@ class Window(Gtk.ApplicationWindow):
pass
return
- def on_drag_data_received(self, _widget, drag_context, _x, _y,
- data, info, time):
- """Handle drag and drop events"""
- if info == 1:
- # uri target
- uris = data.get_uris()
- for uri in uris:
- uri = urllib.parse.unquote_plus(uri)
- mime = mimetypes.guess_type(uri)
+ def toggle_gradient_overlay(self, state):
+ """Toggle the gradient overlay
- if mime[0] is not None and mime[0].startswith('image'):
- if uri.startswith("file://"):
- uri = uri[7:]
- text = "![Insert image title here](%s)" % uri
- limit_left = 2
- limit_right = 23
- else:
- text = "[Insert link title here](%s)" % uri
- limit_left = 1
- limit_right = 22
- self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark(
- self.text_buffer.get_mark('gtk_drag_target')))
- self.text_buffer.insert_at_cursor(text)
- insert_mark = self.text_buffer.get_insert()
- selection_bound = self.text_buffer.get_selection_bound()
- cursor_iter = self.text_buffer.get_iter_at_mark(insert_mark)
- cursor_iter.backward_chars(len(text) - limit_left)
- self.text_buffer.move_mark(insert_mark, cursor_iter)
- cursor_iter.forward_chars(limit_right)
- self.text_buffer.move_mark(selection_bound, cursor_iter)
+ Arguments:
+ state {gtk bool} -- Desired state of the gradient overlay (enabled/disabled)
+ """
- elif info == 2:
- # Text target
- self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark(
- self.text_buffer.get_mark('gtk_drag_target')))
- self.text_buffer.insert_at_cursor(data.get_text())
- Gtk.drag_finish(drag_context, True, True, time)
- self.present()
- return False
+ if state.get_boolean():
+ self.overlay_id = self.scrolled_window.connect_after("draw", self.draw_gradient)
+ elif self.overlay_id:
+ self.scrolled_window.disconnect(self.overlay_id)
def toggle_preview(self, state):
"""Toggle the preview mode
@@ -849,8 +560,8 @@ class Window(Gtk.ApplicationWindow):
def show_text_editor(self):
self.scrolled_window.remove(self.scrolled_window.get_child())
- self.scrolled_window.add(self.text_editor)
- self.text_editor.show()
+ self.scrolled_window.add(self.text_view)
+ self.text_view.show()
self.preview_webview.destroy()
self.preview_webview = None
self.queue_draw()
@@ -862,49 +573,17 @@ class Window(Gtk.ApplicationWindow):
self.preview_webview.show()
self.queue_draw()
else:
- # Insert a tag with ID to scroll to
- # self.TextBuffer.insert_at_cursor('')
- # TODO
- # Find a way to find the next header, scroll to the next header.
- # TODO: provide a local version of mathjax
-
- # We need to convert relative routes to absolute ones
- # For that first we need to know if the file is saved:
- if self.filename:
- base_path = os.path.dirname(self.filename)
- else:
- base_path = ''
- os.environ['PANDOC_PREFIX'] = base_path + '/'
-
- args = ['pandoc',
- '-s',
- '--from=markdown',
- '--to=html5',
+ args = ['--standalone',
'--mathjax',
'--css=' + Theme.get_current().web_css_path,
- '--quiet',
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
-
- # TODO: find a way to pass something like this instead of the quiet arg
- #'--metadata pagetitle="test"',
-
- proc = subprocess.Popen(
- args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-
- text = bytes(self.get_text(), "utf-8")
- output = proc.communicate(text)[0]
+ output = helpers.pandoc_convert(self.text_view.get_text(), to="html5", args=args)
if self.preview_webview is None:
self.preview_webview = WebKit.WebView()
self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True)
- # Delete the cursor-scroll mark again
- # cursor_iter = self.TextBuffer.get_iter_at_mark(self.TextBuffer.get_insert())
- # begin_del = cursor_iter.copy()
- # begin_del.backward_chars(30)
- # self.TextBuffer.delete(begin_del, cursor_iter)
-
# Show preview once the load is finished
self.preview_webview.connect("load-changed", self.on_preview_load_change)
@@ -912,7 +591,11 @@ class Window(Gtk.ApplicationWindow):
# but local files are opened in appropriate apps:
self.preview_webview.connect("decide-policy", self.on_click_link)
- self.preview_webview.load_html(output.decode("utf-8"), 'file://localhost/')
+ self.preview_webview.load_html(output, 'file://localhost/')
+
+ def reload_preview(self):
+ if self.preview_webview:
+ self.show_preview()
def load_file(self, filename=None):
"""Open File from command line or open / open recent etc."""
@@ -923,19 +606,14 @@ class Window(Gtk.ApplicationWindow):
if filename.startswith('file://'):
filename = filename[7:]
filename = urllib.parse.unquote_plus(filename)
+ self.text_view.clear()
try:
- if not os.path.exists(filename):
- self.text_buffer.set_text("")
- else:
+ if os.path.exists(filename):
current_file = codecs.open(filename, encoding="utf-8", mode='r')
- self.text_buffer.set_text(current_file.read())
+ self.text_view.set_text(current_file.read())
current_file.close()
- self.markup_buffer.markup_buffer(0)
- self.set_headerbar_title(
- os.path.basename(filename) + self.title_end)
- self.text_editor.undo_stack = []
- self.text_editor.redo_stack = []
+ self.set_headerbar_title(os.path.basename(filename) + self.title_end)
self.set_filename(filename)
except Exception:
@@ -967,7 +645,7 @@ class Window(Gtk.ApplicationWindow):
response = self.export.dialog.run()
if response == 1:
- self.export.export(bytes(self.get_text(), "utf-8"))
+ self.export.export(bytes(self.text_view.get_text(), "utf-8"))
self.export.dialog.destroy()
@@ -991,7 +669,7 @@ class Window(Gtk.ApplicationWindow):
if (self.was_motion is False
and self.status_bar_visible
and self.buffer_modified_for_status_bar
- and self.text_editor.props.has_focus): # pylint: disable=no-member
+ and self.text_view.props.has_focus): # pylint: disable=no-member
# self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True)
self.statusbar_revealer.set_reveal_child(False)
self.headerbar.hb_revealer.set_reveal_child(False)
@@ -1064,24 +742,6 @@ class Window(Gtk.ApplicationWindow):
cr.set_source(lg_btm)
cr.fill()
- def use_experimental_features(self, _val):
- """use experimental features
- """
- pass
- # try:
- # self.auto_correct = AutoCorrect(
- # self.text_editor, self.text_buffer)
- # except:
- # LOGGER.debug("Couldn't install autocorrect.")
-
- # self.plugins = [BibTex(self)]
-
- # def alt_mod(self, _widget, event, _data=None):
- # # TODO: Click and open when alt is pressed
- # if event.state & Gdk.ModifierType.MOD2_MASK:
- # LOGGER.info("Alt pressed")
- # return
-
def on_delete_called(self, _widget, _data=None):
"""Called when the TexteditorWindow is closed.
"""
@@ -1091,7 +751,7 @@ class Window(Gtk.ApplicationWindow):
return False
def on_mnu_close_activate(self, _widget, _data=None):
- """Signal handler for closing the UberwriterWindow.
+ """Signal handler for closing the Window.
Overriden from parent Window Class
"""
if self.on_delete_called(self): # Really destroy?