forked from Mirrors/apostrophe
538 lines
19 KiB
Python
538 lines
19 KiB
Python
# -*- 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 os
|
|
import re
|
|
import http.client
|
|
import urllib
|
|
from urllib.error import URLError, HTTPError
|
|
import webbrowser
|
|
import gettext
|
|
import subprocess
|
|
import tempfile
|
|
|
|
import threading
|
|
from pprint import pprint
|
|
|
|
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
|
|
from uberwriter_lib import LatexToPNG
|
|
from .Settings import Settings
|
|
|
|
from .FixTable import FixTable
|
|
|
|
from .MarkupBuffer import MarkupBuffer
|
|
|
|
from gettext import gettext as _
|
|
|
|
import logging
|
|
logger = logging.getLogger('uberwriter')
|
|
|
|
GObject.threads_init() # Still needed?
|
|
|
|
# TODO:
|
|
# - Don't insert a span with id, it breaks the text to often
|
|
# Would be better to search for the nearest title and generate
|
|
# A jumping URL from that (for preview)
|
|
# Also, after going to preview, set cursor back to where it was
|
|
|
|
import telnetlib
|
|
|
|
import subprocess
|
|
|
|
class DictAccessor(object):
|
|
|
|
def __init__( self, host='pan.alephnull.com', port=2628, timeout=60 ):
|
|
self.tn = telnetlib.Telnet( host, port )
|
|
self.timeout = timeout
|
|
self.login_response = self.tn.expect( [ self.reEndResponse ], self.timeout )[ 2 ]
|
|
|
|
|
|
def getOnline(self, word):
|
|
p = subprocess.Popen(['dict', '-d', 'wn', word], stdout=subprocess.PIPE)
|
|
return p.communicate()[0]
|
|
|
|
def runCommand( self, cmd ):
|
|
self.tn.write( cmd.encode('utf-8') + b'\r\n' )
|
|
return self.tn.expect( [ self.reEndResponse ], self.timeout )[ 2 ]
|
|
|
|
def getMatches( self, database, strategy, word ):
|
|
if database in [ '', 'all' ]:
|
|
d = '*'
|
|
else:
|
|
d = database
|
|
if strategy in [ '', 'default' ]:
|
|
s = '.'
|
|
else:
|
|
s = strategy
|
|
w = word.replace( '"', r'\"' )
|
|
tsplit = self.runCommand( 'MATCH %s %s "%s"' % ( d, s, w ) ).splitlines( )
|
|
mlist = list( )
|
|
if tsplit[ -1 ].startswith( b'250 ok' ) and tsplit[ 0 ].startswith( b'1' ):
|
|
mlines = tsplit[ 1:-2 ]
|
|
for line in mlines:
|
|
lsplit = line.strip( ).split( )
|
|
db = lsplit[ 0 ]
|
|
word = unquote( ' '.join( lsplit[ 1: ] ) )
|
|
mlist.append( ( db, word ) )
|
|
return mlist
|
|
|
|
reEndResponse = re.compile( b'^[2-5][0-58][0-9] .*\r\n$', re.DOTALL + re.MULTILINE )
|
|
reDefinition = re.compile( b'^151(.*?)^\.', re.DOTALL + re.MULTILINE )
|
|
|
|
def getDefinition( self, database, word ):
|
|
if database in [ '', 'all' ]:
|
|
d = '*'
|
|
else:
|
|
d = database
|
|
w = word.replace( '"', r'\"' )
|
|
dsplit = self.runCommand( 'DEFINE %s "%s"' % ( d, w ) ).splitlines( True )
|
|
# dsplit = self.getOnline(word).splitlines()
|
|
|
|
dlist = list( )
|
|
if dsplit[ -1 ].startswith( b'250 ok' ) and dsplit[ 0 ].startswith( b'1' ):
|
|
dlines = dsplit[ 1:-1 ]
|
|
dtext = b''.join( dlines )
|
|
dlist = self.reDefinition.findall( dtext )
|
|
# print(dlist)
|
|
dlist = [ dtext ]
|
|
# dlist = dsplit # not using the localhost telnet connection
|
|
return dlist
|
|
|
|
def close( self ):
|
|
t = self.runCommand( 'QUIT' )
|
|
self.tn.close( )
|
|
return t
|
|
|
|
def parse_wordnet(self, response):
|
|
# consisting of group (n,v,adj,adv)
|
|
# number, description, examples, synonyms, antonyms
|
|
capture = {}
|
|
lines = response.splitlines()
|
|
lines = lines[2:]
|
|
lines = ' '.join(lines)
|
|
lines = re.sub('\s+', ' ', lines).strip()
|
|
lines = re.split(r'( adv | adj | n | v |^adv |^adj |^n |^v )', lines)
|
|
res = []
|
|
act_res = {'defs': [], 'class': 'none', 'num': 'None'}
|
|
for l in lines:
|
|
l = l.strip()
|
|
if len(l) == 0:
|
|
continue
|
|
if l in ['adv', 'adj', 'n', 'v']:
|
|
if act_res:
|
|
res.append(act_res.copy())
|
|
act_res = {}
|
|
act_res['defs'] = []
|
|
act_res['class'] = l
|
|
else:
|
|
ll = re.split('(?: |^)(\d): ', l)
|
|
act_def = {}
|
|
for lll in ll:
|
|
if lll.strip().isdigit() or not lll.strip():
|
|
if 'description' in act_def and act_def['description']:
|
|
act_res['defs'].append(act_def.copy())
|
|
act_def = {'num': lll}
|
|
continue
|
|
a = re.findall(r'(\[(syn|ant): (.+?)\] ??)+', lll)
|
|
for n in a:
|
|
if n[1] == 'syn':
|
|
act_def['syn'] = re.findall(r'\{(.*?)\}.*?', n[2])
|
|
else:
|
|
act_def['ant'] = re.findall(r'\{(.*?)\}.*?', n[2])
|
|
tbr = re.search(r'\[.+\]', lll)
|
|
if tbr:
|
|
lll = lll[:tbr.start()]
|
|
lll = lll.split(';')
|
|
act_def['examples'] = []
|
|
act_def['description'] = []
|
|
for llll in lll:
|
|
llll = llll.strip()
|
|
if(llll.strip().startswith('"')):
|
|
act_def['examples'].append(llll)
|
|
else:
|
|
act_def['description'].append(llll)
|
|
|
|
if act_def and 'description' in act_def:
|
|
act_res['defs'].append(act_def.copy())
|
|
|
|
# pprint(act_res)
|
|
res.append(act_res.copy())
|
|
return res
|
|
|
|
def check_url(url, item, spinner):
|
|
logger.debug("thread started, checking url")
|
|
error = False
|
|
try:
|
|
response = urllib.request.urlopen(url)
|
|
except URLError as e:
|
|
error = True
|
|
text = "Error! Reason: %s" % e.reason
|
|
|
|
if not error:
|
|
if (response.code / 100) >= 4:
|
|
logger.debug("Website not available")
|
|
text = _("Website is not available")
|
|
else:
|
|
text = _("Website is available")
|
|
logger.debug("Response: %s" % text)
|
|
spinner.destroy()
|
|
item.set_label(text)
|
|
|
|
def get_dictionary(term):
|
|
da = DictAccessor()
|
|
output = da.getDefinition('wn', term)
|
|
if(len(output)):
|
|
output = output[0]
|
|
else:
|
|
return None
|
|
return da.parse_wordnet(output.decode(encoding='UTF-8'))
|
|
|
|
def get_web_thumbnail(url, item, spinner):
|
|
logger.debug("thread started, generating thumb")
|
|
|
|
# error = False
|
|
|
|
# gnome-web-photo only understands http urls
|
|
if url.startswith("www"):
|
|
url = "http://" + url
|
|
|
|
filename = tempfile.mktemp(suffix='.png')
|
|
thumb_size = '256' # size can only be 32, 64, 96, 128 or 256!
|
|
args = ['gnome-web-photo', '--mode=thumbnail', '-s', thumb_size, url, filename]
|
|
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
output = p.communicate()[0]
|
|
|
|
image = Gtk.Image.new_from_file(filename)
|
|
image.show()
|
|
|
|
# if not error:
|
|
# if (response.code / 100) >= 4:
|
|
# logger.debug("Website not available")
|
|
# text = _("Website is not available")
|
|
# else:
|
|
# text = _("Website is available")
|
|
|
|
spinner.destroy()
|
|
item.add(image)
|
|
item.show()
|
|
|
|
def fill_lexikon_bubble(vocab, lexikon_dict):
|
|
grid = Gtk.Grid.new()
|
|
i = 0
|
|
grid.set_name('LexikonBubble')
|
|
grid.set_row_spacing(2)
|
|
grid.set_column_spacing(4)
|
|
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.set_halign(Gtk.Align.START)
|
|
vocab_label.set_justify(Gtk.Justification.LEFT)
|
|
grid.attach(vocab_label, 0, i, 3, 1)
|
|
|
|
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.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.props.wrap = True
|
|
grid.attach(def_label, 1, i, 1, 1)
|
|
i = i + 1
|
|
grid.show_all()
|
|
return grid
|
|
else:
|
|
return None
|
|
|
|
|
|
|
|
class UberwriterInlinePreview():
|
|
|
|
def __init__(self, view, text_buffer):
|
|
self.TextView = view
|
|
self.TextBuffer = text_buffer
|
|
self.LatexConverter = LatexToPNG.LatexToPNG()
|
|
cursor_mark = self.TextBuffer.get_insert()
|
|
cursor_iter = self.TextBuffer.get_iter_at_mark(cursor_mark)
|
|
self.ClickMark = self.TextBuffer.create_mark('click', cursor_iter)
|
|
# Events for popup menu
|
|
# self.TextView.connect_after('populate-popup', self.populate_popup)
|
|
# self.TextView.connect_after('popup-menu', self.move_popup)
|
|
self.TextView.connect('button-press-event', self.click_move_button)
|
|
self.popover = None
|
|
self.settings = Settings.new()
|
|
|
|
def open_popover_with_widget(self, widget):
|
|
a = self.TextBuffer.create_child_anchor(self.TextBuffer.get_iter_at_mark(self.ClickMark))
|
|
lbl = Gtk.Label('')
|
|
self.TextView.add_child_at_anchor(lbl, a)
|
|
lbl.show()
|
|
# a = Gtk.Window.new(Gtk.WindowType.POPUP)
|
|
# a.set_transient_for(self.TextView.get_toplevel())
|
|
# a.grab_focus()
|
|
# a.set_name("QuickPreviewPopup")
|
|
# # a.set_attached_to(self.TextView)
|
|
# a.move(300, 300)
|
|
# a.set_modal(True)
|
|
# def close(widget, event, *args):
|
|
# if(event.keyval == Gdk.KEY_Escape):
|
|
# widget.destroy()
|
|
# a.connect('key-press-event', close)
|
|
b = Gtk.Grid.new()
|
|
alignment = Gtk.Alignment()
|
|
alignment.props.margin_bottom = 5
|
|
alignment.props.margin_top = 5
|
|
alignment.props.margin_left = 4
|
|
alignment.add(widget)
|
|
# self.TextView.add_child_in_window(b, Gtk.TextWindowType.WIDGET, 200, 200)
|
|
# b.attach(Gtk.Label.new("test 123"), 0, 0, 1, 1)
|
|
# b.show_all()
|
|
# a.show_all()
|
|
self.popover = Gtk.Popover.new(lbl)
|
|
self.popover.get_style_context().add_class("QuickPreviewPopup")
|
|
self.popover.add(alignment)
|
|
# a.add(alignment)
|
|
dismiss, rect = self.popover.get_pointing_to()
|
|
rect.y = rect.y - 20
|
|
self.popover.set_pointing_to(rect)
|
|
# widget = Gtk.Label.new("testasds a;12j3 21 lk3j213")
|
|
widget.show_all()
|
|
|
|
# b.attach(widget, 0, 1, 1, 1)
|
|
self.popover.set_modal(True)
|
|
self.popover.show_all()
|
|
# print(self.popover)
|
|
self.popover.set_property('width-request', 50)
|
|
|
|
def click_move_button(self, widget, event):
|
|
if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
|
|
x, y = self.TextView.window_to_buffer_coords(2,
|
|
int(event.x),
|
|
int(event.y))
|
|
self.TextBuffer.move_mark(self.ClickMark,
|
|
self.TextView.get_iter_at_location(x, y).iter)
|
|
self.populate_popup(self.TextView)
|
|
|
|
def fix_table(self, widget, data=None):
|
|
logger.debug('fixing that table')
|
|
f = FixTable(self.TextBuffer)
|
|
f.fix_table()
|
|
|
|
def populate_popup(self, editor, data=None):
|
|
# popover = Gtk.Popover.new(editor)
|
|
# pop_cont = Gtk.Container.new()
|
|
# popover.add(pop_cont)
|
|
# popover.show_all()
|
|
|
|
item = Gtk.MenuItem.new()
|
|
item.set_name("PreviewMenuItem")
|
|
separator = Gtk.SeparatorMenuItem.new()
|
|
|
|
# table_item = Gtk.MenuItem.new()
|
|
# table_item.set_label('Fix that table')
|
|
|
|
# table_item.connect('activate', self.fix_table)
|
|
# table_item.show()
|
|
# menu.prepend(table_item)
|
|
# menu.show()
|
|
|
|
start_iter = self.TextBuffer.get_iter_at_mark(self.ClickMark)
|
|
# Line offset of click mark
|
|
line_offset = start_iter.get_line_offset()
|
|
end_iter = start_iter.copy()
|
|
start_iter.set_line_offset(0)
|
|
end_iter.forward_to_line_end()
|
|
|
|
text = self.TextBuffer.get_text(start_iter, end_iter, False)
|
|
|
|
math = MarkupBuffer.regex["MATH"]
|
|
link = MarkupBuffer.regex["LINK"]
|
|
|
|
footnote = re.compile('\[\^([^\s]+?)\]')
|
|
image = re.compile("!\[(.*?)\]\((.+?)\)")
|
|
|
|
buf = self.TextBuffer
|
|
context_offset = 0
|
|
|
|
matchlist = []
|
|
|
|
found_match = False
|
|
|
|
matches = re.finditer(math, text)
|
|
for match in matches:
|
|
logger.debug(match.group(1))
|
|
if match.start() < line_offset and match.end() > line_offset:
|
|
success, result = self.LatexConverter.generatepng(match.group(1))
|
|
if success:
|
|
image = Gtk.Image.new_from_file(result)
|
|
image.show()
|
|
logger.debug("logging image")
|
|
# item.add(image)
|
|
self.open_popover_with_widget(image)
|
|
else:
|
|
label = Gtk.Label()
|
|
msg = 'Formula looks incorrect:\n' + result
|
|
label.set_alignment(0.0, 0.5)
|
|
label.set_text(msg)
|
|
label.show()
|
|
item.add(label)
|
|
self.open_popover_with_widget(item)
|
|
item.show()
|
|
# menu.prepend(separator)
|
|
# separator.show()
|
|
# menu.prepend(item)
|
|
# menu.show()
|
|
found_match = True
|
|
break
|
|
|
|
if not found_match:
|
|
# Links
|
|
matches = re.finditer(link, text)
|
|
for match in matches:
|
|
if match.start() < line_offset and match.end() > line_offset:
|
|
text = text[text.find("http://"):-1]
|
|
|
|
item.connect("activate", lambda w: webbrowser.open(text))
|
|
|
|
logger.debug(text)
|
|
|
|
statusitem = Gtk.MenuItem.new()
|
|
statusitem.show()
|
|
|
|
spinner = Gtk.Spinner.new()
|
|
spinner.start()
|
|
statusitem.add(spinner)
|
|
spinner.show()
|
|
|
|
thread = threading.Thread(target=check_url,
|
|
args=(text, statusitem, spinner))
|
|
thread.start()
|
|
|
|
webphoto_item = Gtk.MenuItem.new()
|
|
webphoto_item.show()
|
|
spinner_2 = Gtk.Spinner.new()
|
|
spinner_2.start()
|
|
webphoto_item.add(spinner_2)
|
|
spinner_2.show()
|
|
|
|
thread_image = threading.Thread(target=get_web_thumbnail,
|
|
args=(text, webphoto_item, spinner_2))
|
|
|
|
thread_image.start()
|
|
|
|
item.set_label(_("Open Link in Webbrowser"))
|
|
item.show()
|
|
self.open_popover_with_widget(webphoto_item)
|
|
|
|
# menu.prepend(separator)
|
|
# separator.show()
|
|
|
|
# menu.prepend(webphoto_item)
|
|
# menu.prepend(statusitem)
|
|
# menu.prepend(item)
|
|
# menu.show()
|
|
|
|
|
|
found_match = True
|
|
break
|
|
|
|
if not found_match:
|
|
matches = re.finditer(image, text)
|
|
for match in matches:
|
|
if match.start() < line_offset and match.end() > line_offset:
|
|
path = match.group(2)
|
|
if path.startswith("file://"):
|
|
path = path[7:]
|
|
elif not path.startswith("/"):
|
|
# then the path is relative
|
|
base_path = self.settings.get_value("open-file-path").get_string()
|
|
path = base_path + "/" + path
|
|
|
|
logger.info(path)
|
|
pb = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 400, 300)
|
|
image = Gtk.Image.new_from_pixbuf(pb)
|
|
image.show()
|
|
self.open_popover_with_widget(image)
|
|
item.set_property('width-request', 50)
|
|
|
|
# item.add(image)
|
|
# item.set_property('width-request', 50)
|
|
# item.show()
|
|
# menu.prepend(separator)
|
|
# separator.show()
|
|
# menu.prepend(item)
|
|
# menu.show()
|
|
found_match = True
|
|
break
|
|
|
|
if not found_match:
|
|
matches = re.finditer(footnote, text)
|
|
for match in matches:
|
|
if match.start() < line_offset and match.end() > line_offset:
|
|
logger.debug(match.group(1))
|
|
footnote_match = re.compile("\[\^" + match.group(1) + "\]: (.+(?:\n|\Z)(?:^[\t].+(?:\n|\Z))*)", re.MULTILINE)
|
|
replace = re.compile("^\t", re.MULTILINE)
|
|
start, end = self.TextBuffer.get_bounds()
|
|
fn_match = re.search(footnote_match, self.TextBuffer.get_text(start, end, False))
|
|
label = Gtk.Label()
|
|
label.set_alignment(0.0, 0.5)
|
|
logger.debug(fn_match)
|
|
if fn_match:
|
|
result = re.sub(replace, "", fn_match.group(1))
|
|
if result.endswith("\n"):
|
|
result = result[:-1]
|
|
else:
|
|
result = _("No matching footnote found")
|
|
label.set_max_width_chars(40)
|
|
label.set_line_wrap(True)
|
|
label.set_text(result)
|
|
label.show()
|
|
item.add(label)
|
|
item.show()
|
|
self.open_popover_with_widget(item)
|
|
|
|
# menu.prepend(separator)
|
|
# separator.show()
|
|
# menu.prepend(item)
|
|
# menu.show()
|
|
found_match = True
|
|
break
|
|
|
|
if not found_match:
|
|
start_iter = self.TextBuffer.get_iter_at_mark(self.ClickMark)
|
|
start_iter.backward_word_start()
|
|
end_iter = start_iter.copy()
|
|
end_iter.forward_word_end()
|
|
word = self.TextBuffer.get_text(start_iter, end_iter, False)
|
|
terms = get_dictionary(word)
|
|
if terms:
|
|
sc = Gtk.ScrolledWindow.new()
|
|
sc.add(fill_lexikon_bubble(word, terms))
|
|
sc.props.width_request = 500
|
|
sc.props.height_request = 400
|
|
sc.show_all()
|
|
self.open_popover_with_widget(sc)
|
|
|
|
return
|
|
|
|
def move_popup(self):
|
|
pass
|