2019-03-31 02:16:18 +00:00
|
|
|
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
|
2019-03-31 02:50:34 +00:00
|
|
|
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
|
|
|
|
from uberwriter.scroller import Scroller
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
gi.require_version('Gtk', '3.0')
|
2019-04-22 21:12:51 +00:00
|
|
|
gi.require_version('Gspell', '1')
|
|
|
|
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-04-22 21:12:51 +00:00
|
|
|
# Spell checking
|
|
|
|
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
|
|
|
|
self.gspell_view.basic_setup()
|
|
|
|
|
2019-03-31 02:16:18 +00:00
|
|
|
# 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
|
2019-03-31 02:50:34 +00:00
|
|
|
self.scroller = None
|
2019-03-31 02:16:18 +00:00
|
|
|
self.get_buffer().connect('mark-set', self.on_mark_set)
|
|
|
|
|
|
|
|
# Focus mode
|
|
|
|
self.focus_mode = False
|
|
|
|
self.connect('button-release-event', self.on_button_release_event)
|
|
|
|
|
|
|
|
# Hemingway mode
|
|
|
|
self.hemingway_mode = False
|
|
|
|
self.connect('key-press-event', self.on_key_press_event)
|
|
|
|
|
|
|
|
def get_text(self):
|
|
|
|
text_buffer = self.get_buffer()
|
|
|
|
start_iter = text_buffer.get_start_iter()
|
|
|
|
end_iter = text_buffer.get_end_iter()
|
|
|
|
return text_buffer.get_text(start_iter, end_iter, False)
|
|
|
|
|
|
|
|
def set_text(self, text):
|
|
|
|
text_buffer = self.get_buffer()
|
|
|
|
text_buffer.set_text(text)
|
|
|
|
|
|
|
|
def on_text_changed(self, *_):
|
|
|
|
self.markup.apply()
|
2019-04-11 02:33:31 +00:00
|
|
|
GLib.idle_add(self.scroll_to)
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
def on_size_allocate(self, *_):
|
|
|
|
self.update_vertical_margin()
|
Add support for code blocks, improve overall markup handling
This commit adds markup support for code blocks, styling them in a
conservative manner, similar to blockquotes, solely indenting them.
Partially fixes #90
Code-wise, this means marking up around the cursor becomes exponentially
more complex, as a change in one line can affect multiple lines. Solving
it is non-trivial, so the whole document is always marked up.
Marking up the whole document is irrelevant for small to medium
documents, but can incur in a performance penalty for
very large documents (empirical testing: 1M characters takes ~0.15s).
To alleviate this, GLib.idle_add is used to ensure that markup is only
parsed and applied when the UI is idle. Again, small to medium-sized
documents see no difference. For very large documents, markup will be
slightly delayed to allow for a fluid typing experience.
It's important to note that the previous flows frequently used full
document markup: paste, focus mode, and search and replace.
In some extreme cases, doubly parsing (eg. paste + text change).
For very large documents, doing any of these actions would freeze the UI
unconditionally, so in more ways than one this is an upgrade.
Lastly, it's a little overzealous: with over 1M characters the UI itself
struggles more than parsing.
In sum:
* Markup is always applied to the whole document
* The code is simpler
* There is never double work
* Markup is applied when the UI is idle, for a more smooth experience
* Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
|
|
|
self.markup.update_margins_indents()
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
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
|
2019-04-22 21:12:51 +00:00
|
|
|
self.gspell_view.set_inline_spell_checking(not focus_mode)
|
2019-03-31 02:16:18 +00:00
|
|
|
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:
|
2019-04-20 04:09:02 +00:00
|
|
|
self.props.top_margin = 80
|
|
|
|
self.props.bottom_margin = 64
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
def on_button_release_event(self, _widget, _event):
|
|
|
|
if self.focus_mode:
|
Add support for code blocks, improve overall markup handling
This commit adds markup support for code blocks, styling them in a
conservative manner, similar to blockquotes, solely indenting them.
Partially fixes #90
Code-wise, this means marking up around the cursor becomes exponentially
more complex, as a change in one line can affect multiple lines. Solving
it is non-trivial, so the whole document is always marked up.
Marking up the whole document is irrelevant for small to medium
documents, but can incur in a performance penalty for
very large documents (empirical testing: 1M characters takes ~0.15s).
To alleviate this, GLib.idle_add is used to ensure that markup is only
parsed and applied when the UI is idle. Again, small to medium-sized
documents see no difference. For very large documents, markup will be
slightly delayed to allow for a fluid typing experience.
It's important to note that the previous flows frequently used full
document markup: paste, focus mode, and search and replace.
In some extreme cases, doubly parsing (eg. paste + text change).
For very large documents, doing any of these actions would freeze the UI
unconditionally, so in more ways than one this is an upgrade.
Lastly, it's a little overzealous: with over 1M characters the UI itself
struggles more than parsing.
In sum:
* Markup is always applied to the whole document
* The code is simpler
* There is never double work
* Markup is applied when the UI is idle, for a more smooth experience
* Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
|
|
|
self.markup.apply()
|
2019-03-31 02:16:18 +00:00
|
|
|
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."""
|
|
|
|
|
2019-04-20 04:09:02 +00:00
|
|
|
margin = 32
|
2019-03-31 02:50:34 +00:00
|
|
|
scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__)
|
2019-04-16 00:15:11 +00:00
|
|
|
if not scrolled_window:
|
|
|
|
return
|
2019-03-31 02:50:34 +00:00
|
|
|
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:
|
2019-04-11 02:33:31 +00:00
|
|
|
self.scroller = Scroller(scrolled_window, va.props.value, target_pos)
|
2019-03-31 02:50:34 +00:00
|
|
|
self.scroller.start()
|
2019-03-31 02:16:18 +00:00
|
|
|
|
|
|
|
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
|
|
|
|
if mark.get_name() == 'insert':
|
Add support for code blocks, improve overall markup handling
This commit adds markup support for code blocks, styling them in a
conservative manner, similar to blockquotes, solely indenting them.
Partially fixes #90
Code-wise, this means marking up around the cursor becomes exponentially
more complex, as a change in one line can affect multiple lines. Solving
it is non-trivial, so the whole document is always marked up.
Marking up the whole document is irrelevant for small to medium
documents, but can incur in a performance penalty for
very large documents (empirical testing: 1M characters takes ~0.15s).
To alleviate this, GLib.idle_add is used to ensure that markup is only
parsed and applied when the UI is idle. Again, small to medium-sized
documents see no difference. For very large documents, markup will be
slightly delayed to allow for a fluid typing experience.
It's important to note that the previous flows frequently used full
document markup: paste, focus mode, and search and replace.
In some extreme cases, doubly parsing (eg. paste + text change).
For very large documents, doing any of these actions would freeze the UI
unconditionally, so in more ways than one this is an upgrade.
Lastly, it's a little overzealous: with over 1M characters the UI itself
struggles more than parsing.
In sum:
* Markup is always applied to the whole document
* The code is simpler
* There is never double work
* Markup is applied when the UI is idle, for a more smooth experience
* Multi-line formatting is now possible to do reliably
2019-04-10 01:59:00 +00:00
|
|
|
self.markup.apply()
|
2019-03-31 02:54:44 +00:00
|
|
|
if self.focus_mode:
|
|
|
|
self.scroll_to(mark)
|
2019-03-31 02:16:18 +00:00
|
|
|
elif mark.get_name() == 'gtk_drag_target':
|
|
|
|
self.scroll_to(mark)
|
|
|
|
return True
|