Refactor textview / textbuffer into separate modules

Changes include:
* Much better encapsulation of textview/textbuffer, with each isolated
responsibility living independently on its own class/file.
* Less code overall
* Various small fixes around the components involved, such as:
  * Indentation of nested lists (fixes #120)
  * Unwanted scroll on select all (ctrl+a)
* Removal of unused code around the components involved
* Fixes for scrollbar location, now at the edge of the window
ft.font-size^2
Gonçalo Silva 2019-03-31 03:16:18 +01:00
parent 16a8ac78db
commit 6688eb259e
22 changed files with 1074 additions and 1323 deletions

View File

@ -8,42 +8,39 @@
bind "<ctl>i" { "insert-italic" () };
bind "<ctl>b" { "insert-bold" () };
bind "<ctl>r" { "insert-hrule" () };
bind "<ctl>u" { "insert-ulistitem" () };
bind "<ctl>h" { "insert-heading" () };
bind "<ctl>u" { "insert-listitem" () };
bind "<ctl>h" { "insert-header" () };
bind "<ctl>z" { "undo" () };
bind "<ctl>y" { "redo" () };
bind "<ctl><shift>d" { "insert-strikeout" () };
bind "<ctl><shift>d" { "insert-strikethrough" () };
/*bind "<ctl>t" { "insert-at-cursor" ('[ ] ') };*/
bind "<ctl><shift>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;
}

View File

@ -186,23 +186,16 @@
<property name="vscroll_policy">natural</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="editor_alignment">
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="vadjustment">adjustment1</property>
<child>
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="vadjustment">adjustment1</property>
<child>
<placeholder/>
</child>
</object>
<placeholder/>
</child>
</object>
</child>

View File

@ -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.~~

View File

@ -47,8 +47,8 @@ is *emphasized with asterisks*.</code></pre>
<pre><code>This is * not emphasized *, and \*neither is this\*.</code></pre>
<p>Because <code>_</code> is sometimes used inside words and identifiers, pandoc does not interpret a <code>_</code> surrounded by alphanumeric characters as an emphasis marker. If you want to emphasize just part of a word, use <code>*</code>:</p>
<pre><code>feas*ible*, not feas*able*.</code></pre>
<h4 id="strikeout">Strikeout</h4>
<p>To strikeout a section of text with a horizontal line, begin and end it with <code>~~</code>. Thus, for example,</p>
<h4 id="strikethrough">Strikethrough</h4>
<p>To strikethrough a section of text with a horizontal line, begin and end it with <code>~~</code>. Thus, for example,</p>
<pre><code>This ~~is deleted text.~~</code></pre>
<h3 id="block-quotations">Block quotations</h3>
<p>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 <code>&gt;</code> character and a space.</p>

View File

@ -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.~~

View File

@ -32,12 +32,4 @@ def main():
# Run the application.
app = application.Application()
# ~ if args:
# ~ for arg in args:
# ~ pass
# ~ else:
# ~ pass
# ~ if options.experimental_features:
# ~ window.use_experimental_features(True)
app.run(sys.argv)

View File

@ -173,8 +173,6 @@ class Application(Gtk.Application):
self.window = window.Window(self)
if self.args:
self.window.load_file(self.args[0])
if self.options.experimental_features:
self.window.use_experimental_features(True)
self.window.present()

View File

@ -35,7 +35,8 @@ from uberwriter.helpers import get_builder
LOGGER = logging.getLogger('uberwriter')
class UberwriterExportDialog:
class Export:
"""
Manages all the export operations and dialogs
"""

View File

@ -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

View File

@ -36,14 +36,14 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
self.hb.props.show_close_button = True
self.hb.get_style_context().add_class("titlebar")
self.hb_revealer = Gtk.Revealer(name='titlebar_revealer')
self.hb_revealer = Gtk.Revealer(name='titlebar-revealer')
self.hb_revealer.add(self.hb)
self.hb_revealer.props.transition_duration = 1000
self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
self.hb_revealer.show()
self.hb_revealer.set_reveal_child(True)
self.hb_container = Gtk.Frame(name='titlebar_container')
self.hb_container = Gtk.Frame(name='titlebar-container')
self.hb_container.set_shadow_type(Gtk.ShadowType.NONE)
self.hb_container.add(self.hb_revealer)
self.hb_container.show()

View File

@ -23,6 +23,8 @@ import shutil
import gi
from gi.overrides.Pango import Pango
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # pylint: disable=E0611
@ -160,6 +162,7 @@ def exist_executable(command):
return shutil.which(command) is not None
def get_descendant(widget, child_name, level, doPrint=False):
if widget is not None:
if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) +
@ -188,3 +191,8 @@ def get_descendant(widget, child_name, level, doPrint=False):
if child is not None:
found = get_descendant(child, child_name, level+1, doPrint) # //search the child
if found: return found
def get_char_width(widget):
return Pango.units_to_double(
widget.get_pango_context().get_metrics().get_approximate_char_width())

View File

@ -14,28 +14,27 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
# END LICENSE
import re
import urllib
from urllib.error import URLError
import webbrowser
import subprocess
import tempfile
import logging
import threading
import re
import subprocess
import telnetlib
import tempfile
import threading
import urllib
import webbrowser
from gettext import gettext as _
from urllib.error import URLError
from urllib.parse import unquote
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
from uberwriter import latex_to_PNG
from uberwriter import latex_to_PNG, text_view_markup_handler
from uberwriter.settings import Settings
from uberwriter.fix_table import FixTable
from uberwriter.markup_buffer import MarkupBuffer
LOGGER = logging.getLogger('uberwriter')
GObject.threads_init() # Still needed?
@ -240,7 +239,7 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
if lexikon_dict:
for entry in lexikon_dict:
vocab_label = Gtk.Label.new(vocab + ' ~ ' + entry['class'])
vocab_label.get_style_context().add_class('lexikon_heading')
vocab_label.get_style_context().add_class('lexikon-heading')
vocab_label.set_halign(Gtk.Align.START)
vocab_label.set_justify(Gtk.Justification.LEFT)
grid.attach(vocab_label, 0, i, 3, 1)
@ -248,14 +247,14 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
for definition in entry['defs']:
i = i + 1
num_label = Gtk.Label.new(definition['num'])
num_label.get_style_context().add_class('lexikon_num')
num_label.get_style_context().add_class('lexikon-num')
num_label.set_justify(Gtk.Justification.RIGHT)
grid.attach(num_label, 0, i, 1, 1)
def_label = Gtk.Label.new(' '.join(definition['description']))
def_label.set_halign(Gtk.Align.START)
def_label.set_justify(Gtk.Justification.LEFT)
def_label.get_style_context().add_class('lexikon_definition')
def_label.get_style_context().add_class('lexikon-definition')
def_label.props.wrap = True
grid.attach(def_label, 1, i, 1, 1)
i = i + 1
@ -264,11 +263,11 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
return None
class InlinePreview():
class InlinePreview:
def __init__(self, view, text_buffer):
self.text_view = view
self.text_buffer = text_buffer
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
self.latex_converter = latex_to_PNG.LatexToPNG()
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
@ -307,7 +306,7 @@ class InlinePreview():
# b.show_all()
# a.show_all()
self.popover = Gtk.Popover.new(lbl)
self.popover.get_style_context().add_class("QuickPreviewPopup")
self.popover.get_style_context().add_class("quick-preview-popup")
self.popover.add(alignment)
# a.add(alignment)
_dismiss, rect = self.popover.get_pointing_to()
@ -363,8 +362,8 @@ class InlinePreview():
text = self.text_buffer.get_text(start_iter, end_iter, False)
math = MarkupBuffer.regex["MATH"]
link = MarkupBuffer.regex["LINK"]
math = text_view_markup_handler.regex["MATH"]
link = text_view_markup_handler.regex["LINK"]
footnote = re.compile(r'\[\^([^\s]+?)\]')
image = re.compile(r"!\[(.*?)\]\((.+?)\)")

View File

@ -1,326 +0,0 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Pango
class MarkupBuffer():
regex = {
"ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_
"STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__
"STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"),
"BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE),
"STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"),
"LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE),
"NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE),
"INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE),
"HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE),
"HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE),
"HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE),
"MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"),
"HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"),
"TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL),
"LINK": re.compile(r"\(http(.+?)\)")
}
def __init__(self, window, text_editor, base_leftmargin):
self.margin_multiplier = 10
self.parent = window
self.text_editor = text_editor
self.text_buffer = text_editor.get_buffer()
# Styles
self.italic = self.text_buffer.create_tag("italic",
style=Pango.Style.ITALIC)
self.emph = self.text_buffer.create_tag("emph",
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.bolditalic = self.text_buffer.create_tag("bolditalic",
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.headline_two = self.text_buffer.create_tag("headline_two",
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.normal_indent = self.text_buffer.create_tag('normal_indent', indent=100)
self.math_text = self.text_buffer.create_tag('math_text')
self.unfocused_text = self.text_buffer.create_tag('graytag',
foreground="gray")
self.underline = self.text_buffer.create_tag("underline",
underline=Pango.Underline.SINGLE)
self.underline.set_property('weight', Pango.Weight.BOLD)
self.strikethrough = self.text_buffer.create_tag("strikethrough",
strikethrough=True)
self.centertext = self.text_buffer.create_tag("centertext",
justification=Gtk.Justification.CENTER)
self.text_buffer.apply_tag(
self.normal_indent,
self.text_buffer.get_start_iter(),
self.text_buffer.get_end_iter()
)
self.rev_leftmargin = []
for i in range(0, 6):
name = "rev_marg_indent_left" + str(i)
self.rev_leftmargin.append(self.text_buffer.create_tag(name))
self.rev_leftmargin[i].set_property("left-margin", 90 - 10 * (i + 1))
self.rev_leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
#self.leftmargin[i].set_property("background", "gray")
self.leftmargin = []
for i in range(0, 6):
name = "marg_indent_left" + str(i)
self.leftmargin.append(self.text_buffer.create_tag(name))
self.leftmargin[i].set_property("left-margin", base_leftmargin + 10 + 10 * (i + 1))
self.leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
self.leftindent = []
for i in range(0, 15):
name = "indent_left" + str(i)
self.leftindent.append(self.text_buffer.create_tag(name))
self.leftindent[i].set_property("indent", - 10 * (i + 1) - 20)
self.table_env = self.text_buffer.create_tag('table_env')
self.table_env.set_property('wrap-mode', Gtk.WrapMode.NONE)
self.table_env.set_property('pixels-above-lines', 0)
self.table_env.set_property('pixels-below-lines', 0)
# Theme
self.text_editor.connect('style-updated', self.apply_current_theme)
self.apply_current_theme()
def apply_current_theme(self, *_):
# Math text color
(found, color) = self.text_editor.get_style_context().lookup_color('math_text_color')
if not found:
(_, color) = self.text_editor.get_style_context().lookup_color('foreground_color')
self.math_text.set_property("foreground", color.to_string())
# Margin
mets = self.text_editor.get_pango_context().get_metrics()
self.set_multiplier(Pango.units_to_double(mets.get_approximate_char_width()) + 1)
def markup_buffer(self, mode=0):
buf = self.text_buffer
# Test for shifting first line
# bbs = buf.get_start_iter()
# bbb = buf.get_iter_at_offset(3)
# buf.apply_tag(self.ftag, bbs, bbb)
# Modes:
# 0 -> start to end
# 1 -> around the cursor
# 2 -> n.d.
if mode == 0:
context_start = buf.get_start_iter()
context_end = buf.get_end_iter()
context_offset = 0
elif mode == 1:
cursor_mark = buf.get_insert()
context_start = buf.get_iter_at_mark(cursor_mark)
context_start.backward_lines(3)
context_end = buf.get_iter_at_mark(cursor_mark)
context_end.forward_lines(2)
context_offset = context_start.get_offset()
text = buf.get_slice(context_start, context_end, False)
self.text_buffer.remove_tag(self.italic, context_start, context_end)
matches = re.finditer(self.regex["ITALIC"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.italic, start_iter, end_iter)
self.text_buffer.remove_tag(self.emph, context_start, context_end)
matches = re.finditer(self.regex["STRONG"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
matches = re.finditer(self.regex["STRONGITALIC"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.bolditalic, start_iter, end_iter)
self.text_buffer.remove_tag(self.strikethrough, context_start, context_end)
matches = re.finditer(self.regex["STRIKETHROUGH"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.strikethrough, start_iter, end_iter)
self.text_buffer.remove_tag(self.math_text, context_start, context_end)
matches = re.finditer(self.regex["MATH"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.math_text, start_iter, end_iter)
for margin in self.rev_leftmargin:
self.text_buffer.remove_tag(margin, context_start, context_end)
matches = re.finditer(self.regex["LIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.rev_leftmargin[0], start_iter, end_iter)
matches = re.finditer(self.regex["NUMERICLIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 1
if index < len(self.rev_leftmargin):
margin = self.rev_leftmargin[index]
self.text_buffer.apply_tag(margin, start_iter, end_iter)
matches = re.finditer(self.regex["BLOCKQUOTE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 2
if index < len(self.leftmargin):
self.text_buffer.apply_tag(self.leftmargin[index], start_iter, end_iter)
for leftindent in self.leftindent:
self.text_buffer.remove_tag(leftindent, context_start, context_end)
matches = re.finditer(self.regex["INDENTEDLIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = (len(match.group(1)) - 1) * 2 + len(match.group(2))
if index < len(self.leftindent):
self.text_buffer.apply_tag(self.leftindent[index], start_iter, end_iter)
matches = re.finditer(self.regex["HEADINDICATOR"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 1
if index < len(self.rev_leftmargin):
margin = self.rev_leftmargin[index]
self.text_buffer.apply_tag(margin, start_iter, end_iter)
matches = re.finditer(self.regex["HORIZONTALRULE"], text)
rulecontext = context_start.copy()
rulecontext.forward_lines(3)
self.text_buffer.remove_tag(self.centertext, rulecontext, context_end)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
start_iter.forward_chars(2)
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.centertext, start_iter, end_iter)
matches = re.finditer(self.regex["HEADLINE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
matches = re.finditer(self.regex["HEADLINE_TWO"], text)
self.text_buffer.remove_tag(self.headline_two, rulecontext, context_end)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.headline_two, start_iter, end_iter)
matches = re.finditer(self.regex["TABLE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.table_env, start_iter, end_iter)
if self.parent.focusmode:
self.focusmode_highlight()
def focusmode_highlight(self):
start_document = self.text_buffer.get_start_iter()
end_document = self.text_buffer.get_end_iter()
self.text_buffer.remove_tag(
self.unfocused_text,
start_document,
end_document)
cursor = self.text_buffer.get_mark("insert")
cursor_iter = self.text_buffer.get_iter_at_mark(cursor)
end_sentence = cursor_iter.copy()
end_sentence.forward_sentence_end()
end_line = cursor_iter.copy()
end_line.forward_to_line_end()
comp = end_line.compare(end_sentence)
# if comp < 0, end_line is BEFORE end_sentence
if comp <= 0:
end_sentence = end_line
start_sentence = cursor_iter.copy()
start_sentence.backward_sentence_start()
# grey out everything before
self.text_buffer.apply_tag(
self.unfocused_text,
self.text_buffer.get_start_iter(), start_sentence)
self.text_buffer.apply_tag(
self.unfocused_text,
end_sentence, self.text_buffer.get_end_iter())
def set_multiplier(self, multiplier):
self.margin_multiplier = multiplier
def recalculate(self, lm):
multiplier = self.margin_multiplier
for i in range(0, 6):
new_margin = (lm - multiplier) - multiplier * (i + 1)
self.rev_leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
self.rev_leftmargin[i].set_property("indent", - multiplier * (i + 1) - multiplier)
for i in range(0, 6):
new_margin = (lm - multiplier) + multiplier + multiplier * (i + 1)
self.leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
self.leftmargin[i].set_property("indent", - (multiplier - 1) * (i + 1) - multiplier)

View File

@ -14,21 +14,26 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import logging
import re
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository import Gdk
# from plugins import plugins
LOGGER = logging.getLogger('uberwriter')
class SearchAndReplace():
class SearchAndReplace:
"""
Adds (regex) search and replace functionality to
uberwriter
"""
def __init__(self, parentwindow):
def __init__(self, parentwindow, textview):
self.parentwindow = parentwindow
self.box = parentwindow.builder.get_object("searchbar_placeholder")
self.box.set_reveal_child(False)
@ -41,8 +46,8 @@ class SearchAndReplace():
self.open_replace_button = parentwindow.builder.get_object("replace")
self.open_replace_button.connect("toggled", self.toggle_replace)
self.textbuffer = parentwindow.text_buffer
self.texteditor = parentwindow.text_editor
self.textview = textview
self.textbuffer = textview.get_buffer()
self.nextbutton = parentwindow.builder.get_object("next_result")
self.prevbutton = parentwindow.builder.get_object("previous_result")
@ -66,7 +71,7 @@ class SearchAndReplace():
self.highlight = self.textbuffer.create_tag('search_highlight',
background="yellow")
self.texteditor.connect("focus-in-event", self.focused_texteditor)
self.textview.connect("focus-in-event", self.focused_texteditor)
def toggle_replace(self, widget, _data=None):
"""toggle the replace box
@ -100,7 +105,6 @@ class SearchAndReplace():
self.hide()
self.open_replace_button.set_active(False)
def search(self, _widget=None, _data=None, scroll=True):
searchtext = self.searchentry.get_text()
buf = self.textbuffer
@ -147,7 +151,7 @@ class SearchAndReplace():
self.active = 0
matchiter = self.matchiters[self.active]
self.texteditor.get_buffer().select_range(matchiter[0], matchiter[1])
self.textview.get_buffer().select_range(matchiter[0], matchiter[1])
# self.texteditor.scroll_to_iter(matchiter[0], 0.0, True, 0.0, 0.5)
@ -157,8 +161,7 @@ class SearchAndReplace():
self.textbuffer.remove_tag(self.highlight,
self.textbuffer.get_start_iter(),
self.textbuffer.get_end_iter())
self.texteditor.grab_focus()
self.textview.grab_focus()
def replace_clicked(self, _widget, _data=None):
self.replace(self.active)
@ -177,5 +180,4 @@ class SearchAndReplace():
active = self.active
self.search(scroll=False)
self.active = active
self.parentwindow.MarkupBuffer.markup_buffer()
self.scrollto(self.active)

View File

@ -1,487 +0,0 @@
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
### END LICENSE
"""Module for the TextView widgth wich encapsulates management of TextBuffer
and TextIter for common functionality, such as cut, copy, paste, undo, redo,
and highlighting of text.
Using
#create the TextEditor and set the text
editor = TextEditor()
editor.text = "Text to add to the editor"
#use cut, works the same for copy, paste, undo, and redo
def __handle_on_cut(self, widget, data=None):
self.editor.cut()
#add string to highlight
self.editor.add_highlight("Ubuntu")
self.editor.add_highlight("Quickly")
#remove highlights
self.editor.clear_highlight("Ubuntu")
self.editor.clear_all_highlight()
Configuring
#Configure as a TextView
self.editor.set_wrap_mode(Gtk.WRAP_CHAR)
#Access the Gtk.TextBuffer if needed
buffer = self.editor.get_buffer()
Extending
A TextEditor is Gtk.TextView
"""
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject
from uberwriter.format_shortcuts import FormatShortcuts
import logging
LOGGER = logging.getLogger('uberwriter')
class UndoableInsert:
"""something that has been inserted into our textbuffer"""
def __init__(self, text_iter, text, length):
self.offset = text_iter.get_offset()
self.text = text
self.length = length
if self.length > 1 or self.text in ("\r", "\n", " "):
self.mergeable = False
else:
self.mergeable = True
class UndoableDelete:
"""something that has ben deleted from our textbuffer"""
def __init__(self, text_buffer, start_iter, end_iter):
self.text = text_buffer.get_text(start_iter, end_iter, False)
self.start = start_iter.get_offset()
self.end = end_iter.get_offset()
# need to find out if backspace or delete key has been used
# so we don't mess up during redo
insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
self.mergeable = not bool(self.end - self.start > 1
or self.text in ("\r", "\n", " "))
class UberwriterTextEditor(Gtk.TextView):
"""TextEditor encapsulates management of TextBuffer and TextIter for
common functionality, such as cut, copy, paste, undo, redo, and
highlighting of text.
"""
__gsignals__ = {
'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
'insert-ulistitem': (GObject.SignalFlags.ACTION, None, ()),
'insert-heading': (GObject.SignalFlags.ACTION, None, ()),
'insert-strikeout': (GObject.SignalFlags.ACTION, None, ()),
'undo': (GObject.SignalFlags.ACTION, None, ()),
'redo': (GObject.SignalFlags.ACTION, None, ())
}
def scroll_to_iter(self, iterable, *args):
self.get_buffer().place_cursor(iterable)
def __init__(self):
"""
Create a TextEditor
"""
Gtk.TextView.__init__(self)
self.undo_max = None
self.insert_event = self.get_buffer().connect("insert-text",
self.on_insert_text)
self.delete_event = self.get_buffer().connect("delete-range",
self.on_delete_range)
display = self.get_display()
self.clipboard = Gtk.Clipboard.get_for_display(display,
Gdk.SELECTION_CLIPBOARD)
self.undo_stack = []
self.redo_stack = []
self.not_undoable_action = False
self.undo_in_progress = False
self.can_delete = True
self.connect('key-press-event', self.on_key_press_event)
self.format_shortcuts = FormatShortcuts(self.get_buffer(), self)
self.connect('insert-italic', self.set_italic)
self.connect('insert-bold', self.set_bold)
self.connect('insert-strikeout', self.set_strikeout)
self.connect('insert-hrule', self.insert_horizontal_rule)
self.connect('insert-ulistitem', self.insert_unordered_list_item)
self.connect('insert-heading', self.insert_heading)
self.connect('redo', self.redo)
self.connect('undo', self.undo)
self.get_style_context().add_class("uberwriter-editor")
@property
def text(self):
"""
text - a string specifying all the text currently
in the TextEditor's buffer.
This property is read/write.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
end_iter = self.get_buffer().get_iter_at_offset(-1)
return self.get_buffer().get_text(start_iter, end_iter, False)
@property
def can_undo(self):
return bool(self.undo_stack)
@property
def can_redo(self):
return bool(self.redo_stack)
@text.setter
def text(self, text):
self.get_buffer().set_text(text)
def append(self, text):
"""append: appends text to the end of the textbuffer.
arguments:
text - a string to add to the buffer. The text will be the
last text in the buffer. The insertion cursor will not be moved.
"""
end_iter = self.get_buffer().get_iter_at_offset(-1)
self.get_buffer().insert(end_iter, text)
def prepend(self, text):
"""prepend: appends text to the start of the textbuffer.
arguments:
text - a string to add to the buffer. The text will be the
first text in the buffer. The insertion cursor will not be moved.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
self.get_buffer().insert(start_iter, text)
insert_iter = self.get_buffer().get_iter_at_offset(len(text)-1)
self.get_buffer().place_cursor(insert_iter)
def cursor_to_end(self):
"""cursor_to_end: moves the insertion curson to the last position
in the buffer.
"""
end_iter = self.get_buffer().get_iter_at_offset(-1)
self.get_buffer().place_cursor(end_iter)
def cursor_to_start(self):
"""cursor_to_start: moves the insertion curson to the first position
in the buffer.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
self.get_buffer().place_cursor(start_iter)
def cut(self, _widget=None, _data=None):
"""cut: cut currently selected text and put it on the clipboard.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().cut_clipboard(self.clipboard, True)
def copy(self, _widget=None, _data=None):
"""copy: copy currently selected text to the clipboard.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().copy_clipboard(self.clipboard)
def paste(self, _widget=None, _data=None):
"""paste: Insert any text currently on the clipboard into the
buffer.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().paste_clipboard(self.clipboard, None, True)
def undo(self, _widget=None, _data=None):
"""undo inserts or deletions
undone actions are being moved to redo stack"""
if not self.undo_stack:
return
self.begin_not_undoable_action()
self.undo_in_progress = True
undo_action = self.undo_stack.pop()
self.redo_stack.append(undo_action)
buf = self.get_buffer()
if isinstance(undo_action, UndoableInsert):
offset = undo_action.offset
start = buf.get_iter_at_offset(offset)
stop = buf.get_iter_at_offset(
offset + undo_action.length
)
buf.place_cursor(start)
buf.delete(start, stop)
else:
start = buf.get_iter_at_offset(undo_action.start)
buf.insert(start, undo_action.text)
if undo_action.delete_key_used:
buf.place_cursor(start)
else:
stop = buf.get_iter_at_offset(undo_action.end)
buf.place_cursor(stop)
self.end_not_undoable_action()
self.undo_in_progress = False
def redo(self, _widget=None, _data=None):
"""redo inserts or deletions
redone actions are moved to undo stack"""
if not self.redo_stack:
return
self.begin_not_undoable_action()
self.undo_in_progress = True
redo_action = self.redo_stack.pop()
self.undo_stack.append(redo_action)
buf = self.get_buffer()
if isinstance(redo_action, UndoableInsert):
start = buf.get_iter_at_offset(redo_action.offset)
buf.insert(start, redo_action.text)
new_cursor_pos = buf.get_iter_at_offset(
redo_action.offset + redo_action.length
)
buf.place_cursor(new_cursor_pos)
else:
start = buf.get_iter_at_offset(redo_action.start)
stop = buf.get_iter_at_offset(redo_action.end)
buf.delete(start, stop)
buf.place_cursor(start)
self.end_not_undoable_action()
self.undo_in_progress = False
def on_insert_text(self, _textbuffer, text_iter, text, _length):
"""
_on_insert: internal function to handle programatically inserted
text. Do not call directly.
"""
def can_be_merged(prev, cur):
"""see if we can merge multiple inserts here
will try to merge words or whitespace
can't merge if prev and cur are not mergeable in the first place
can't merge when user set the input bar somewhere else
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if cur.offset != (prev.offset + prev.length):
return False
if cur.text in whitespace and not prev.text in whitespace:
return False
if prev.text in whitespace and not cur.text in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableInsert(text_iter, text, len(text))
try:
prev_insert = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_insert, UndoableInsert):
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_insert, undo_action):
prev_insert.length += undo_action.length
prev_insert.text += undo_action.text
self.undo_stack.append(prev_insert)
else:
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
def on_delete_range(self, text_buffer, start_iter, end_iter):
"""On delete
"""
def can_be_merged(prev, cur):
"""see if we can merge multiple deletions here
will try to merge words or whitespace
can't merge if prev and cur are not mergeable in the first place
can't merge if delete and backspace key were both used
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if prev.delete_key_used != cur.delete_key_used:
return False
if prev.start != cur.start and prev.start != cur.end:
return False
if cur.text not in whitespace and \
prev.text in whitespace:
return False
if cur.text in whitespace and \
prev.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
try:
prev_delete = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_delete, UndoableDelete):
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_delete, undo_action):
if prev_delete.start == undo_action.start: # delete key used
prev_delete.text += undo_action.text
prev_delete.end += (undo_action.end - undo_action.start)
else: # Backspace used
prev_delete.text = "%s%s" % (undo_action.text,
prev_delete.text)
prev_delete.start = undo_action.start
self.undo_stack.append(prev_delete)
else:
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
def begin_not_undoable_action(self):
"""don't record the next actions
toggles self.not_undoable_action"""
self.not_undoable_action = True
def end_not_undoable_action(self):
"""record next actions
toggles self.not_undoable_action"""
self.not_undoable_action = False
def on_key_press_event(self, widget, event):
if widget == self and not self.can_delete:
return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
else:
return False
def set_italic(self, _widget, _data=None):
"""Ctrl + I"""
self.format_shortcuts.italic()
def set_bold(self, _widget, _data=None):
"""Ctrl + Shift + D"""
self.format_shortcuts.bold()
def set_strikeout(self, _widget, _data=None):
"""Ctrl + B"""
self.format_shortcuts.strikeout()
def insert_horizontal_rule(self, _widget, _data=None):
"""Ctrl + R"""
self.format_shortcuts.rule()
def insert_unordered_list_item(self, _widget, _data=None):
"""Ctrl + U"""
self.format_shortcuts.unordered_list_item()
def insert_ordered_list(self, _widget, _data=None):
"""CTRL + O"""
self.format_shortcuts.ordered_list_item()
def insert_heading(self, _widget, _data=None):
"""CTRL + H"""
self.format_shortcuts.heading()
class TestWindow(Gtk.Window):
"""For testing and demonstrating AsycnTaskProgressBox.
"""
def __init__(self):
# create a window a VBox to hold the controls
Gtk.Window.__init__(self)
self.set_title("TextEditor Test Window")
windowbox = Gtk.VBox(homogeneous=False, spacing=2)
windowbox.show()
self.add(windowbox)
self.editor = UberwriterTextEditor()
self.editor.show()
windowbox.pack_end(self.editor, True, True, 0)
self.set_size_request(200, 200)
self.show()
self.maximize()
self.connect("destroy", Gtk.main_quit)
self.editor.text = "this is some inserted text"
self.editor.append("\nLine 3")
self.editor.prepend("Line1\n")
self.editor.cursor_to_end()
self.editor.cursor_to_start()
self.editor.undo_max = 100
cut_button = Gtk.Button(label="Cut")
cut_button.connect("clicked", self.editor.cut)
cut_button.show()
windowbox.pack_start(cut_button, False, False, 0)
copy_button = Gtk.Button(label="Copy")
copy_button.connect("clicked", self.editor.copy)
copy_button.show()
windowbox.pack_start(copy_button, False, False, 0)
paste_button = Gtk.Button(label="Paste")
paste_button.connect("clicked", self.editor.paste)
paste_button.show()
windowbox.pack_start(paste_button, False, False, 0)
undo_button = Gtk.Button(label="Undo")
undo_button.connect("clicked", self.editor.undo)
undo_button.show()
windowbox.pack_start(undo_button, False, False, 0)
redo_button = Gtk.Button(label="Redo")
redo_button.connect("clicked", self.editor.redo)
redo_button.show()
windowbox.pack_start(redo_button, False, False, 0)
if __name__ == "__main__":
TEST = TestWindow()
Gtk.main()

View File

@ -0,0 +1,175 @@
import gi
from uberwriter.inline_preview import InlinePreview
from uberwriter.text_view_format_inserter import FormatInserter
from uberwriter.text_view_markup_handler import MarkupHandler
from uberwriter.text_view_undo_redo_handler import UndoRedoHandler
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, \
TARGET_TEXT
from uberwriter.text_view_scroller import Scroller
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject
import logging
LOGGER = logging.getLogger('uberwriter')
class TextView(Gtk.TextView):
"""UberwriterTextView encapsulates all the features around the editor.
It combines the following:
- Undo / redo (via TextBufferUndoRedoHandler)
- Format shortcuts (via TextBufferShortcutInserter)
- Markup (via TextBufferMarkupHandler)
- Preview popover (via TextBufferMarkupHandler)
- Drag and drop (via TextViewDragDropHandler)
- Scrolling (via TextViewScroller)
- The various modes supported by UberWriter (eg. Focus Mode, Hemingway Mode)
"""
__gsignals__ = {
'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
'insert-listitem': (GObject.SignalFlags.ACTION, None, ()),
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
'undo': (GObject.SignalFlags.ACTION, None, ()),
'redo': (GObject.SignalFlags.ACTION, None, ())
}
def __init__(self):
super().__init__()
# Appearance
self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
self.set_pixels_above_lines(4)
self.set_pixels_below_lines(4)
self.set_pixels_inside_wrap(8)
self.get_style_context().add_class('uberwriter-editor')
# General behavior
self.get_buffer().connect('changed', self.on_text_changed)
self.connect('size-allocate', self.on_size_allocate)
# Undo / redo
self.undo_redo = UndoRedoHandler()
self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text)
self.get_buffer().connect('delete-range', self.undo_redo.on_delete_range)
self.connect('undo', self.undo_redo.undo)
self.connect('redo', self.undo_redo.redo)
# Format shortcuts
self.shortcut = FormatInserter()
self.connect('insert-italic', self.shortcut.insert_italic)
self.connect('insert-bold', self.shortcut.insert_bold)
self.connect('insert-strikethrough', self.shortcut.insert_strikethrough)
self.connect('insert-hrule', self.shortcut.insert_horizontal_rule)
self.connect('insert-listitem', self.shortcut.insert_list_item)
self.connect('insert-header', self.shortcut.insert_header)
# Markup
self.markup = MarkupHandler(self)
self.connect('style-updated', self.markup.on_style_updated)
self.get_buffer().connect('paste-done', self.on_paste_done)
# Preview popover
self.preview_popover = InlinePreview(self)
# Drag and drop
self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT)
# Scrolling
self.scroller = Scroller()
self.get_buffer().connect('mark-set', self.on_mark_set)
# Focus mode
self.focus_mode = False
self.original_top_margin = self.props.top_margin
self.original_bottom_margin = self.props.bottom_margin
self.connect('button-release-event', self.on_button_release_event)
# Hemingway mode
self.hemingway_mode = False
self.connect('key-press-event', self.on_key_press_event)
def get_text(self):
text_buffer = self.get_buffer()
start_iter = text_buffer.get_start_iter()
end_iter = text_buffer.get_end_iter()
return text_buffer.get_text(start_iter, end_iter, False)
def set_text(self, text):
text_buffer = self.get_buffer()
text_buffer.set_text(text)
self.markup.apply() # TODO
def on_text_changed(self, *_):
self.markup.apply(True)
self.scroll_to()
def on_paste_done(self, *_):
self.markup.apply()
def on_size_allocate(self, *_):
self.update_vertical_margin()
self.markup.update_margins()
def set_focus_mode(self, focus_mode):
"""Toggle focus mode.
When in focus mode, the cursor sits in the middle of the text view,
and the surrounding text is greyed out."""
self.focus_mode = focus_mode
self.update_vertical_margin()
self.markup.apply()
self.scroll_to()
def update_vertical_margin(self):
if self.focus_mode:
height = self.get_allocation().height
self.props.top_margin = height / 2
self.props.bottom_margin = height / 2
else:
self.props.top_margin = self.original_top_margin
self.props.bottom_margin = self.original_bottom_margin
def on_button_release_event(self, _widget, _event):
if self.focus_mode:
self.markup.apply(True)
return False
def set_hemingway_mode(self, hemingway_mode):
"""Toggle hemingway mode.
When in hemingway mode, the backspace and delete keys are ignored."""
self.hemingway_mode = hemingway_mode
def on_key_press_event(self, _widget, event):
if self.hemingway_mode:
return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
else:
return False
def clear(self):
"""Clear text and undo history"""
self.get_buffer().set_text('')
self.undo_redo.clear()
def scroll_to(self, mark=None):
"""Scrolls if needed to ensure mark is visible.
If mark is unspecified, the cursor is used."""
self.scroller.scroll_to(self, mark, self.focus_mode)
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
if mark.get_name() == 'insert':
self.markup.apply(not self.focus_mode)
elif mark.get_name() == 'gtk_drag_target':
self.scroll_to(mark)
return True

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,246 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import gi
from uberwriter import helpers
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Pango
class MarkupHandler:
regex = {
"ITALIC": re.compile(r"(\*|_)(.*?)\1"),
"BOLD": re.compile(r"(\*\*|__)(.*?)\1"),
"BOLDITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"),
"STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"),
"LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-\*\+][ ].+", re.MULTILINE),
"NUMBEREDLIST": re.compile(r"^((?:\t|[ ]{4})*)((?:\d|[a-z])+[\.\)])[ ].+", re.MULTILINE),
"BLOCKQUOTE": re.compile(r"^[ ]{0,3}(?:\>|(?:\> )+).+", re.MULTILINE),
"HEADER": re.compile(r"^[ ]{0,3}(#{1,6}) .+", re.MULTILINE),
"HEADER_UNDER": re.compile(r"^[ ]{0,3}\w.+\n[ ]{0,3}[\=\-]{3,}", re.MULTILINE),
"HORIZONTALRULE": re.compile(r"^\n([ ]{0,3}[\*\-_]{3,}[ ]*)\n", re.MULTILINE),
"TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL),
"MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"),
}
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
# Styles
buffer = self.text_buffer
self.italic = buffer.create_tag('italic',
weight=Pango.Weight.NORMAL,
style=Pango.Style.ITALIC)
self.bold = buffer.create_tag('bold',
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.bolditalic = buffer.create_tag('bolditalic',
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.graytext = buffer.create_tag('graytext', foreground='gray')
self.strikethrough = buffer.create_tag('strikethrough', strikethrough=True)
self.centertext = buffer.create_tag('centertext', justification=Gtk.Justification.CENTER)
self.invisible = buffer.create_tag('invisible', invisible=True)
self.table = buffer.create_tag('table')
self.table.set_property('wrap-mode', Gtk.WrapMode.NONE)
self.table.set_property('pixels-above-lines', 0)
self.table.set_property('pixels-below-lines', 0)
self.mathtext = buffer.create_tag('mathtext')
# Margins
# A default margin is set to allow negative indents for formatting headers, lists, etc
self.baseline_margin = 0
self.margins = {}
self.update_margins()
# Style
self.on_style_updated()
def on_style_updated(self, *_):
(found, color) = self.text_view.get_style_context().lookup_color('math_text_color')
if not found:
(_, color) = self.text_view.get_style_context().lookup_color('foreground_color')
self.mathtext.set_property("foreground", color.to_string())
def apply(self, around_cursor=False):
buffer = self.text_buffer
if around_cursor:
cursor_mark = buffer.get_insert()
start = buffer.get_iter_at_mark(cursor_mark)
start.backward_lines(3)
end = buffer.get_iter_at_mark(cursor_mark)
end.forward_lines(2)
offset = start.get_offset()
else:
start = buffer.get_start_iter()
end = buffer.get_end_iter()
offset = 0
text = buffer.get_slice(start, end, False)
# Remove tags
buffer.remove_tag(self.italic, start, end)
buffer.remove_tag(self.bold, start, end)
buffer.remove_tag(self.bolditalic, start, end)
buffer.remove_tag(self.strikethrough, start, end)
buffer.remove_tag(self.mathtext, start, end)
buffer.remove_tag(self.centertext, start, end)
for tag in self.margins.values():
buffer.remove_tag(tag, start, end)
buffer.remove_tag(self.graytext, start, end)
# Apply "_italic_" tag (italic)
matches = re.finditer(self.regex["ITALIC"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.italic, start_iter, end_iter)
# Apply "**bold**" tag (bold)
matches = re.finditer(self.regex["BOLD"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.bold, start_iter, end_iter)
# Apply "***bolditalic***" tag (bold/italic)
matches = re.finditer(self.regex["BOLDITALIC"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.bolditalic, start_iter, end_iter)
# Apply "~~strikethrough~~" tag (strikethrough)
matches = re.finditer(self.regex["STRIKETHROUGH"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.strikethrough, start_iter, end_iter)
# Apply "* list" tag (offset)
matches = re.finditer(self.regex["LIST"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
# Lists use character+space (eg. "* ")
indent = 2
nest = len(match.group(1).replace(" ", "\t"))
buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter)
# Apply "1. numbered list" tag (offset)
matches = re.finditer(self.regex["NUMBEREDLIST"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
# Numeric lists use numbers/letters+dot/parens+space (eg. "123. ")
indent = len(match.group(2)) + 1
nest = len(match.group(1).replace(" ", "\t"))
buffer.apply_tag(self.get_margin(-indent - 2 * nest), start_iter, end_iter)
# Apply "> blockquote" tag (offset)
matches = re.finditer(self.regex["BLOCKQUOTE"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.get_margin(2), start_iter, end_iter)
# Apply "#" tag (offset + bold)
matches = re.finditer(self.regex["HEADER"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
indent = -len(match.group(1)) - 1
buffer.apply_tag(self.get_margin(indent), start_iter, end_iter)
buffer.apply_tag(self.bold, start_iter, end_iter)
# Apply "======" header underline tag (bold)
matches = re.finditer(self.regex["HEADER_UNDER"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.bold, start_iter, end_iter)
# Apply "---" horizontal rule tag (center)
matches = re.finditer(self.regex["HORIZONTALRULE"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start(1))
end_iter = buffer.get_iter_at_offset(offset + match.end(1))
buffer.apply_tag(self.centertext, start_iter, end_iter)
# Apply "---" table tag (wrap/pixels)
matches = re.finditer(self.regex["TABLE"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.table, start_iter, end_iter)
# Apply "$math$" tag (colorize)
matches = re.finditer(self.regex["MATH"], text)
for match in matches:
start_iter = buffer.get_iter_at_offset(offset + match.start())
end_iter = buffer.get_iter_at_offset(offset + match.end())
buffer.apply_tag(self.mathtext, start_iter, end_iter)
# Apply focus mode tag (grey out before/after current sentence)
if self.text_view.focus_mode:
cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
start_sentence = cursor_iter.copy()
start_sentence.backward_sentence_start()
end_sentence = cursor_iter.copy()
end_sentence.forward_sentence_end()
if start.compare(start_sentence) <= 0:
buffer.apply_tag(self.graytext, start, start_sentence)
if end.compare(end_sentence) >= 0:
buffer.apply_tag(self.graytext, end_sentence, end)
def get_margin(self, level):
if level not in self.margins:
char_width = helpers.get_char_width(self.text_view)
tag = self.text_buffer.create_tag("indent_" + str(level))
tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0))
self.margins[level] = tag
return self.margins[level]
def update_margins(self):
char_width = helpers.get_char_width(self.text_view)
# Adjust tab size, as character width can change
tab_array = Pango.TabArray.new(1, True)
tab_array.set_tab(0, Pango.TabAlign.LEFT, 2 * char_width)
self.text_view.set_tabs(tab_array)
# Adjust baseline margin, as character width can change
# Baseline needs to account for
self.baseline_margin = char_width * 10
self.text_view.set_left_margin(self.baseline_margin)
self.text_view.set_right_margin(self.baseline_margin)
# Adjust left margins, as character width can change
for level, tag in self.margins.items():
tag.set_property("left-margin", max(self.baseline_margin + char_width * level, 0))

View File

@ -0,0 +1,84 @@
from gi.repository import Gtk
class Scroller:
def __init__(self):
super().__init__()
self.smooth_scroll_starttime = 0
self.smooth_scroll_endtime = 0
self.smooth_scroll_acttarget = 0
self.smooth_scroll_data = {
'target_pos': -1,
'source_pos': -1,
'duration': 0
}
self.smooth_scroll_tickid = -1
def scroll_to(self, text_view, mark=None, center=False):
"""Scrolls if needed to ensure mark is visible.
If mark is unspecified, the cursor is used."""
margin = 80
scrolled_window = text_view.get_ancestor(Gtk.ScrolledWindow.__gtype__)
va = scrolled_window.get_vadjustment()
if va.props.page_size < margin * 2:
return
text_buffer = text_view.get_buffer()
if mark:
ins_it = text_buffer.get_iter_at_mark(mark)
else:
ins_it = text_buffer.get_iter_at_mark(text_buffer.get_insert())
loc_rect = text_view.get_iter_location(ins_it)
pos_y = loc_rect.y + loc_rect.height + text_view.props.top_margin
pos = pos_y - va.props.value
target_pos = -1
if center:
if pos != (va.props.page_size * 0.5):
target_pos = pos_y - (va.props.page_size * 0.5)
elif pos > va.props.page_size - margin:
target_pos = pos_y - va.props.page_size + margin
elif pos < margin:
target_pos = pos_y - margin
self.smooth_scroll_data = {
'target_pos': target_pos,
'source_pos': va.props.value,
'duration': 2000
}
if self.smooth_scroll_tickid == -1:
self.smooth_scroll_tickid = scrolled_window.add_tick_callback(self.on_tick)
def on_tick(self, widget, frame_clock, _data=None):
if self.smooth_scroll_data['target_pos'] == -1:
return True
def ease_out_cubic(value):
return pow(value - 1, 3) + 1
now = frame_clock.get_frame_time()
if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']:
self.smooth_scroll_starttime = now
self.smooth_scroll_endtime = now + self.smooth_scroll_data['duration'] * 100
self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos']
if now < self.smooth_scroll_endtime:
time = float(now - self.smooth_scroll_starttime) / float(
self.smooth_scroll_endtime - self.smooth_scroll_starttime)
else:
time = 1
pos = self.smooth_scroll_data['source_pos'] \
+ (time * (self.smooth_scroll_data['target_pos']
- self.smooth_scroll_data['source_pos']))
widget.get_vadjustment().props.value = pos
self.smooth_scroll_data['target_pos'] = -1
return True
time = ease_out_cubic(time)
pos = self.smooth_scroll_data['source_pos'] \
+ (time * (self.smooth_scroll_data['target_pos']
- self.smooth_scroll_data['source_pos']))
widget.get_vadjustment().props.value = pos
return True

View File

@ -0,0 +1,204 @@
class UndoableInsert:
"""Something has been inserted into text_buffer"""
def __init__(self, text_iter, text, length):
self.offset = text_iter.get_offset()
self.text = text
self.length = length
self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " "))
class UndoableDelete:
"""Something has been deleted from text_buffer"""
def __init__(self, text_buffer, start_iter, end_iter):
self.text = text_buffer.get_text(start_iter, end_iter, False)
self.start = start_iter.get_offset()
self.end = end_iter.get_offset()
# Find out if backspace or delete were used to not mess up redo
insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
self.mergeable = not bool(self.end - self.start > 1 or self.text in ("\r", "\n", " "))
class UndoRedoHandler:
"""Manages undo/redo for a given text_buffer.
Methods can be called directly, as well as be used as signal callbacks."""
def __init__(self):
self.undo_stack = []
self.redo_stack = []
self.not_undoable_action = False
self.undo_in_progress = False
def undo(self, text_view, _data=None):
"""Undo insertions or deletions. Undone actions are moved to redo stack.
This method can be registered to a custom undo signal, or used independently."""
if not self.undo_stack:
return
self.__begin_not_undoable_action()
self.undo_in_progress = True
undo_action = self.undo_stack.pop()
self.redo_stack.append(undo_action)
text_buffer = text_view.get_buffer()
if isinstance(undo_action, UndoableInsert):
offset = undo_action.offset
start = text_buffer.get_iter_at_offset(offset)
stop = text_buffer.get_iter_at_offset(
offset + undo_action.length
)
text_buffer.place_cursor(start)
text_buffer.delete(start, stop)
else:
start = text_buffer.get_iter_at_offset(undo_action.start)
text_buffer.insert(start, undo_action.text)
if undo_action.delete_key_used:
text_buffer.place_cursor(start)
else:
stop = text_buffer.get_iter_at_offset(undo_action.end)
text_buffer.place_cursor(stop)
self.__end_not_undoable_action()
self.undo_in_progress = False
def redo(self, text_view, _data):
"""Redo insertions or deletions. Redone actions are moved to undo stack
This method can be registered to a custom redo signal, or used independently."""
if not self.redo_stack:
return
self.__begin_not_undoable_action()
self.undo_in_progress = True
redo_action = self.redo_stack.pop()
self.undo_stack.append(redo_action)
text_buffer = text_view.get_buffer()
if isinstance(redo_action, UndoableInsert):
start = text_buffer.get_iter_at_offset(redo_action.offset)
text_buffer.insert(start, redo_action.text)
new_cursor_pos = text_buffer.get_iter_at_offset(
redo_action.offset + redo_action.length)
text_buffer.place_cursor(new_cursor_pos)
else:
start = text_buffer.get_iter_at_offset(redo_action.start)
stop = text_buffer.get_iter_at_offset(redo_action.end)
text_buffer.delete(start, stop)
text_buffer.place_cursor(start)
self.__end_not_undoable_action()
self.undo_in_progress = False
def clear(self):
self.undo_stack = []
self.redo_stack = []
def on_insert_text(self, _text_buffer, text_iter, text, _length):
"""Registers a text insert. Refer to TextBuffer's "insert-text" signal.
This method must be registered to TextBuffer's "insert-text" signal, or called manually."""
def can_be_merged(prev, cur):
"""Check if multiple insertions can be merged
can't merge if prev and cur are not mergeable in the first place
can't merge when user set the input bar somewhere else
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if cur.offset != (prev.offset + prev.length):
return False
if cur.text in whitespace and prev.text not in whitespace:
return False
if prev.text in whitespace and cur.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableInsert(text_iter, text, len(text))
try:
prev_insert = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_insert, UndoableInsert):
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_insert, undo_action):
prev_insert.length += undo_action.length
prev_insert.text += undo_action.text
self.undo_stack.append(prev_insert)
else:
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
def on_delete_range(self, text_buffer, start_iter, end_iter):
"""Registers a range deletion. Refer to TextBuffer's "delete-range" signal.
This method must be registered to TextBuffer's "delete-range" signal, or called manually."""
def can_be_merged(prev, cur):
"""Check if multiple deletions can be merged
can't merge if prev and cur are not mergeable in the first place
can't merge if delete and backspace key were both used
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if prev.delete_key_used != cur.delete_key_used:
return False
if prev.start != cur.start and prev.start != cur.end:
return False
if cur.text not in whitespace and \
prev.text in whitespace:
return False
if cur.text in whitespace and \
prev.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
try:
prev_delete = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_delete, UndoableDelete):
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_delete, undo_action):
if prev_delete.start == undo_action.start: # delete key used
prev_delete.text += undo_action.text
prev_delete.end += (undo_action.end - undo_action.start)
else: # Backspace used
prev_delete.text = "%s%s" % (undo_action.text,
prev_delete.text)
prev_delete.start = undo_action.start
self.undo_stack.append(prev_delete)
else:
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
def __begin_not_undoable_action(self):
"""Toggle to stop recording actions"""
self.not_undoable_action = True
def __end_not_undoable_action(self):
"""Toggle to start recording actions"""
self.not_undoable_action = False

View File

@ -17,7 +17,6 @@
import codecs
import locale
import logging
import mimetypes
import os
import re
import subprocess
@ -27,14 +26,13 @@ from gettext import gettext as _
import gi
from uberwriter.export_dialog import UberwriterExportDialog
from uberwriter.text_editor import UberwriterTextEditor
from uberwriter.export_dialog import Export
from uberwriter.text_view import TextView
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0') # pylint: disable=wrong-import-position
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
from gi.repository import WebKit2 as WebKit
from gi.repository import Pango # pylint: disable=E0611
import cairo
@ -43,15 +41,12 @@ from uberwriter.theme import Theme
from uberwriter.helpers import get_builder
from uberwriter.gtkspellcheck import SpellChecker
from uberwriter.markup_buffer import MarkupBuffer
from uberwriter.inline_preview import InlinePreview
from uberwriter.sidebar import Sidebar
from uberwriter.search_and_replace import SearchAndReplace
from uberwriter.settings import Settings
from . import headerbars
# Some Globals
# TODO move them somewhere for better
# accesibility from other files
@ -60,8 +55,6 @@ LOGGER = logging.getLogger('uberwriter')
CONFIG_PATH = os.path.expanduser("~/.config/uberwriter/")
# See texteditor_lib.Window.py for more details about how this class works
class Window(Gtk.ApplicationWindow):
__gsignals__ = {
@ -85,15 +78,15 @@ class Window(Gtk.ApplicationWindow):
# Set UI
self.builder = get_builder('UberwriterWindow')
self.add(self.builder.get_object("FullscreenOverlay"))
root = self.builder.get_object("FullscreenOverlay")
root.connect('style-updated', self.apply_current_theme)
self.add(root)
self.set_default_size(850, 500)
self.set_default_size(900, 500)
# preferences
# Preferences
self.settings = Settings.new()
self.set_name('UberwriterWindow')
# Headerbars
self.headerbar = headerbars.MainHeaderbar(app)
self.set_titlebar(self.headerbar.hb_container)
@ -102,20 +95,18 @@ class Window(Gtk.ApplicationWindow):
self.title_end = " UberWriter"
self.set_headerbar_title("New File" + self.title_end)
self.focusmode = False
self.word_count = self.builder.get_object('word_count')
self.char_count = self.builder.get_object('char_count')
# Setup status bar hide after 3 seconds
self.status_bar = self.builder.get_object('status_bar_box')
self.statusbar_revealer = self.builder.get_object('status_bar_revealer')
self.status_bar.get_style_context().add_class('status_bar_box')
self.status_bar.get_style_context().add_class('status-bar-box')
self.status_bar_visible = True
self.was_motion = True
self.buffer_modified_for_status_bar = False
self.timestamp_last_mouse_motion = 0
if self.settings.get_value("poll-motion"):
self.connect("motion-notify-event", self.on_motion_notify)
GObject.timeout_add(3000, self.poll_for_motion)
@ -124,43 +115,20 @@ class Window(Gtk.ApplicationWindow):
self.add_accel_group(self.accel_group)
# Setup text editor
self.text_editor = UberwriterTextEditor()
self.text_editor.set_name('UberwriterEditor')
self.get_style_context().add_class('uberwriter_window')
self.text_view = TextView()
self.text_view.props.halign = Gtk.Align.CENTER
self.text_view.connect('focus-out-event', self.focus_out)
self.text_view.show()
self.text_view.grab_focus()
base_leftmargin = 100
self.text_editor.set_left_margin(base_leftmargin)
self.text_editor.set_left_margin(40)
self.text_editor.set_top_margin(80)
self.text_editor.props.width_request = 600
self.text_editor.props.halign = Gtk.Align.CENTER
self.text_editor.set_vadjustment(self.builder.get_object('vadjustment1'))
self.text_editor.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
self.text_editor.connect('focus-out-event', self.focus_out)
self.text_editor.connect('style-updated', self.apply_current_theme)
self.text_editor.set_top_margin(80)
self.text_editor.set_bottom_margin(16)
self.text_editor.set_pixels_above_lines(4)
self.text_editor.set_pixels_below_lines(4)
self.text_editor.set_pixels_inside_wrap(8)
tab_array = Pango.TabArray.new(1, True)
tab_array.set_tab(0, Pango.TabAlign.LEFT, 20)
self.text_editor.set_tabs(tab_array)
self.text_editor.show()
self.text_editor.grab_focus()
self.text_view.get_buffer().connect('changed', self.on_text_changed)
# Setup preview webview
self.preview_webview = None
self.editor_alignment = self.builder.get_object('editor_alignment')
self.scrolled_window = self.builder.get_object('editor_scrolledwindow')
self.scrolled_window.props.width_request = 600
self.scrolled_window.add(self.text_editor)
self.alignment_padding = 40
self.scrolled_window.get_style_context().add_class('uberwriter-scrolled-window')
self.scrolled_window.add(self.text_view)
self.editor_viewport = self.builder.get_object('editor_viewport')
# some people seems to have performance problems with the overlay.
@ -169,68 +137,9 @@ class Window(Gtk.ApplicationWindow):
if self.settings.get_value("gradient-overlay"):
self.overlay = self.scrolled_window.connect_after("draw", self.draw_gradient)
self.smooth_scroll_starttime = 0
self.smooth_scroll_endtime = 0
self.smooth_scroll_acttarget = 0
self.smooth_scroll_data = {
'target_pos': -1,
'source_pos': -1,
'duration': 0
}
self.smooth_scroll_tickid = -1
self.text_buffer = self.text_editor.get_buffer()
self.text_buffer.set_text('')
# Init Window height for top/bottom padding
self.window_height = self.get_size()[1]
self.text_change_event = self.text_buffer.connect(
'changed', self.text_changed)
# Init file name with None
self.set_filename()
# Markup and Shortcuts for the TextBuffer
self.markup_buffer = MarkupBuffer(
self, self.text_editor, base_leftmargin)
self.markup_buffer.markup_buffer()
# Scrolling -> Dark or not?
self.textchange = False
self.scroll_count = 0
self.timestamp_last_mouse_motion = 0
self.text_buffer.connect_after('mark-set', self.mark_set)
# Drag and drop
# self.TextEditor.drag_dest_unset()
# self.TextEditor.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self.target_list = Gtk.TargetList.new([])
self.target_list.add_uri_targets(1)
self.target_list.add_text_targets(2)
self.text_editor.drag_dest_set_target_list(self.target_list)
self.text_editor.connect_after(
'drag-data-received', self.on_drag_data_received)
def on_drop(_widget, *_args):
print("drop")
self.text_editor.connect('drag-drop', on_drop)
self.text_buffer.connect('paste-done', self.paste_done)
# self.connect('key-press-event', self.alt_mod)
# Events for Typewriter mode
# Setting up inline preview
self.inline_preview = InlinePreview(
self.text_editor, self.text_buffer)
# Vertical scrolling
self.vadjustment = self.scrolled_window.get_vadjustment()
self.vadjustment.connect('value-changed', self.scrolled)
# Setting up spellcheck
self.auto_correct = None
self.toggle_spellcheck(self.settings.get_value("spellcheck"))
@ -248,7 +157,7 @@ class Window(Gtk.ApplicationWindow):
# Search and replace initialization
# Same interface as Sidebar ;)
###
self.searchreplace = SearchAndReplace(self)
self.searchreplace = SearchAndReplace(self, self.text_view)
# Window resize
self.window_resize(self)
@ -257,6 +166,7 @@ class Window(Gtk.ApplicationWindow):
# Set current theme
self.apply_current_theme()
self.get_style_context().add_class('uberwriter-window')
def apply_current_theme(self, *_):
"""Adjusts the window, CSD and preview for the current theme.
@ -284,58 +194,14 @@ class Window(Gtk.ApplicationWindow):
# Redraw contents of window
self.queue_draw()
def scrolled(self, widget):
"""if window scrolled + focusmode make font black again"""
# if self.focusmode:
# if self.textchange == False:
# if self.scroll_count >= 4:
# self.TextBuffer.apply_tag(
# self.MarkupBuffer.blackfont,
# self.TextBuffer.get_start_iter(),
# self.TextBuffer.get_end_iter())
# else:
# self.scroll_count += 1
# else:
# self.scroll_count = 0
# self.textchange = False
def paste_done(self, *_):
self.markup_buffer.markup_buffer(0)
def init_typewriter(self):
"""put the cursor at the center of the screen by setting top and
bottom margins to height/2
"""
editor_height = self.text_editor.get_allocation().height
self.text_editor.props.top_margin = editor_height / 2
self.text_editor.props.bottom_margin = editor_height / 2
def remove_typewriter(self):
"""set margins to default values
"""
self.text_editor.props.top_margin = 80
self.text_editor.props.bottom_margin = 16
self.text_change_event = self.text_buffer.connect(
'changed', self.text_changed)
def get_text(self):
"""get text from self.text_buffer
"""
start_iter = self.text_buffer.get_start_iter()
end_iter = self.text_buffer.get_end_iter()
return self.text_buffer.get_text(start_iter, end_iter, False)
def update_line_and_char_count(self):
"""it... it updates line and characters count
"""
if self.status_bar_visible is False:
return
self.char_count.set_text(str(self.text_buffer.get_char_count()))
text = self.get_text()
text = self.text_view.get_text()
self.char_count.set_text(str(len(text)))
words = re.split(self.WORDCOUNT, text)
length = len(words)
# Last word a "space"
@ -348,12 +214,7 @@ class Window(Gtk.ApplicationWindow):
length = 0
self.word_count.set_text(str(length))
def mark_set(self, _buffer, _location, mark, _data=None):
if mark.get_name() in ['insert', 'gtk_drag_target']:
self.check_scroll(mark)
return True
def text_changed(self, *_args):
def on_text_changed(self, *_args):
"""called when the text changes, sets the self.did_change to true and
updates the title and the counters to reflect that
"""
@ -363,12 +224,8 @@ class Window(Gtk.ApplicationWindow):
title = self.get_title()
self.set_headerbar_title("* " + title)
self.markup_buffer.markup_buffer(1)
self.textchange = True
self.buffer_modified_for_status_bar = True
self.update_line_and_char_count()
self.check_scroll(self.text_buffer.get_insert())
def set_fullscreen(self, state):
"""Puts the application in fullscreen mode and show/hides
@ -386,178 +243,52 @@ class Window(Gtk.ApplicationWindow):
self.unfullscreen()
self.fs_headerbar.events.hide()
self.text_editor.grab_focus()
self.text_view.grab_focus()
def set_focus_mode(self, state):
"""toggle focusmode
"""
if state.get_boolean():
self.init_typewriter()
self.markup_buffer.focusmode_highlight()
self.focusmode = True
self.text_editor.grab_focus()
self.check_scroll(self.text_buffer.get_insert())
if self.spell_checker:
self.spell_checker._misspelled.set_property('underline', 0)
self.click_event = self.text_editor.connect("button-release-event",
self.on_focusmode_click)
else:
self.remove_typewriter()
self.focusmode = False
self.text_buffer.remove_tag(self.markup_buffer.unfocused_text,
self.text_buffer.get_start_iter(),
self.text_buffer.get_end_iter())
self.text_buffer.remove_tag(self.markup_buffer.blackfont,
self.text_buffer.get_start_iter(),
self.text_buffer.get_end_iter())
self.markup_buffer.markup_buffer(1)
self.text_editor.grab_focus()
self.update_line_and_char_count()
self.check_scroll()
if self.spell_checker:
self.spell_checker._misspelled.set_property('underline', 4)
_click_event = self.text_editor.disconnect(self.click_event)
focus_mode = state.get_boolean()
self.text_view.set_focus_mode(focus_mode)
if self.spell_checker:
self.spell_checker._misspelled.set_property('underline', 0 if focus_mode else 4)
self.text_view.grab_focus()
def set_hemingway_mode(self, state):
"""toggle hemingwaymode
"""
self.text_editor.can_delete = not state.get_boolean()
self.text_editor.grab_focus()
def on_focusmode_click(self, *_args):
"""call MarkupBuffer to mark as bold the line where the cursor is
"""
self.text_view.set_hemingway_mode(state.get_boolean())
self.text_view.grab_focus()
self.markup_buffer.markup_buffer(1)
def scroll_smoothly(self, widget, frame_clock, _data=None):
if self.smooth_scroll_data['target_pos'] == -1:
return True
def ease_out_cubic(time):
time = time - 1
return pow(time, 3) + 1
now = frame_clock.get_frame_time()
if self.smooth_scroll_acttarget != self.smooth_scroll_data['target_pos']:
self.smooth_scroll_starttime = now
self.smooth_scroll_endtime = now + \
self.smooth_scroll_data['duration'] * 100
self.smooth_scroll_acttarget = self.smooth_scroll_data['target_pos']
if now < self.smooth_scroll_endtime:
time = float(now - self.smooth_scroll_starttime) / float(
self.smooth_scroll_endtime - self.smooth_scroll_starttime)
else:
time = 1
pos = self.smooth_scroll_data['source_pos'] \
+ (time * (self.smooth_scroll_data['target_pos']
- self.smooth_scroll_data['source_pos']))
widget.get_vadjustment().props.value = pos
self.smooth_scroll_data['target_pos'] = -1
return True
time = ease_out_cubic(time)
pos = self.smooth_scroll_data['source_pos'] \
+ (time * (self.smooth_scroll_data['target_pos']
- self.smooth_scroll_data['source_pos']))
widget.get_vadjustment().props.value = pos
return True # continue ticking
def check_scroll(self, mark=None):
gradient_offset = 80
buf = self.text_editor.get_buffer()
if mark:
ins_it = buf.get_iter_at_mark(mark)
else:
ins_it = buf.get_iter_at_mark(buf.get_insert())
loc_rect = self.text_editor.get_iter_location(ins_it)
# alignment offset added from top
pos_y = loc_rect.y + loc_rect.height + self.text_editor.props.top_margin # pylint: disable=no-member
ha = self.scrolled_window.get_vadjustment()
if ha.props.page_size < gradient_offset:
return
pos = pos_y - ha.props.value
# print("pos: %i, pos_y %i, page_sz: %i, val: %i" % (pos, pos_y, ha.props.page_size
# - gradient_offset, ha.props.value))
# global t, amount, initvadjustment
target_pos = -1
if self.focusmode:
# print("pos: %i > %i" % (pos, ha.props.page_size * 0.5))
if pos != (ha.props.page_size * 0.5):
target_pos = pos_y - (ha.props.page_size * 0.5)
elif pos > ha.props.page_size - gradient_offset - 60:
target_pos = pos_y - ha.props.page_size + gradient_offset + 40
elif pos < gradient_offset:
target_pos = pos_y - gradient_offset
self.smooth_scroll_data = {
'target_pos': target_pos,
'source_pos': ha.props.value,
'duration': 2000
}
if self.smooth_scroll_tickid == -1:
self.smooth_scroll_tickid = self.scrolled_window.add_tick_callback(
self.scroll_smoothly)
def window_resize(self, widget, _data=None):
def window_resize(self, window, _data=None):
"""set paddings dependant of the window size
"""
# To calc padding top / bottom
self.window_height = widget.get_allocation().height
w_width = widget.get_allocation().width
# Calculate left / right margin
# Adjust text editor width depending on window width, so that:
# - The number of characters per line is adequate (http://webtypography.net/2.1.2)
# - The number of characters stays constant while resizing the window / font
# - There is enough text margin for MarkupBuffer to apply indents / negative margins
w_width = window.get_allocation().width
if w_width < 900:
width_request = 600
self.markup_buffer.set_multiplier(8)
self.current_font_size = 12
self.alignment_padding = 30
lm = 7 * 8
self.get_style_context().remove_class("medium")
self.get_style_context().remove_class("large")
width_request = 700 # ~66 characters
self.get_style_context().add_class("small")
self.get_style_context().remove_class("large")
elif w_width < 1400:
width_request = 800
self.markup_buffer.set_multiplier(10)
self.current_font_size = 15
self.alignment_padding = 40
lm = 7 * 10
elif w_width < 1200:
width_request = 870 # ~66 characters
self.get_style_context().remove_class("small")
self.get_style_context().remove_class("large")
self.get_style_context().add_class("medium")
else:
width_request = 1000
self.markup_buffer.set_multiplier(13)
self.current_font_size = 17
self.alignment_padding = 60
lm = 7 * 13
self.get_style_context().remove_class("medium")
width_request = 830 # ~66 characters
self.get_style_context().remove_class("small")
self.get_style_context().add_class("large")
self.editor_alignment.props.margin_bottom = 0
self.editor_alignment.props.margin_top = 0
self.text_editor.set_left_margin(lm)
self.text_editor.set_right_margin(lm)
self.markup_buffer.recalculate(lm)
if self.focusmode:
self.remove_typewriter()
self.init_typewriter()
if self.text_editor.props.width_request != width_request: # pylint: disable=no-member
self.text_editor.props.width_request = width_request
if self.text_view.props.width_request != width_request:
self.text_view.props.width_request = width_request
self.scrolled_window.props.width_request = width_request
alloc = self.text_editor.get_allocation()
alloc.width = width_request
self.text_editor.size_allocate(alloc)
# TODO: refactorizable
def save_document(self, _widget=None, _data=None):
@ -569,7 +300,7 @@ class Window(Gtk.ApplicationWindow):
LOGGER.info("saving")
filename = self.filename
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.get_text())
file_to_save.write(self.text_view.get_text())
file_to_save.close()
if self.did_change:
self.did_change = False
@ -603,7 +334,7 @@ class Window(Gtk.ApplicationWindow):
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.get_text())
file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
@ -644,7 +375,7 @@ class Window(Gtk.ApplicationWindow):
pass
file_to_save = codecs.open(filename, encoding="utf-8", mode='w')
file_to_save.write(self.get_text())
file_to_save.write(self.text_view.get_text())
file_to_save.close()
self.set_filename(filename)
@ -671,7 +402,7 @@ class Window(Gtk.ApplicationWindow):
proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
text = bytes(self.get_text(), "utf-8")
text = bytes(self.text_view.get_text(), "utf-8")
output = proc.communicate(text)[0]
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
@ -711,14 +442,14 @@ class Window(Gtk.ApplicationWindow):
"""Show dialog to prevent loss of unsaved changes
"""
if self.did_change and self.get_text():
if self.did_change and self.text_view.get_text():
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.NONE,
_("You have not saved your changes.")
)
dialog.add_button(_("Close without Saving"), Gtk.ResponseType.NO)
dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO)
dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
dialog.add_button(_("Save now"), Gtk.ResponseType.YES)
# dialog.set_default_size(200, 60)
@ -745,9 +476,7 @@ class Window(Gtk.ApplicationWindow):
if self.check_change() == Gtk.ResponseType.CANCEL:
return
self.text_buffer.set_text('')
self.text_editor.undos = []
self.text_editor.redos = []
self.text_view.clear()
self.did_change = False
self.set_filename()
@ -771,7 +500,7 @@ class Window(Gtk.ApplicationWindow):
except:
try:
self.spell_checker = SpellChecker(
self.text_editor, locale.getdefaultlocale()[0],
self.text_view, locale.getdefaultlocale()[0],
collapse=False)
if self.auto_correct:
self.auto_correct.set_language(self.spell_checker.language)
@ -787,7 +516,7 @@ class Window(Gtk.ApplicationWindow):
_("You can not enable the Spell Checker.")
)
dialog.format_secondary_text(
_("Please install 'hunspell' or 'aspell' dictionarys"
_("Please install 'hunspell' or 'aspell' dictionaries"
+ " for your language from the software center."))
_response = dialog.run()
return
@ -799,46 +528,6 @@ class Window(Gtk.ApplicationWindow):
pass
return
def on_drag_data_received(self, _widget, drag_context, _x, _y,
data, info, time):
"""Handle drag and drop events"""
if info == 1:
# uri target
uris = data.get_uris()
for uri in uris:
uri = urllib.parse.unquote_plus(uri)
mime = mimetypes.guess_type(uri)
if mime[0] is not None and mime[0].startswith('image'):
if uri.startswith("file://"):
uri = uri[7:]
text = "![Insert image title here](%s)" % uri
limit_left = 2
limit_right = 23
else:
text = "[Insert link title here](%s)" % uri
limit_left = 1
limit_right = 22
self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark(
self.text_buffer.get_mark('gtk_drag_target')))
self.text_buffer.insert_at_cursor(text)
insert_mark = self.text_buffer.get_insert()
selection_bound = self.text_buffer.get_selection_bound()
cursor_iter = self.text_buffer.get_iter_at_mark(insert_mark)
cursor_iter.backward_chars(len(text) - limit_left)
self.text_buffer.move_mark(insert_mark, cursor_iter)
cursor_iter.forward_chars(limit_right)
self.text_buffer.move_mark(selection_bound, cursor_iter)
elif info == 2:
# Text target
self.text_buffer.place_cursor(self.text_buffer.get_iter_at_mark(
self.text_buffer.get_mark('gtk_drag_target')))
self.text_buffer.insert_at_cursor(data.get_text())
Gtk.drag_finish(drag_context, True, True, time)
self.present()
return False
def toggle_preview(self, state):
"""Toggle the preview mode
@ -855,8 +544,8 @@ class Window(Gtk.ApplicationWindow):
def show_text_editor(self):
self.scrolled_window.remove(self.scrolled_window.get_child())
self.scrolled_window.add(self.text_editor)
self.text_editor.show()
self.scrolled_window.add(self.text_view)
self.text_view.show()
self.preview_webview.destroy()
self.preview_webview = None
self.queue_draw()
@ -868,12 +557,6 @@ class Window(Gtk.ApplicationWindow):
self.preview_webview.show()
self.queue_draw()
else:
# Insert a tag with ID to scroll to
# self.TextBuffer.insert_at_cursor('<span id="scroll_mark"></span>')
# TODO
# Find a way to find the next header, scroll to the next header.
# TODO: provide a local version of mathjax
# We need to convert relative routes to absolute ones
# For that first we need to know if the file is saved:
if self.filename:
@ -899,19 +582,13 @@ class Window(Gtk.ApplicationWindow):
proc = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
text = bytes(self.get_text(), "utf-8")
text = bytes(self.text_view.get_text(), "utf-8")
output = proc.communicate(text)[0]
if self.preview_webview is None:
self.preview_webview = WebKit.WebView()
self.preview_webview.get_settings().set_allow_universal_access_from_file_urls(True)
# Delete the cursor-scroll mark again
# cursor_iter = self.TextBuffer.get_iter_at_mark(self.TextBuffer.get_insert())
# begin_del = cursor_iter.copy()
# begin_del.backward_chars(30)
# self.TextBuffer.delete(begin_del, cursor_iter)
# Show preview once the load is finished
self.preview_webview.connect("load-changed", self.on_preview_load_change)
@ -930,19 +607,14 @@ class Window(Gtk.ApplicationWindow):
if filename.startswith('file://'):
filename = filename[7:]
filename = urllib.parse.unquote_plus(filename)
self.text_view.clear()
try:
if not os.path.exists(filename):
self.text_buffer.set_text("")
else:
if os.path.exists(filename):
current_file = codecs.open(filename, encoding="utf-8", mode='r')
self.text_buffer.set_text(current_file.read())
self.text_view.set_text(current_file.read())
current_file.close()
self.markup_buffer.markup_buffer(0)
self.set_headerbar_title(
os.path.basename(filename) + self.title_end)
self.text_editor.undo_stack = []
self.text_editor.redo_stack = []
self.set_headerbar_title(os.path.basename(filename) + self.title_end)
self.set_filename(filename)
except Exception:
@ -969,12 +641,12 @@ class Window(Gtk.ApplicationWindow):
"""open the export and advanced export dialog
"""
self.export = UberwriterExportDialog(self.filename)
self.export = Export(self.filename)
self.export.dialog.set_transient_for(self)
response = self.export.dialog.run()
if response == 1:
self.export.export(bytes(self.get_text(), "utf-8"))
self.export.export(bytes(self.text_view.get_text(), "utf-8"))
self.export.dialog.destroy()
@ -998,7 +670,7 @@ class Window(Gtk.ApplicationWindow):
if (self.was_motion is False
and self.status_bar_visible
and self.buffer_modified_for_status_bar
and self.text_editor.props.has_focus): # pylint: disable=no-member
and self.text_view.props.has_focus): # pylint: disable=no-member
# self.status_bar.set_state_flags(Gtk.StateFlags.INSENSITIVE, True)
self.statusbar_revealer.set_reveal_child(False)
self.headerbar.hb_revealer.set_reveal_child(False)
@ -1071,24 +743,6 @@ class Window(Gtk.ApplicationWindow):
cr.set_source(lg_btm)
cr.fill()
def use_experimental_features(self, _val):
"""use experimental features
"""
pass
# try:
# self.auto_correct = AutoCorrect(
# self.text_editor, self.text_buffer)
# except:
# LOGGER.debug("Couldn't install autocorrect.")
# self.plugins = [BibTex(self)]
# def alt_mod(self, _widget, event, _data=None):
# # TODO: Click and open when alt is pressed
# if event.state & Gdk.ModifierType.MOD2_MASK:
# LOGGER.info("Alt pressed")
# return
def on_delete_called(self, _widget, _data=None):
"""Called when the TexteditorWindow is closed.
"""