forked from Mirrors/apostrophe
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 windowft.font-size^2
parent
16a8ac78db
commit
6688eb259e
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.~~
|
||||
|
||||
|
|
|
@ -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>></code> character and a space.</p>
|
||||
|
|
|
@ -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.~~
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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"!\[(.*?)\]\((.+?)\)")
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue