# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- # BEGIN LICENSE # Copyright (C) 2012, Wolf Vollprecht # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # END LICENSE import logging 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, text_view_markup_handler from uberwriter.settings import Settings from uberwriter.fix_table import FixTable LOGGER = logging.getLogger('uberwriter') # 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 class DictAccessor(): def __init__(self, host='pan.alephnull.com', port=2628, timeout=60): self.telnet = telnetlib.Telnet(host, port) self.timeout = timeout self.login_response = self.telnet.expect( [self.reEndResponse], self.timeout)[2] def get_online(self, word): process = subprocess.Popen(['dict', '-d', 'wn', word], stdout=subprocess.PIPE) return process.communicate()[0] def run_command(self, cmd): self.telnet.write(cmd.encode('utf-8') + b'\r\n') return self.telnet.expect([self.reEndResponse], self.timeout)[2] def get_matches(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.run_command('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( br'^[2-5][0-58][0-9] .*\r\n$', re.DOTALL + re.MULTILINE) reDefinition = re.compile(br'^151(.*?)^\.', re.DOTALL + re.MULTILINE) def get_definition(self, database, word): if database in ['', 'all']: d = '*' else: d = database w = word.replace('"', r'\"') dsplit = self.run_command('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.run_command('QUIT') self.telnet.close() return t def parse_wordnet(self, response): # consisting of group (n,v,adj,adv) # number, description, examples, synonyms, antonyms lines = response.splitlines() lines = lines[2:] lines = ' '.join(lines) lines = re.sub(r'\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 not l: 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(r'(?: |^)(\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.get_definition('wn', term) if 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] process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) _output = process.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 return None class InlinePreview: 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) self.click_mark = self.text_buffer.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.text_view.connect('button-press-event', self.click_move_button) self.popover = None self.settings = Settings.new() def open_popover_with_widget(self, widget): a = self.text_buffer.create_child_anchor( self.text_buffer.get_iter_at_mark(self.click_mark)) lbl = Gtk.Label('') self.text_view.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) 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("quick-preview-popup") 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.text_view.window_to_buffer_coords(2, int(event.x), int(event.y)) self.text_buffer.move_mark(self.click_mark, self.text_view.get_iter_at_location(x, y).iter) self.populate_popup(self.text_view) def fix_table(self, _widget, _data=None): LOGGER.debug('fixing that table') fix_table = FixTable(self.text_buffer) fix_table.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.text_buffer.get_iter_at_mark(self.click_mark) # 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.text_buffer.get_text(start_iter, end_iter, False) math = text_view_markup_handler.regex["MATH"] link = text_view_markup_handler.regex["LINK"] footnote = re.compile(r'\[\^([^\s]+?)\]') image = re.compile(r"!\[(.*?)\]\((.+?)\)") found_match = False matches = re.finditer(math, text) for match in matches: LOGGER.debug(match.group(1)) if match.start() < line_offset < match.end(): success, result = self.latex_converter.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 < match.end(): 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 < match.end(): 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) pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 400, 300) image = Gtk.Image.new_from_pixbuf(pixbuf) 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 < match.end(): LOGGER.debug(match.group(1)) footnote_match = re.compile( r"\[\^" + match.group(1) + r"\]: (.+(?:\n|\Z)(?:^[\t].+(?:\n|\Z))*)", re.MULTILINE) replace = re.compile(r"^\t", re.MULTILINE) start, end = self.text_buffer.get_bounds() fn_match = re.search( footnote_match, self.text_buffer.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.text_buffer.get_iter_at_mark(self.click_mark) start_iter.backward_word_start() end_iter = start_iter.copy() end_iter.forward_word_end() word = self.text_buffer.get_text(start_iter, end_iter, False) terms = get_dictionary(word) if terms: scrolled_window = Gtk.ScrolledWindow.new() scrolled_window.add(fill_lexikon_bubble(word, terms)) scrolled_window.props.width_request = 500 scrolled_window.props.height_request = 400 scrolled_window.show_all() self.open_popover_with_widget(scrolled_window) def move_popup(self): pass