Merge pull request #167 from goncalossilva/ft.qol

"Quality of life" improvements
github/fork/yochananmarqos/patch-1
somas95 2019-07-30 23:35:10 +02:00 committed by GitHub
commit d75a14d0c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1119 additions and 1598 deletions

View File

@ -1,59 +0,0 @@
#!/usr/bin/python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2019, 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
### DO NOT EDIT THIS FILE ###
import sys
import os
import pkg_resources
import gettext
import locale
# Add project root directory (enable symlink and trunk execution)
PROJECT_ROOT_DIRECTORY = os.path.abspath(
os.path.dirname(os.path.dirname(os.path.realpath(sys.argv[0]))))
# Set the path if needed. This allows uberwriter to run without installing it :)
python_path = []
if os.path.abspath(__file__).startswith('/opt'):
gettext.bindtextdomain('uberwriter', '/opt/extras.ubuntu.com/uberwriter/share/locale')
syspath = sys.path[:] # copy to avoid infinite loop in pending objects
for path in syspath:
opt_path = path.replace('/usr', '/opt/extras.ubuntu.com/uberwriter')
python_path.insert(0, opt_path)
sys.path.insert(0, opt_path)
os.putenv("XDG_DATA_DIRS", "%s:%s" % ("/opt/extras.ubuntu.com/uberwriter/share/", os.getenv("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/")))
if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'uberwriter'))
and PROJECT_ROOT_DIRECTORY not in sys.path):
python_path.insert(0, PROJECT_ROOT_DIRECTORY)
sys.path.insert(0, PROJECT_ROOT_DIRECTORY)
if python_path:
os.putenv('PYTHONPATH', "%s:%s" % (os.getenv('PYTHONPATH', ''), ':'.join(python_path))) # for subprocesses
import uberwriter
locale_dir = os.path.abspath(os.path.join(os.path.dirname(uberwriter.__file__),'../po/'))
# L10n
locale.textdomain('uberwriter')
locale.bindtextdomain('uberwriter', locale_dir)
gettext.textdomain('uberwriter')
gettext.bindtextdomain('uberwriter', locale_dir)
uberwriter.main()

View File

@ -17,7 +17,7 @@
bind "<ctl><shift>z" { "redo" () };
}
@define-color math_text_color mix(@theme_fg_color, #00b5ff, 0.15);
@define-color code_bg_color mix(@theme_base_color, @theme_fg_color, 0.05);
/* Main window and text colors */
@ -117,63 +117,37 @@
background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
}
#PreviewMenuItem image {
border-radius: 2px;
color: #666;
padding: 3px 5px;
border: none;
background: #FFF;
}
.uberwriter-window treeview {
padding: 3px 3px 3px 3px;
padding: 4px 4px 4px 4px;
}
#LexikonBubble {
/*font: serif 10;*/
font-family: serif;
font-size: 10px;
background: @theme_bg_color;
border-radius: 4px;
border-color: @theme_bg_color;
margin: 5px;
padding: 5px;
}
/* .quick-preview-popup {
padding: 5px;
margin: 5px;
border: 1px solid #333;
background: @ligth_bg;
border-radius: 3px;
border-color: @theme_bg_color;
} */
#LexikonBubble label {
/*padding: 5px;*/
}
#LexikonBubble {
background-color: @theme_bg_color;
border: 5px solid @theme_bg_color;
}
#LexikonBubble .lexikon-heading {
.lexikon {
font-family: serif;
font-size: 12px;
padding-bottom: 5px;
padding-top: 5px;
font-weight: bold;
padding-left: 10px;
background: @theme_bg_color;
border: 4px solid @theme_bg_color;
}
#LexikonBubble .lexikon-num {
padding-right: 5px;
padding-left: 20px;
.lexikon .header {
font-family: serif;
font-size: 14px;
padding-top: 16px;
padding-bottom: 4px;
font-weight: bold;
}
.lexikon .header.first {
padding-top: 0px;
}
.lexikon .number {
padding-left: 16px;
padding-right: 4px;
}
.quick-preview-popup {
background-color: @theme_bg_color;
padding: 8px 12px 8px 12px;
}
.quick-preview-popup grid {

View File

@ -95,7 +95,7 @@ To give your document some meta-information and a nice title, you can use title
Emphasizing some text is done by surrounding it with *s:
This is *emphasized with asterisks*, and this will be a **bold text**. And even more ***krass***. And if you want to erase something: ~~completely gone~~ (sorrounded by ~)
This is *emphasized with asterisks*, and this will be a **bold text**. And even more ***krass***. And if you want to erase something: ~~completely gone~~ (surrounded by ~)
### Horizontal Rules

View File

@ -21,11 +21,11 @@
</section>
<section>
<item>
<attribute name="label" translatable="yes">Save _As</attribute>
<attribute name="label" translatable="yes">Save As</attribute>
<attribute name="action">app.save_as</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Export</attribute>
<attribute name="label" translatable="yes">Export</attribute>
<attribute name="action">app.export</attribute>
</item>
<item>
@ -33,6 +33,12 @@
<attribute name="action">app.copy_html</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Find and Replace</attribute>
<attribute name="action">app.search_replace</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Preferences</attribute>

View File

@ -92,14 +92,21 @@
<child>
<object class="GtkShortcutsGroup" id="search">
<property name="visible">True</property>
<property name="title" translatable="yes" context="shortcut window">Search</property>
<property name="title" translatable="yes" context="shortcut window">Find</property>
<child>
<object class="GtkShortcutsShortcut" id="s1-10">
<property name="visible">True</property>
<property name="title" translatable="yes" context="shortcut window">Search</property>
<property name="title" translatable="yes" context="shortcut window">Find</property>
<property name="accelerator">&lt;Primary&gt;f</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut" id="s1-11">
<property name="visible">True</property>
<property name="title" translatable="yes" context="shortcut window">Find and replace</property>
<property name="accelerator">&lt;Primary&gt;h</property>
</object>
</child>
</object>
</child>
<child>

View File

@ -6,6 +6,7 @@
<object class="GtkImage" id="edit-find-replace">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xpad">12</property>
<property name="icon_name">edit-find-replace-symbolic</property>
</object>
<object class="GtkImage" id="go-up">
@ -268,7 +269,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Open Replace</property>
<property name="tooltip_text" translatable="yes">Replace</property>
<property name="image">edit-find-replace</property>
</object>
<packing>
@ -360,7 +361,7 @@
</child>
<child>
<object class="GtkButton" id="replace_all">
<property name="label" translatable="yes">Replace all</property>
<property name="label" translatable="yes">Replace All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>

View File

@ -1,121 +0,0 @@
{
"id": "de.wolfvollprecht.UberWriter.Plugin.WebPhoto",
"runtime": "de.wolfvollprecht.UberWriter",
"branch": "stable",
"sdk": "org.gnome.Sdk//3.26",
"build-extension": true,
"separate-locales": false,
"appstream-compose": false,
"finish-args": [
],
"build-options" : {
"prefix": "/app/extensions/WebPhoto",
"env": {
"PATH": "/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin"
}
},
"cleanup": [],
"modules": [
{
"name": "Glib2",
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/gnome/sources/glib/2.56/glib-2.56.1.tar.xz",
"sha256": "40ef3f44f2c651c7a31aedee44259809b6f03d3d20be44545cd7d177221c0b8d"
}
]
},
{
"name": "LibIDL",
"buildsystem": "autotools",
"sources": [
{
"type": "git",
"url": "https://github.com/GNOME/libIDL/",
"tag": "LIBIDL_0_8_14",
"commit": "666fcbf086fb859738b67417c99a9895bb3d8ce5"
}
]
},
{
"name": "ORBit2",
"rm-configure": true,
"config-opts": ["--prefix=/app/extensions/WebPhoto"],
"build-options": {
"env":{
"PKG_CONFIG_PATH": "/app/extensions/WebPhoto/lib/pkgconfig",
"GNOME2_DIR": "/app/extensions/WebPhoto",
"LD_LIBRARY_PATH": "/app/extensions/WebPhoto/lib",
"PATH": "/app/extensions/WebPhoto/bin:/usr/bin"
}
},
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/gnome/sources/ORBit2/2.14/ORBit2-2.14.19.tar.bz2",
"sha256": "55c900a905482992730f575f3eef34d50bda717c197c97c08fa5a6eafd857550"
},
{
"type": "patch",
"path": "ORBit2.patch"
},
{
"type": "script",
"dest-filename": "autogen.sh",
"commands": [
"autoreconf -fi"
]
}
]
},
{
"name": "gconf",
"buildsystem": "autotools",
"config-opts": ["--prefix=/app/extensions/WebPhoto"],
"build-options": {
"env":{
"PKG_CONFIG_PATH": "/app/extensions/WebPhoto/lib/pkgconfig",
"GNOME2_DIR": "/app/extensions/WebPhoto",
"LD_LIBRARY_PATH": "/app/extensions/WebPhoto/lib",
"PATH": "/app/extensions/WebPhoto/bin:/usr/bin"
}
},
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/GConf/3.2/GConf-3.2.6.tar.xz",
"sha256": "1912b91803ab09a5eed34d364bf09fe3a2a9c96751fde03a4e0cfa51a04d784c"
}
]
},
{
"name": "gnome-web-photo",
"buildsystem": "autotools",
"config-opts": [
"--with-gtk=3.0",
"--prefix=/app/extensions/WebPhoto"
],
"build-options": {
"env":{
"LD_LIBRARY_PATH": "/app/extensions/WebPhoto/lib",
"PATH": "/app/bin:/app/extensions/WebPhoto/bin:/usr/bin",
"ACLOCAL_PATH": "/app/extensions/WebPhoto/share/aclocal"
}
},
"sources": [
{
"type": "git",
"url": "https://github.com/GNOME/gnome-web-photo/",
"tag": "0.10.6",
"commit": "827d6b98c120b4dd8d689a1faf52450685ca6d46"
},
{
"type": "patch",
"path": "GnomeWebPhoto.patch"
}
]
}
]
}

View File

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python3
#
# Generates color palettes based on the specified background/foreground colors.
#

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>

View File

@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2019, 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 unittest
import re
from uberwriter import markup_regex
class TestRegex(unittest.TestCase):
"""Test cases based on CommonMark's specs and demo:
- https://spec.commonmark.org/
- https://spec.commonmark.org/dingus/
CommonMark is the Markdown variant chosen as first-class. It's great and encouraged that
others are supported as well, but when in conflict or undecided, CommonMark should be picked.
TODO: Use decorators. This needs decorators everywhere.
"""
def test_bold(self):
test_texts = {
"**bold**": "bold",
"__bold__": "bold",
"This is **bold** text": "bold",
"This is __bold__ text": "bold",
"before**middle**end": "middle",
"before** middle **end": " middle ",
"empty * * bold": None
}
for test, result in test_texts.items():
with self.subTest(name=test):
match = re.search(markup_regex.BOLD, test)
if not match:
self.assertFalse(result, msg=test)
else:
self.assertEqual(match.group("text"), result, msg=test)
def test_header(self):
test_texts = {
"# Header 1": "Header 1",
"## Header 2": "Header 2",
"### Header 3": "Header 3",
"#### Header 4": "Header 4",
"##### Header 5": "Header 5",
"###### Header 6": "Header 6",
"#": None,
"#######": None,
"before\n# Header\nafter": "Header"
}
for test, result in test_texts.items():
with self.subTest(name=test):
match = re.search(markup_regex.HEADER, test)
if not match:
self.assertFalse(result, msg=test)
else:
self.assertEqual(match.group("text"), result, msg=test)
def test_header_under(self):
test_texts = {
"Header 1\n=": "Header 1",
"Header 1##\n=": "Header 1##",
"Header 2\n-- \n": "Header 2",
"Header 1\n=f": None,
"Header 1\n =": "Header 1"
}
for test, result in test_texts.items():
with self.subTest(name=test):
match = re.search(markup_regex.HEADER_UNDER, test)
if not match:
self.assertFalse(result, msg=test)
else:
self.assertEqual(match.group("text"), result, msg=test)
if __name__ == '__main__':
unittest.main()

View File

@ -1,4 +1,4 @@
#!/usr/bin/python3
#!/usr/bin/env python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>

View File

@ -98,6 +98,10 @@ class Application(Gtk.Application):
action.connect("activate", self.on_copy_html)
self.add_action(action)
action = Gio.SimpleAction.new("search_replace", None)
action.connect("activate", self.on_search_replace)
self.add_action(action)
action = Gio.SimpleAction.new("preferences", None)
action.connect("activate", self.on_preferences)
self.add_action(action)
@ -143,6 +147,7 @@ class Application(Gtk.Application):
self.set_accels_for_action("app.fullscreen", ["F11"])
self.set_accels_for_action("app.preview", ["<Ctl>p"])
self.set_accels_for_action("app.search", ["<Ctl>f"])
self.set_accels_for_action("app.search_replace", ["<Ctl>h"])
self.set_accels_for_action("app.spellcheck", ["F7"])
self.set_accels_for_action("app.new", ["<Ctl>n"])
@ -208,7 +213,10 @@ class Application(Gtk.Application):
self.window.save_document()
def on_search(self, _action, _value):
self.window.open_search_and_replace()
self.window.open_search()
def on_search_replace(self, _action, _value):
self.window.open_search(replace=True)
def on_focus_mode(self, action, value):
action.set_state(value)

View File

@ -1,186 +0,0 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
# BEGIN LICENSE
# Copyright (C) 2019, 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
from gettext import gettext as _
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
def rule(self):
"""insert ruler at cursor
"""
self.text_buffer.insert_at_cursor("\n\n-------\n")
self.text_editor.scroll_mark_onscreen(self.text_buffer.get_insert())
def bold(self):
"""set selected text as bold
"""
self.apply_format("**")
def italic(self):
"""set selected text as italic
"""
self.apply_format("*")
def strikeout(self):
"""set selected text as stricked out
"""
self.apply_format("~~")
def apply_format(self, wrap="*"):
"""apply the given wrap to a selected text, or insert a helper text wraped
if nothing is selected
Keyword Arguments:
wrap {str} -- [the format mark] (default: {"*"})
"""
if self.text_buffer.get_has_selection():
# Find current highlighting
(start, end) = self.text_buffer.get_selection_bounds()
moved = False
if (start.get_offset() >= len(wrap) and
end.get_offset() <= self.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 = self.text_buffer.get_text(ext_start, ext_end, True)
else:
text = self.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
self.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)
self.text_buffer.delete(start, end)
move_back = len(wrap)
self.text_buffer.insert_at_cursor(text)
text_length = len(new_text)
else:
helptext = ""
if wrap == "*":
helptext = _("emphasized text")
elif wrap == "**":
helptext = _("strong text")
elif wrap == "~~":
helptext = _("striked out text")
self.text_buffer.insert_at_cursor(wrap + helptext + wrap)
text_length = len(helptext)
move_back = len(wrap)
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
cursor_iter.backward_chars(move_back)
self.text_buffer.move_mark_by_name('selection_bound', cursor_iter)
cursor_iter.backward_chars(text_length)
self.text_buffer.move_mark_by_name('insert', cursor_iter)
def unordered_list_item(self):
"""insert unordered list items or mark a selection as
an item in an unordered list
"""
helptext = _("List item")
text_length = len(helptext)
move_back = 0
if self.text_buffer.get_has_selection():
(start, end) = self.text_buffer.get_selection_bounds()
if start.starts_line():
text = self.text_buffer.get_text(start, end, False)
if text.startswith(("- ", "* ", "+ ")):
delete_end = start.forward_chars(2)
self.text_buffer.delete(start, delete_end)
else:
self.text_buffer.insert(start, "- ")
else:
move_back = 0
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
start_ext = cursor_iter.copy()
start_ext.backward_lines(3)
text = self.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():
self.text_buffer.insert_at_cursor(line[:2] + helptext)
else:
self.text_buffer.insert_at_cursor(
"\n" + line[:2] + helptext)
break
else:
if not lines[-1] and not lines[-2]:
self.text_buffer.insert_at_cursor("- " + helptext)
elif not lines[-1]:
if cursor_iter.starts_line():
self.text_buffer.insert_at_cursor("- " + helptext)
else:
self.text_buffer.insert_at_cursor("\n- " + helptext)
else:
self.text_buffer.insert_at_cursor("\n\n- " + helptext)
break
self.select_edit(move_back, text_length)
def ordered_list_item(self):
# TODO: implement ordered lists
pass
def select_edit(self, move_back, text_length):
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
cursor_iter.backward_chars(move_back)
self.text_buffer.move_mark_by_name('selection_bound', cursor_iter)
cursor_iter.backward_chars(text_length)
self.text_buffer.move_mark_by_name('insert', cursor_iter)
self.text_editor.scroll_mark_onscreen(self.text_buffer.get_insert())
def heading(self):
"""insert heading at cursor position or set selected text as one
"""
helptext = _("Heading")
if self.text_buffer.get_has_selection():
(start, end) = self.text_buffer.get_selection_bounds()
text = self.text_buffer.get_text(start, end, False)
self.text_buffer.delete(start, end)
else:
text = helptext
self.text_buffer.insert_at_cursor("#" + " " + text)
self.select_edit(0, len(text))

View File

@ -166,7 +166,7 @@ def main_buttons(app):
btn.open_recent.pack_start(open_button, False, False, 0)
btn.open_recent.pack_end(recents_button, False, False, 0)
btn.search.set_tooltip_text(_("Search and Replace"))
btn.search.set_tooltip_text(_("Find"))
btn.menu.set_tooltip_text(_("Menu"))
btn.menu.set_image(Gtk.Image.new_from_icon_name("open-menu-symbolic",
Gtk.IconSize.BUTTON))

View File

@ -20,6 +20,7 @@
import logging
import os
import shutil
from contextlib import contextmanager
import gi
import pypandoc
@ -52,6 +53,13 @@ def get_builder(builder_file_name):
return builder
@contextmanager
def user_action(text_buffer):
text_buffer.begin_user_action()
yield text_buffer
text_buffer.end_user_action()
def path_to_file(path):
"""Return a file path (file:///) for the given path"""
@ -148,12 +156,14 @@ def show_uri(parent, link):
def alias(alternative_function_name):
'''see http://www.drdobbs.com/web-development/184406073#l9'''
def decorator(function):
'''attach alternative_function_name(s) to function'''
if not hasattr(function, 'aliases'):
function.aliases = []
function.aliases.append(alternative_function_name)
return function
return decorator
@ -172,21 +182,21 @@ def exist_executable(command):
def get_descendant(widget, child_name, level, doPrint=False):
if widget is not None:
if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) +
if doPrint: print("-" * level + str(Gtk.Buildable.get_name(widget)) +
" :: " + widget.get_name())
else:
if doPrint: print("-"*level + "None")
if doPrint: print("-" * level + "None")
return None
#/*** If it is what we are looking for ***/
if Gtk.Buildable.get_name(widget) == child_name: # not widget.get_name() !
# /*** If it is what we are looking for ***/
if Gtk.Buildable.get_name(widget) == child_name: # not widget.get_name() !
return widget
#/*** If this widget has one child only search its child ***/
# /*** If this widget has one child only search its child ***/
if (hasattr(widget, 'get_child') and
callable(getattr(widget, 'get_child')) and
child_name != ""):
child = widget.get_child()
if child is not None:
return get_descendant(child, child_name, level+1,doPrint)
return get_descendant(child, child_name, level + 1, doPrint)
# /*** Ity might have many children, so search them ***/
elif (hasattr(widget, 'get_children') and
callable(getattr(widget, 'get_children')) and
@ -196,7 +206,7 @@ def get_descendant(widget, child_name, level, doPrint=False):
found = None
for child in children:
if child is not None:
found = get_descendant(child, child_name, level+1, doPrint) # //search the child
found = get_descendant(child, child_name, level + 1, doPrint) # //search the child
if found: return found

View File

@ -14,101 +14,72 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
# 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
from uberwriter.text_view_markup_handler import MarkupHandler
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf
from uberwriter import latex_to_PNG
gi.require_version("Gtk", "3.0")
gi.require_version("WebKit2", "4.0")
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
from gi.repository import WebKit2
from uberwriter import latex_to_PNG, markup_regex
from uberwriter.settings import Settings
from uberwriter.fix_table import FixTable
LOGGER = logging.getLogger('uberwriter')
class DictAccessor:
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)
# 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):
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]
self.login_response = self.telnet.expect([self.reEndResponse], self.timeout)[2]
def run_command(self, cmd):
self.telnet.write(cmd.encode('utf-8') + b'\r\n')
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 = '*'
if database in ["", "all"]:
d = "*"
else:
d = database
if strategy in ['', 'default']:
s = '.'
if strategy in ["", "default"]:
s = "."
else:
s = strategy
w = word.replace('"', r'\"')
tsplit = self.run_command('MATCH %s %s "%s"' % (d, s, w)).splitlines()
w = word.replace("\"", r"\\\"")
tsplit = self.run_command("MATCH {} {} \"{}\"".format(d, s, w)).splitlines()
mlist = list()
if tsplit[-1].startswith(b'250 ok') and tsplit[0].startswith(b'1'):
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:]))
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 = '*'
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()
w = word.replace("\"", r"\\\"")
dsplit = self.run_command("DEFINE {} \"{}\"".format(d, w)).splitlines(True)
dlist = list()
if dsplit[-1].startswith(b'250 ok') and dsplit[0].startswith(b'1'):
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)
dtext = b"".join(dlines)
dlist = [dtext]
# dlist = dsplit # not using the localhost telnet connection
return dlist
def close(self):
t = self.run_command('QUIT')
t = self.run_command("QUIT")
self.telnet.close()
return t
@ -118,415 +89,211 @@ class DictAccessor():
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)
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'}
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 l in ["adv", "adj", "n", "v"]:
if act_res:
res.append(act_res.copy())
act_res = {}
act_res['defs'] = []
act_res['class'] = l
act_res = {"defs": [], "class": l}
else:
ll = re.split(r'(?: |^)(\d): ', l)
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}
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)
a = re.findall(r"(\[(syn|ant): (.+?)\] ??)+", lll)
for n in a:
if n[1] == 'syn':
act_def['syn'] = re.findall(r'\{(.*?)\}.*?', n[2])
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)
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'] = []
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)
if llll.strip().startswith("\""):
act_def["examples"].append(llll)
else:
act_def['description'].append(llll)
act_def["description"].append(llll)
if act_def and "description" in act_def:
act_res["defs"].append(act_def.copy())
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)
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
return da.parse_wordnet(output.decode(encoding="UTF-8"))
class InlinePreview:
WIDTH = 400
HEIGHT = 300
def __init__(self, text_view):
self.text_view = text_view
self.text_view.connect("button-press-event", self.on_button_press_event)
self.text_buffer = text_view.get_buffer()
self.cursor_mark = self.text_buffer.create_mark(
"click", self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()))
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()
self.characters_per_line = Settings.new().get_int("characters-per-line")
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 = Gtk.Popover.new(self.text_view)
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):
self.preview_fns = {
markup_regex.MATH: self.get_view_for_math,
markup_regex.IMAGE: self.get_view_for_image,
markup_regex.LINK: self.get_view_for_link,
markup_regex.LINK_ALT: self.get_view_for_link,
markup_regex.FOOTNOTE_ID: self.get_view_for_footnote,
re.compile(r"(?P<text>\w+)"): self.get_view_for_lexikon
}
def on_button_press_event(self, _text_view, 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)
x, y = self.text_view.window_to_buffer_coords(2, int(event.x), int(event.y))
self.text_buffer.move_mark(
self.cursor_mark, self.text_view.get_iter_at_location(x, y).iter)
self.open_popover(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 get_view_for_math(self, match):
success, result = self.latex_converter.generatepng(match.group("text"))
if success:
return Gtk.Image.new_from_file(result)
else:
error = _("Formula looks incorrect:")
error += "\n\n{}".format(result)
return Gtk.Label(label=error)
def populate_popup(self, _editor, _data=None):
# popover = Gtk.Popover.new(editor)
# pop_cont = Gtk.Container.new()
# popover.add(pop_cont)
# popover.show_all()
def get_view_for_image(self, match):
path = match.group("url")
if not path.startswith(("file://", "/")):
return self.get_view_for_link(match)
elif path.startswith("file://"):
path = path[7:]
return Gtk.Image.new_from_pixbuf(
GdkPixbuf.Pixbuf.new_from_file_at_size(path, self.WIDTH, self.HEIGHT))
item = Gtk.MenuItem.new()
item.set_name("PreviewMenuItem")
separator = Gtk.SeparatorMenuItem.new()
def get_view_for_link(self, match):
url = match.group("url")
web_view = WebKit2.WebView(zoom_level=0.3125) # ~1280x960
web_view.set_size_request(self.WIDTH, self.HEIGHT)
if GLib.uri_parse_scheme(url) is None:
url = "http://{}".format(url)
web_view.load_uri(url)
return web_view
# table_item = Gtk.MenuItem.new()
# table_item.set_label('Fix that table')
def get_view_for_footnote(self, match):
footnote_id = match.group("id")
fn_matches = re.finditer(markup_regex.FOOTNOTE, self.text_buffer.props.text)
for fn_match in fn_matches:
if fn_match.group("id") == footnote_id:
if fn_match:
footnote = re.sub("\n[\t ]+", "\n", fn_match.group("text"))
else:
footnote = _("No matching footnote found")
label = Gtk.Label(label=footnote)
label.set_max_width_chars(self.characters_per_line)
label.set_line_wrap(True)
return label
return None
# table_item.connect('activate', self.fix_table)
# table_item.show()
# menu.prepend(table_item)
# menu.show()
def get_view_for_lexikon(self, match):
term = match.group("text")
lexikon_dict = get_dictionary(term)
if lexikon_dict:
grid = Gtk.Grid.new()
grid.get_style_context().add_class("lexikon")
grid.set_row_spacing(2)
grid.set_column_spacing(4)
i = 0
for entry in lexikon_dict:
if not entry["defs"]:
continue
elif entry["class"].startswith("n"):
word_type = _("noun")
elif entry["class"].startswith("v"):
word_type = _("verb")
elif entry["class"].startswith("adj"):
word_type = _("adjective")
elif entry["class"].startswith("adv"):
word_type = _("adverb")
else:
continue
start_iter = self.text_buffer.get_iter_at_mark(self.click_mark)
# Line offset of click mark
vocab_label = Gtk.Label.new(term + " ~ " + word_type)
vocab_label.get_style_context().add_class("header")
if i == 0:
vocab_label.get_style_context().add_class("first")
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("number")
num_label.set_valign(Gtk.Align.START)
grid.attach(num_label, 0, i, 1, 1)
def_label = Gtk.Label(label=" ".join(definition["description"]))
def_label.get_style_context().add_class("description")
def_label.set_halign(Gtk.Align.START)
def_label.set_max_width_chars(self.characters_per_line)
def_label.set_line_wrap(True)
def_label.set_justify(Gtk.Justification.FILL)
grid.attach(def_label, 1, i, 1, 1)
i = i + 1
if i > 0:
return grid
return None
def open_popover(self, _editor, _data=None):
start_iter = self.text_buffer.get_iter_at_mark(self.cursor_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 = MarkupHandler.regex["MATH"]
link = MarkupHandler.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 regex, get_view_fn in self.preview_fns.items():
matches = re.finditer(regex, 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_string("open-file-path")
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
if match.start() <= line_offset <= match.end():
prev_view = self.popover.get_child()
if prev_view:
prev_view.destroy()
view = get_view_fn(match)
view.show_all()
self.popover.add(view)
rect = self.text_view.get_iter_location(
self.text_buffer.get_iter_at_mark(self.cursor_mark))
rect.x, rect.y = self.text_view.buffer_to_window_coords(
Gtk.TextWindowType.TEXT, rect.x, rect.y)
self.popover.set_pointing_to(rect)
GLib.idle_add(self.popover.popup) # TODO: It doesn't popup without idle_add.
return

View File

@ -3,61 +3,59 @@
Based on latex2png.py from Stuart Rackham
AUTHOR
Written by Stuart Rackham, <srackham@gmail.com>
The code was inspired by Kjell Magne Fauske's code:
http://fauskes.net/nb/htmleqII/
Written by Stuart Rackham, <srackham@gmail.com>
The code was inspired by Kjell Magne Fauske"s code:
http://fauskes.net/nb/htmleqII/
See also:
http://www.amk.ca/python/code/mt-math
http://code.google.com/p/latexmath2png/
See also:
http://www.amk.ca/python/code/mt-math
http://code.google.com/p/latexmath2png/
COPYING
Copyright (C) 2010 Stuart Rackham. Free use of this software is
granted under the terms of the MIT License.
Copyright (C) 2010 Stuart Rackham. Free use of this software is
granted under the terms of the MIT License.
"""
import os
import sys
import tempfile
import subprocess
import tempfile
class LatexToPNG():
class LatexToPNG:
TEX_HEADER = r"""\documentclass{article}
\usepackage{amsmath}
\usepackage{amsthm}
\usepackage{amssymb}
\usepackage{bm}
\newcommand{\mx}[1]{\mathbf{\bm{#1}}} % Matrix command
\newcommand{\vc}[1]{\mathbf{\bm{#1}}} % Vector command
\newcommand{\T}{\text{T}} % Transpose
\pagestyle{empty}
\begin{document}"""
TEX_HEADER = r'''\documentclass{article}
\usepackage{amsmath}
\usepackage{amsthm}
\usepackage{amssymb}
\usepackage{bm}
\newcommand{\mx}[1]{\mathbf{\bm{#1}}} % Matrix command
\newcommand{\vc}[1]{\mathbf{\bm{#1}}} % Vector command
\newcommand{\T}{\text{T}} % Transpose
\pagestyle{empty}
\begin{document}'''
TEX_FOOTER = r'''\end{document}'''
TEX_FOOTER = r"""\end{document}"""
def __init__(self):
self.temp_result = tempfile.NamedTemporaryFile(suffix='.png')
self.temp_result = tempfile.NamedTemporaryFile(suffix=".png")
def latex2png(self, tex, outfile, dpi, modified):
'''Convert LaTeX input file infile to PNG file named outfile.'''
"""Convert LaTeX input file infile to PNG file named outfile."""
outfile = os.path.abspath(outfile)
outdir = os.path.dirname(outfile)
texfile = tempfile.mktemp(suffix='.tex', dir=os.path.dirname(outfile))
texfile = tempfile.mktemp(suffix=".tex", dir=os.path.dirname(outfile))
basefile = os.path.splitext(texfile)[0]
dvifile = basefile + '.dvi'
temps = [basefile + ext for ext in ('.tex', '.dvi', '.aux', '.log')]
dvifile = basefile + ".dvi"
temps = [basefile + ext for ext in (".tex", ".dvi", ".aux", ".log")]
skip = False
tex = '%s\n%s\n%s\n' % (self.TEX_HEADER, tex.strip(), self.TEX_FOOTER)
tex = "{}\n{}\n{}\n".format(self.TEX_HEADER, tex.strip(), self.TEX_FOOTER)
open(texfile, 'w').write(tex)
open(texfile, "w").write(tex)
saved_pwd = os.getcwd()
os.chdir(outdir)
args = ['latex', '-halt-on-error', texfile]
args = ["latex", "-halt-on-error", texfile]
p = subprocess.Popen(args,
stderr=subprocess.STDOUT,
stdout=subprocess.PIPE)
@ -66,13 +64,13 @@ class LatexToPNG():
output_lines = output.readlines()
if os.path.isfile(dvifile): # DVI File exists
# Convert DVI file to PNG.
args = ['dvipng',
'-D', str(dpi),
'-T', 'tight',
'-x', '1000',
'-z', '9',
'-bg', 'Transparent',
'-o', outfile,
args = ["dvipng",
"-D", str(dpi),
"-T", "tight",
"-x", "1000",
"-z", "9",
"-bg", "Transparent",
"-o", outfile,
dvifile]
p = subprocess.Popen(args)
@ -80,15 +78,15 @@ class LatexToPNG():
else:
self.clean_up(temps)
'''
Errors in Latex output start with "! "
Stripping exclamation marks and superflous newlines
and telling the user what he's done wrong.
'''
"""
Errors in Latex output start with "! "
Stripping exclamation marks and superflous newlines
and telling the user what he"s done wrong.
"""
i = []
error = ""
for line in output_lines:
line = line.decode('utf-8')
line = line.decode("utf-8")
if line.startswith("!"):
error += line[2:] # removing "! "
if error.endswith("\n"):
@ -97,14 +95,14 @@ class LatexToPNG():
def generatepng(self, formula):
try:
self.temp_result = tempfile.NamedTemporaryFile(suffix='.png')
self.temp_result = tempfile.NamedTemporaryFile(suffix=".png")
formula = "$" + formula + "$"
self.latex2png(formula, self.temp_result.name, 300, False)
return (True, self.temp_result.name)
return True, self.temp_result.name
except Exception as e:
self.temp_result.close()
return (False, e.args[0])
return False, e.args[0]
def clean_up(self, files):
for f in files:

View File

@ -442,19 +442,36 @@ class MainWindow(StyledWindow):
if filename:
if filename.startswith('file://'):
filename = filename[7:]
filename = urllib.parse.unquote_plus(filename)
self.text_view.clear()
try:
if os.path.exists(filename):
current_file = codecs.open(filename, encoding="utf-8", mode='r')
self.text_view.set_text(current_file.read())
current_file.close()
with codecs.open(filename, encoding="utf-8", mode='r') as current_file:
self.text_view.set_text(current_file.read())
else:
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.CLOSE,
_("The file you tried to open doesn't exist.\
\nA new file will be created in its place when you save the current one.")
)
dialog.run()
dialog.destroy()
self.set_headerbar_title(os.path.basename(filename) + self.title_end)
self.set_filename(filename)
except Exception:
LOGGER.warning("Error Reading File: %r" % Exception)
except Exception as e:
LOGGER.warning(_("Error Reading File: %r") % e)
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,
Gtk.ButtonsType.CLOSE,
_("Error reading file:\
\n%r" %e)
)
dialog.run()
dialog.destroy()
self.did_change = False
else:
LOGGER.warning("No File arg")
@ -467,11 +484,11 @@ class MainWindow(StyledWindow):
self.load_file(helpers.get_media_file('uberwriter_markdown.md'))
def open_search_and_replace(self):
def open_search(self, replace=False):
"""toggle the search box
"""
self.searchreplace.toggle_search()
self.searchreplace.toggle_search(replace=replace)
def open_advanced_export(self, _widget=None, _data=None):
"""open the export and advanced export dialog
@ -482,7 +499,18 @@ class MainWindow(StyledWindow):
response = self.export.dialog.run()
if response == 1:
self.export.export(bytes(self.text_view.get_text(), "utf-8"))
try:
self.export.export(bytes(self.text_view.get_text(), "utf-8"))
except Exception as e:
dialog = Gtk.MessageDialog(self,
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE,
_("An error happened while trying to export:\n\n{err_msg}")
.format(err_msg= str(e).encode().decode("unicode-escape"))
)
dialog.run()
dialog.destroy()
self.export.dialog.destroy()

View File

@ -0,0 +1,40 @@
import re
ITALIC = re.compile(
r"(\*|_)(?P<text>.*?\S.*?)\1")
BOLD = re.compile(
r"(\*\*|__)(?P<text>.*?\S.*?)\1")
BOLD_ITALIC = re.compile(
r"((\*\*|__)([*_])|([*_])(\*\*|__))(?P<text>.*?\S.*?)(?:\5\4|\3\2)")
STRIKETHROUGH = re.compile(
r"~~(?P<text>.*?\S.*?)~~")
CODE = re.compile(
r"`(?P<text>[^`].+?)`")
LINK = re.compile(
r"\[(?P<text>.*)\]\((?P<url>.+?)(?: \"(?P<title>.+)\")?\)")
LINK_ALT = re.compile(
r"<(?P<text>(?P<url>[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*|(?:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)))>")
IMAGE = re.compile(
r"!\[(?P<text>.*)\]\((?P<url>.+?)(?: \"(?P<title>.+)\")?\)")
HORIZONTAL_RULE = re.compile(
r"(?:^|\n)(?P<symbols> {0,3}[*\-_]{3,} *)(?:\n+|$)")
LIST = re.compile(
r"(?:^|\n)(?P<content>(?P<indent>(?:\t| {4})*)[\-*+]( +)(?P<text>.+(?:\n+ \2.+)*))")
ORDERED_LIST = re.compile(
r"(?:^|\n)(?P<content>(?P<indent>(?:\t| {4})*)(?P<prefix>(?:\d|[a-z])+[.)]) (?P<text>.+(?:\n+ {2}\2.+)*))")
BLOCK_QUOTE = re.compile(
r"^ {0,3}(?:> ?)+(?P<text>.+)", re.M)
HEADER = re.compile(
r"^ {0,3}(?P<level>#{1,6}) (?P<text>[^\n]+)", re.M)
HEADER_UNDER = re.compile(
r"(?:^\n*|\n\n)(?P<text>[^\s].+)\n {0,3}[=\-]+(?: +?\n|$)")
CODE_BLOCK = re.compile(
r"(?:^|\n) {0,3}(?P<block>([`~]{3})(?P<text>.+?) {0,3}\2)(?:\s+?\n|$)", re.S)
TABLE = re.compile(
r"^[\-+]{5,}\n(?P<text>.+?)\n[\-+]{5,}\n", re.S)
MATH = re.compile(
r"([$]{1,2})(?P<text>[^` ].+?[^`\\ ])\1")
FOOTNOTE_ID = re.compile(
r"[^\s]+\[\^(?P<id>(?P<text>[^\s]+))\]")
FOOTNOTE = re.compile(
r"(?:^\n*|\n\n)\[\^(?P<id>[^\s]+)\]: (?P<text>(?:[^\n]+|\n+(?=(?:\t| {4})))+)(?:\n+|$)", re.M)

View File

@ -12,7 +12,7 @@ gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2, GLib
from uberwriter.preview_converter import PreviewConverter
from uberwriter.web_view import WebView
from uberwriter.preview_web_view import PreviewWebView
class Step(IntEnum):
@ -69,7 +69,7 @@ class PreviewHandler:
self.loading = True
if not self.web_view:
self.web_view = WebView()
self.web_view = PreviewWebView()
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
# Show preview once the load is finished
@ -150,12 +150,12 @@ class PreviewHandler:
self.__show(step=Step.RENDER)
def on_text_view_scrolled(self, _text_view, scale):
if self.shown and not math.isclose(scale, self.web_view.get_scroll_scale(), rel_tol=1e-5):
if self.shown and not math.isclose(scale, self.web_view.get_scroll_scale(), rel_tol=1e-4):
self.web_view.set_scroll_scale(scale)
def on_web_view_scrolled(self, _web_view, scale):
if self.shown and self.text_view.get_mapped() and \
not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-5):
not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-4):
self.text_view.set_scroll_scale(scale)
@staticmethod

View File

@ -1,10 +1,12 @@
import webbrowser
import gi
gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2, GLib, GObject
class WebView(WebKit2.WebView):
class PreviewWebView(WebKit2.WebView):
"""A WebView that provides read/write access to scroll.
It does so using JavaScript, by continuously monitoring it while loaded.
@ -31,7 +33,8 @@ e = document.documentElement;
canScroll = e.scrollHeight > e.clientHeight;
wasRendered = typeof isRendered !== "undefined" && isRendered;
isRendered = wasRendered ||
hasMathJax && MathJax.Hub.queue.running == 0 && MathJax.Hub.queue.pending == 0;
!hasMathJax ||
MathJax.Hub.queue.running == 0 && MathJax.Hub.queue.pending == 0;
// Write the current scroll if instructed or if it was just rendered.
if (canScroll && (write || isRendered && !wasRendered)) {{
@ -53,12 +56,13 @@ if (canScroll && isRendered) {{
def __init__(self):
super().__init__()
self.connect("size-allocate", self.on_size_allocate)
self.connect("decide-policy", self.on_decide_policy)
self.connect("load-changed", self.on_load_changed)
self.connect("load-failed", self.on_load_failed)
self.connect("size-allocate", self.on_size_allocate)
self.connect("destroy", self.on_destroy)
self.scroll_scale = 0.0
self.scroll_scale = -1
self.state_loaded = False
self.state_load_failed = False
@ -68,6 +72,9 @@ if (canScroll && isRendered) {{
self.timeout_id = None
def can_scroll(self):
return self.scroll_scale != -1
def get_scroll_scale(self):
return self.scroll_scale
@ -76,6 +83,16 @@ if (canScroll && isRendered) {{
self.scroll_scale = scale
self.state_loop()
def on_size_allocate(self, *_):
self.set_scroll_scale(self.scroll_scale)
def on_decide_policy(self, _web_view, decision, decision_type):
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION and \
decision.get_navigation_action().is_user_gesture():
webbrowser.open(decision.get_request().get_uri())
return True
return False
def on_load_changed(self, _web_view, event):
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
self.state_load_failed = False
@ -88,9 +105,6 @@ if (canScroll && isRendered) {{
self.state_load_failed = True
self.state_loop()
def on_size_allocate(self, *_):
self.set_scroll_scale(self.scroll_scale)
def on_destroy(self, _widget):
self.state_loaded = False
self.state_loop()
@ -113,11 +127,11 @@ if (canScroll && isRendered) {{
self.timeout_id = None
# Set scroll scale if specified, and the state is not dirty
if not self.state_discard_read and scroll_scale not in (None, -1, self.scroll_scale):
if not self.state_discard_read and scroll_scale not in (None, self.scroll_scale):
self.scroll_scale = scroll_scale
self.emit("scroll-scale-changed", self.scroll_scale)
else:
self.state_discard_read = False
if self.scroll_scale != -1:
self.emit("scroll-scale-changed", self.scroll_scale)
self.state_discard_read = False
# Handle the current state
if not self.state_loaded or self.state_load_failed or self.state_waiting:

View File

@ -19,6 +19,8 @@ import re
import gi
from uberwriter.helpers import user_action
gi.require_version('Gtk', '3.0')
from gi.repository import Gdk
@ -80,11 +82,10 @@ class SearchAndReplace:
"""
self.replacebox.set_reveal_child(widget.get_active())
# TODO: refactorize!
def key_pressed(self, _widget, event, _data=None):
"""hide the search and replace content box when ESC is pressed
"""
if event.keyval in [Gdk.KEY_Escape]:
if event.keyval == Gdk.KEY_Escape:
self.hide()
def focused_texteditor(self, _widget, _data=None):
@ -92,15 +93,19 @@ class SearchAndReplace:
"""
self.hide()
def toggle_search(self, _widget=None, _data=None):
def toggle_search(self, replace=False):
"""
show search box
toggle search box
"""
if self.textview.get_mapped() and (
self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False):
search_hidden = self.textview.get_mapped() and (
self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False)
replace_hidden = not self.open_replace_button.get_active()
if search_hidden or (replace and replace_hidden):
self.searchbar.set_search_mode(True)
self.box.set_reveal_child(True)
self.searchentry.grab_focus()
if replace:
self.open_replace_button.set_active(True)
else:
self.hide()
self.open_replace_button.set_active(False)
@ -162,18 +167,20 @@ class SearchAndReplace:
self.replace(self.active)
def replace_all(self, _widget=None, _data=None):
for match in reversed(self.matches):
self.do_replace(match)
with user_action(self.textbuffer):
for match in reversed(self.matches):
self.__do_replace(match)
self.search(scroll=False)
def replace(self, searchindex, _inloop=False):
self.do_replace(self.matches[searchindex])
with user_action(self.textbuffer):
self.__do_replace(self.matches[searchindex])
active = self.active
self.search(scroll=False)
self.active = active
self.scrollto(self.active)
def do_replace(self, match):
def __do_replace(self, match):
start_iter = self.textbuffer.get_iter_at_offset(match[0])
end_iter = self.textbuffer.get_iter_at_offset(match[1])
self.textbuffer.delete(start_iter, end_iter)

View File

@ -1,21 +1,18 @@
import math
import re
from queue import Queue
from threading import Thread
from multiprocessing import Process, Pipe
from gi.repository import GLib
from uberwriter import helpers
from uberwriter.markup_regex import ITALIC, BOLD_ITALIC, BOLD, STRIKETHROUGH, IMAGE, LINK, LINK_ALT,\
HORIZONTAL_RULE, LIST, MATH, TABLE, CODE_BLOCK, HEADER_UNDER, HEADER, BLOCK_QUOTE, ORDERED_LIST, \
FOOTNOTE_ID, FOOTNOTE
class StatsCounter:
"""Counts characters, words, sentences and read time using a background thread."""
"""Counts characters, words, sentences and read time using a worker process."""
# Regexp that matches characters, with the following exceptions:
# * Newlines
# * Sequential spaces
# * Sequential dashes
CHARACTERS = re.compile(r"[^\s-]|(?:[^\S\n](?!\s)|-(?![-\n]))")
# Regexp that matches any character, except for newlines and subsequent spaces.
CHARACTERS = re.compile(r"[^\s]|(?:[^\S\n](?!\s))")
# Regexp that matches Asian letters, general symbols and hieroglyphs,
# as well as sequences of word characters optionally containing non-word characters in-between.
@ -25,41 +22,63 @@ class StatsCounter:
# exclamation mark, paragraph, and variants.
SENTENCES = re.compile(r"[^\n][.。।෴۔።?՞;⸮؟?፧꘏⳺⳻⁇﹖⁈⁉‽!﹗!՜߹႟᥄\n]+")
# Regexp that matches paragraphs, ie. anything separated by newlines.
PARAGRAPHS = re.compile(r".+\n?")
# Regexp that matches paragraphs, ie. anything separated by at least 2 newlines.
PARAGRAPHS = re.compile(r"[^\n]+(\n{2,}|$)")
def __init__(self):
# List of regexp whose matches should be replaced by their "text" group. Order is important.
MARKUP_REGEXP_REPLACE = (
BOLD_ITALIC, ITALIC, BOLD, STRIKETHROUGH, IMAGE, LINK, LINK_ALT, LIST, ORDERED_LIST,
BLOCK_QUOTE, HEADER, HEADER_UNDER, CODE_BLOCK, TABLE, MATH, FOOTNOTE_ID, FOOTNOTE
)
# List of regexp whose matches should be removed. Order is important.
MARKUP_REGEXP_REMOVE = (
HORIZONTAL_RULE,
)
def __init__(self, callback):
super().__init__()
self.queue = Queue()
worker = Thread(target=self.__do_count, name="stats-counter")
worker.daemon = True
worker.start()
# Worker process to handle counting.
self.counting = False
self.count_pending_text = None
self.parent_conn, child_conn = Pipe()
Process(target=self.do_count, args=(child_conn,), daemon=True).start()
GLib.io_add_watch(
self.parent_conn.fileno(), GLib.PRIORITY_LOW, GLib.IO_IN, self.on_counted, callback)
def count(self, text, callback):
"""Count stats for text, calling callback with a result when done.
def count(self, text):
"""Count stats for text.
The callback argument contains the result, in the form:
In case counting is already running, it will re-count once it finishes. This ensure that
the pipe doesn't fill (and block) if multiple requests are made in quick succession."""
(characters, words, sentences, (hours, minutes, seconds))"""
if not self.counting:
self.counting = True
self.count_pending_text = None
self.parent_conn.send(text)
else:
self.count_pending_text = text
self.queue.put((text, callback))
def do_count(self, child_conn):
"""Counts stats in a worker process.
The result is in the format: (characters, words, sentences, (hours, minutes, seconds))"""
def stop(self):
"""Stops the background worker. StatsCounter shouldn't be used after this."""
self.queue.put((None, None))
def __do_count(self):
while True:
while True:
(text, callback) = self.queue.get()
if text is None and callback is None:
try:
text = child_conn.recv()
if not child_conn.poll():
break
except EOFError:
child_conn.close()
return
if self.queue.empty():
break
text = helpers.pandoc_convert(text, to="plain")
for regexp in self.MARKUP_REGEXP_REPLACE:
text = re.sub(regexp, r"\g<text>", text)
for regexp in self.MARKUP_REGEXP_REMOVE:
text = re.sub(regexp, "", text)
character_count = len(re.findall(self.CHARACTERS, text))
@ -73,6 +92,24 @@ class StatsCounter:
read_h, read_m = divmod(read_m, 60)
read_time = (int(read_h), int(read_m), int(read_s))
GLib.idle_add(
callback,
child_conn.send(
(character_count, word_count, sentence_count, paragraph_count, read_time))
def on_counted(self, _source, _condition, callback):
"""Reads the counting result from the pipe and triggers any pending count."""
self.counting = False
if self.count_pending_text is not None:
self.count(self.count_pending_text) # self.count clears the pending text.
try:
if self.parent_conn.poll():
callback(self.parent_conn.recv())
return True
except EOFError:
return False
def stop(self):
"""Stops the worker process. StatsCounter shouldn't be used after this."""
self.parent_conn.close()

View File

@ -36,7 +36,7 @@ class StatsHandler:
self.settings = Settings.new()
self.stats_counter = StatsCounter()
self.stats_counter = StatsCounter(self.update_stats)
self.update_default_stat()
@ -60,9 +60,7 @@ class StatsHandler:
self.text_view.grab_focus()
def on_text_changed(self, buf):
self.stats_counter.count(
buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False),
self.update_stats)
self.stats_counter.count(buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False))
def get_text_for_stat(self, stat):
if stat == self.CHARACTERS:

View File

@ -1,12 +1,12 @@
import gi
from gi.repository.GObject import SignalMatchType
from uberwriter.helpers import user_action
from uberwriter.inline_preview import InlinePreview
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
from uberwriter.text_view_format_inserter import FormatInserter
from uberwriter.text_view_markup_handler import MarkupHandler
from uberwriter.text_view_scroller import TextViewScroller
from uberwriter.text_view_undo_redo_handler import UndoRedoHandler
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
gi.require_version('Gtk', '3.0')
gi.require_version('Gspell', '1')
@ -63,6 +63,7 @@ class TextView(Gtk.TextView):
# General behavior
self.connect('size-allocate', self.on_size_allocate)
self.get_buffer().connect('changed', self.on_text_changed)
self.get_buffer().connect('paste-done', self.on_paste_done)
# Spell checking
self.spellcheck = True
@ -71,6 +72,8 @@ class TextView(Gtk.TextView):
# Undo / redo
self.undo_redo = UndoRedoHandler()
self.get_buffer().connect('begin-user-action', self.undo_redo.on_begin_user_action)
self.get_buffer().connect('end-user-action', self.undo_redo.on_end_user_action)
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)
@ -88,6 +91,7 @@ class TextView(Gtk.TextView):
# Markup
self.markup = MarkupHandler(self)
self.connect('style-updated', self.markup.on_style_updated)
self.connect('destroy', self.markup.stop)
# Preview popover
self.preview_popover = InlinePreview(self)
@ -121,8 +125,15 @@ class TextView(Gtk.TextView):
return text_buffer.get_text(start_iter, end_iter, False)
def set_text(self, text):
"""Set text and clear undo history"""
text_buffer = self.get_buffer()
text_buffer.set_text(text)
with user_action(text_buffer):
text_buffer.set_text(text)
self.undo_redo.clear()
def can_scroll(self):
return self.scroller.can_scroll()
def get_scroll_scale(self):
return self.scroller.get_scroll_scale() if self.scroller else 0
@ -143,6 +154,8 @@ class TextView(Gtk.TextView):
def on_text_changed(self, *_):
self.markup.apply()
def on_paste_done(self, *_):
self.smooth_scroll_to()
def on_parent_set(self, *_):
@ -150,15 +163,16 @@ class TextView(Gtk.TextView):
if parent:
parent.set_size_request(self.get_min_width(), 500)
self.scroller = TextViewScroller(self, parent)
parent.get_vadjustment().connect("changed", self.on_scroll_scale_changed)
parent.get_vadjustment().connect("value-changed", self.on_scroll_scale_changed)
parent.get_vadjustment().connect("changed", self.on_vadjustment_changed)
parent.get_vadjustment().connect("value-changed", self.on_vadjustment_changed)
else:
self.scroller = None
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
if mark.get_name() == 'insert':
if mark.get_name() == 'selection_bound':
self.markup.apply()
self.smooth_scroll_to(mark)
if not self.get_buffer().get_has_selection():
self.smooth_scroll_to(mark)
elif mark.get_name() == 'gtk_drag_target':
self.smooth_scroll_to(mark)
return True
@ -168,10 +182,10 @@ class TextView(Gtk.TextView):
self.markup.apply()
return False
def on_scroll_scale_changed(self, *_):
def on_vadjustment_changed(self, *_):
if self.frozen_scroll_scale is not None:
self.set_scroll_scale(self.frozen_scroll_scale)
else:
elif self.can_scroll():
self.emit("scroll-scale-changed", self.get_scroll_scale())
def unfreeze_scroll_scale(self):
@ -238,8 +252,7 @@ class TextView(Gtk.TextView):
def clear(self):
"""Clear text and undo history"""
self.get_buffer().set_text('')
self.undo_redo.clear()
self.set_text('')
def smooth_scroll_to(self, mark=None):
"""Scrolls if needed to ensure mark is visible.

View File

@ -1,5 +1,6 @@
import mimetypes
import urllib
from os.path import basename
from gi.repository import Gtk
@ -28,17 +29,15 @@ class DragDropHandler:
if info == TARGET_URI:
uris = data.get_uris()
for uri in uris:
uri = urllib.parse.unquote_plus(uri)
name = basename(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
if mime[0] is not None and mime[0].startswith('image/'):
text = "![{}]({})".format(name, uri)
limit_left = 2
limit_right = 23
else:
text = "[Link description](%s)" % uri
text = "[{}]({})".format(name, uri)
limit_left = 1
limit_right = 22
text_buffer.place_cursor(text_buffer.get_iter_at_mark(
@ -58,5 +57,5 @@ class DragDropHandler:
text_buffer.insert_at_cursor(data.get_text())
Gtk.drag_finish(drag_context, True, True, time)
text_view.get_window().present_with_time(time)
text_view.get_toplevel().present_with_time(time)
return False

View File

@ -1,5 +1,7 @@
from gettext import gettext as _
from uberwriter.helpers import user_action
class FormatInserter:
"""Manages insertion of formatting.
@ -25,7 +27,8 @@ class FormatInserter:
"""Insert horizontal rule"""
text_buffer = text_view.get_buffer()
text_buffer.insert_at_cursor("\n\n---\n")
with user_action(text_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):
@ -35,12 +38,14 @@ class FormatInserter:
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, "- ")
with user_action(text_buffer):
text = text_buffer.get_text(start, end, False)
if text.startswith(("- ", "* ", "+ ")):
delete_end = start.copy()
delete_end.forward_chars(2)
text_buffer.delete(start, delete_end)
else:
text_buffer.insert(start, "- ")
else:
helptext = _("Item")
text_length = len(helptext)
@ -53,25 +58,25 @@ class FormatInserter:
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]:
with user_action(text_buffer):
for line in reversed(lines):
if line and line.startswith(("- ", "* ", "+ ")):
if cursor_iter.starts_line():
text_buffer.insert_at_cursor("- " + helptext)
text_buffer.insert_at_cursor(line[:2] + helptext)
else:
text_buffer.insert_at_cursor("\n- " + helptext)
text_buffer.insert_at_cursor("\n" + line[:2] + helptext)
break
else:
text_buffer.insert_at_cursor("\n\n- " + helptext)
break
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)
@ -83,57 +88,60 @@ class FormatInserter:
"""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")
with user_action(text_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)
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)
with user_action(text_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:
if moved and text.startswith(wrap) and text.endswith(wrap):
text = text[len(wrap):-len(wrap)]
new_text = text.lstrip().rstrip()
text = text.replace(new_text, wrap + new_text + 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)
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)
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)

View File

@ -15,255 +15,325 @@
### END LICENSE
import re
from multiprocessing import Pipe, Process
import gi
from gi.overrides import GLib
from uberwriter import helpers
from uberwriter import helpers, markup_regex
from uberwriter.markup_regex import STRIKETHROUGH, BOLD_ITALIC, BOLD, ITALIC, IMAGE, LINK,\
LINK_ALT, HORIZONTAL_RULE, LIST, ORDERED_LIST, BLOCK_QUOTE, HEADER, HEADER_UNDER, TABLE, MATH, \
CODE
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gtk, GLib
from gi.repository import Pango
class MarkupHandler:
# Maximum number of characters for which to markup synchronously.
max_char_sync = 100000
# Regular expressions for various markdown constructs.
regex = {
"ITALIC": re.compile(r"(\*|_)(.+?)\1"),
"BOLD": re.compile(r"(\*\*|__)(.+?)\1"),
"BOLDITALIC": re.compile(r"(\*\*\*|___)(.+?)\1"),
"STRIKETHROUGH": re.compile(r"~~.+?~~"),
"LINK": re.compile(r"(\[).*(\]\(.+?\))"),
"HORIZONTALRULE": re.compile(r"\n\n([ ]{0,3}[*\-_]{3,}[ ]*)\n\n", re.MULTILINE),
"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}) [^\n]+", re.MULTILINE),
"HEADER_UNDER": re.compile(r"^\n[ ]{0,3}\w.+\n[ ]{0,3}[=\-]{3,}", re.MULTILINE),
"CODE": re.compile(r"(?:^|\n)[ ]{0,3}(([`~]{3}).+?[ ]{0,3}\2)(?:\n|$)", re.DOTALL),
"TABLE": re.compile(r"^[\-+]{5,}\n(.+?)\n[\-+]{5,}\n", re.DOTALL),
"MATH": re.compile(r"[$]{1,2}([^` ].+?[^`\\ ])[$]{1,2}"),
}
TAG_NAME_ITALIC = 'italic'
TAG_NAME_BOLD = 'bold'
TAG_NAME_BOLD_ITALIC = 'bold_italic'
TAG_NAME_STRIKETHROUGH = 'strikethrough'
TAG_NAME_CENTER = 'center'
TAG_NAME_WRAP_NONE = 'wrap_none'
TAG_NAME_PLAIN_TEXT = 'plain_text'
TAG_NAME_GRAY_TEXT = 'gray_text'
TAG_NAME_CODE_TEXT = 'code_text'
TAG_NAME_CODE_BLOCK = 'code_block'
TAG_NAME_UNFOCUSED_TEXT = 'unfocused_text'
TAG_NAME_MARGIN_INDENT = 'margin_indent'
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
self.marked_up_text = None
# Styles
# Tags.
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,
self.tag_italic = buffer.create_tag(self.TAG_NAME_ITALIC,
weight=Pango.Weight.NORMAL,
style=Pango.Style.ITALIC)
self.strikethrough = buffer.create_tag('strikethrough', strikethrough=True)
self.horizontalrule = buffer.create_tag('centertext',
justification=Gtk.Justification.CENTER)
self.plaintext = buffer.create_tag('plaintext',
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False,
justification=Gtk.Justification.LEFT)
self.table = buffer.create_tag('table',
wrap_mode=Gtk.WrapMode.NONE,
pixels_above_lines=0,
pixels_below_lines=0)
self.mathtext = buffer.create_tag('mathtext')
self.graytext = buffer.create_tag('graytext',
foreground='gray',
weight=Pango.Weight.NORMAL,
self.tag_bold = buffer.create_tag(self.TAG_NAME_BOLD,
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
# Margin and indents
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc
self.margins_indents = {}
self.tag_bold_italic = buffer.create_tag(self.TAG_NAME_BOLD_ITALIC,
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.tag_strikethrough = buffer.create_tag(self.TAG_NAME_STRIKETHROUGH,
strikethrough=True)
self.tag_center = buffer.create_tag(self.TAG_NAME_CENTER,
justification=Gtk.Justification.CENTER)
self.tag_wrap_none = buffer.create_tag(self.TAG_NAME_WRAP_NONE,
wrap_mode=Gtk.WrapMode.NONE,
pixels_above_lines=0,
pixels_below_lines=0)
self.tag_plain_text = buffer.create_tag(self.TAG_NAME_PLAIN_TEXT,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False,
justification=Gtk.Justification.LEFT)
self.tag_gray_text = buffer.create_tag(self.TAG_NAME_GRAY_TEXT,
foreground='gray',
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL)
self.tag_code_text = buffer.create_tag(self.TAG_NAME_CODE_TEXT,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False)
self.tag_code_block = buffer.create_tag(self.TAG_NAME_CODE_BLOCK,
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL,
strikethrough=False,
indent=self.get_margin_indent(0, 1)[1])
self.tags_markup = {
self.TAG_NAME_ITALIC: lambda args: self.tag_italic,
self.TAG_NAME_BOLD: lambda args: self.tag_bold,
self.TAG_NAME_BOLD_ITALIC: lambda args: self.tag_bold_italic,
self.TAG_NAME_STRIKETHROUGH: lambda args: self.tag_strikethrough,
self.TAG_NAME_CENTER: lambda args: self.tag_center,
self.TAG_NAME_WRAP_NONE: lambda args: self.tag_wrap_none,
self.TAG_NAME_PLAIN_TEXT: lambda args: self.tag_plain_text,
self.TAG_NAME_GRAY_TEXT: lambda args: self.tag_gray_text,
self.TAG_NAME_CODE_TEXT: lambda args: self.tag_code_text,
self.TAG_NAME_CODE_BLOCK: lambda args: self.tag_code_block,
self.TAG_NAME_MARGIN_INDENT: lambda args: self.get_margin_indent_tag(*args)
}
# Focus mode.
self.tag_unfocused_text = buffer.create_tag(self.TAG_NAME_UNFOCUSED_TEXT,
foreground='gray',
weight=Pango.Weight.NORMAL,
style=Pango.Style.NORMAL)
# Margin and indents.
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc.
self.tags_margins_indents = {}
self.baseline_margin = 0
self.char_width = 0
self.update_margins_indents()
# Style
# Style.
self.on_style_updated()
self.version = 0
# Worker process to handle parsing.
self.parsing = False
self.apply_pending = False
self.parent_conn, child_conn = Pipe()
Process(target=self.parse, args=(child_conn,), daemon=True).start()
GLib.io_add_watch(
self.parent_conn.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.on_parsed)
def on_style_updated(self, *_):
(found, color) = self.text_view.get_style_context().lookup_color('math_text_color')
style_context = self.text_view.get_style_context()
(found, color) = style_context.lookup_color('code_bg_color')
if not found:
(_, color) = self.text_view.get_style_context().lookup_color('foreground_color')
self.mathtext.set_property("foreground", color.to_string())
(_, color) = style_context.lookup_color('background_color')
self.tag_code_text.set_property("background", color.to_string())
self.tag_code_block.set_property("paragraph-background", color.to_string())
def apply(self):
self.version = self.version + 1
if self.text_buffer.get_char_count() < self.max_char_sync:
self.do_apply()
else:
GLib.idle_add(self.do_apply, self.version)
"""Applies markup, parsing it in a worker process if the text has changed.
def do_apply(self, version=None):
if version is not None and version != self.version:
return
In case parsing is already running, it will re-apply once it finishes. This ensure that
the pipe doesn't fill (and block) if multiple requests are made in quick succession."""
if not self.parsing:
self.parsing = True
self.apply_pending = False
text = self.text_buffer.get_slice(
self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False)
if text != self.marked_up_text:
self.parent_conn.send(text)
else:
self.do_apply(text)
else:
self.apply_pending = True
def parse(self, child_conn):
"""Parses markup in a worker process."""
while True:
while True:
try:
text = child_conn.recv()
if not child_conn.poll():
break
except EOFError:
child_conn.close()
return
# List of tuples in the form (tag_name, tag_args, tag_start, tag_end).
result = []
# Find:
# - "_italic_" (italic)
# - "**bold**" (bold)
# - "***bolditalic***" (bold/italic)
# - "~~strikethrough~~" (strikethrough)
# - "`code`" (colorize)
# - "$math$" (colorize)
# - "---" table (wrap/pixels)
regexps = (
(ITALIC, self.TAG_NAME_ITALIC),
(BOLD, self.TAG_NAME_BOLD),
(BOLD_ITALIC, self.TAG_NAME_BOLD_ITALIC),
(STRIKETHROUGH, self.TAG_NAME_STRIKETHROUGH),
(CODE, self.TAG_NAME_CODE_TEXT),
(MATH, self.TAG_NAME_CODE_TEXT),
(TABLE, self.TAG_NAME_WRAP_NONE)
)
for regexp, tag_name in regexps:
matches = re.finditer(regexp, text)
for match in matches:
result.append((tag_name, (), match.start(), match.end()))
# Find:
# - "[description](url)" (gray out)
# - "![description](image_url)" (gray out)
regexps = (
(LINK, self.TAG_NAME_GRAY_TEXT),
(IMAGE, self.TAG_NAME_GRAY_TEXT)
)
for regexp, tag_name in regexps:
matches = re.finditer(regexp, text)
for match in matches:
result.append((tag_name, (), match.start(), match.start("text")))
result.append((tag_name, (), match.end("text"), match.end()))
# Find "<url>" links (gray out).
matches = re.finditer(LINK_ALT, text)
for match in matches:
result.append((
self.TAG_NAME_GRAY_TEXT, (), match.start("text"), match.end("text")))
# Find "---" horizontal rule (center).
matches = re.finditer(HORIZONTAL_RULE, text)
for match in matches:
result.append((
self.TAG_NAME_CENTER, (), match.start("symbols"), match.end("symbols")))
# Find "* list" (offset).
matches = re.finditer(LIST, text)
for match in matches:
# Lists use character+space (eg. "* ").
length = 2
nest = len(match.group("indent").replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
result.append((
self.TAG_NAME_MARGIN_INDENT,
(margin, indent),
match.start("content"),
match.end("content")
))
# Find "1. ordered list" (offset).
matches = re.finditer(ORDERED_LIST, text)
for match in matches:
# Ordered lists use numbers/letters+dot/parens+space (eg. "123. ").
length = len(match.group("prefix")) + 1
nest = len(match.group("indent").replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
result.append((
self.TAG_NAME_MARGIN_INDENT,
(margin, indent),
match.start("content"),
match.end("content")
))
# Find "> blockquote" (offset).
matches = re.finditer(BLOCK_QUOTE, text)
for match in matches:
result.append((self.TAG_NAME_MARGIN_INDENT, (2, -2), match.start(), match.end()))
# Find "# Header" (offset+bold).
matches = re.finditer(HEADER, text)
for match in matches:
margin = -len(match.group("level")) - 1
result.append((
self.TAG_NAME_MARGIN_INDENT, (margin, 0), match.start(), match.end()))
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
# Find "=======" header underline (bold).
matches = re.finditer(HEADER_UNDER, text)
for match in matches:
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
# Find "```" code block tag (offset + colorize paragraph).
matches = re.finditer(markup_regex.CODE_BLOCK, text)
for match in matches:
result.append((
self.TAG_NAME_CODE_BLOCK, (), match.start("block"), match.end("block")))
# Send parsed data back.
child_conn.send((text, result))
def on_parsed(self, _source, _condition):
"""Reads the parsing result from the pipe and triggers any pending apply."""
self.parsing = False
if self.apply_pending:
self.apply() # self.apply clears the apply pending flag.
try:
if self.parent_conn.poll():
self.do_apply(*self.parent_conn.recv())
return True
except EOFError:
return False
def do_apply(self, original_text, result=[]):
"""Applies the result of parsing if the current text matches the original text."""
buffer = self.text_buffer
start = buffer.get_start_iter()
end = buffer.get_end_iter()
offset = 0
text = self.text_buffer.get_slice(start, end, False)
text = buffer.get_slice(start, end, False)
# Apply markup tags.
if text == original_text and text != self.marked_up_text:
buffer.remove_tag(self.tag_italic, start, end)
buffer.remove_tag(self.tag_bold, start, end)
buffer.remove_tag(self.tag_bold_italic, start, end)
buffer.remove_tag(self.tag_strikethrough, start, end)
buffer.remove_tag(self.tag_center, start, end)
buffer.remove_tag(self.tag_plain_text, start, end)
buffer.remove_tag(self.tag_gray_text, start, end)
buffer.remove_tag(self.tag_code_text, start, end)
buffer.remove_tag(self.tag_code_block, start, end)
buffer.remove_tag(self.tag_wrap_none, start, end)
for tag in self.tags_margins_indents.values():
buffer.remove_tag(tag, start, end)
# 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.horizontalrule, start, end)
buffer.remove_tag(self.plaintext, start, end)
buffer.remove_tag(self.table, start, end)
buffer.remove_tag(self.mathtext, start, end)
for tag in self.margins_indents.values():
buffer.remove_tag(tag, start, end)
buffer.remove_tag(self.graytext, start, end)
buffer.remove_tag(self.graytext, start, end)
for tag_name, tag_args, tag_start, tag_end in result:
buffer.apply_tag(
self.tags_markup[tag_name](tag_args),
buffer.get_iter_at_offset(tag_start),
buffer.get_iter_at_offset(tag_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)
matches = re.finditer(self.regex["LINK"], 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.graytext, start_iter, end_iter)
start_iter = buffer.get_iter_at_offset(offset + match.start(2))
end_iter = buffer.get_iter_at_offset(offset + match.end(2))
buffer.apply_tag(self.graytext, 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.horizontalrule, 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. "* ")
length = 2
nest = len(match.group(1).replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
buffer.apply_tag(self.get_margin_indent_tag(margin, indent), 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. ")
length = len(match.group(2)) + 1
nest = len(match.group(1).replace(" ", "\t"))
margin = -length - 2 * nest
indent = -length - 2 * length * nest
buffer.apply_tag(self.get_margin_indent_tag(margin, indent), 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_indent_tag(2, -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())
margin = -len(match.group(1)) - 1
buffer.apply_tag(self.get_margin_indent_tag(margin, 0), 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 "```" code tag (offset)
matches = re.finditer(self.regex["CODE"], 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.get_margin_indent_tag(0, 2), start_iter, end_iter)
buffer.apply_tag(self.plaintext, 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)
# Apply focus mode tag (grey out before/after current sentence).
buffer.remove_tag(self.tag_unfocused_text, start, end)
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)
buffer.apply_tag(self.tag_unfocused_text, start, start_sentence)
buffer.apply_tag(self.tag_unfocused_text, end_sentence, end)
# Margin and indent are cumulative. They differ in two ways:
# * Margin is always in the beginning, which means it effectively only affects the first line
@ -272,15 +342,15 @@ class MarkupHandler:
# Indent is always positive, or 0.
def get_margin_indent_tag(self, margin_level, indent_level):
level = (margin_level, indent_level)
if level not in self.margins_indents:
if level not in self.tags_margins_indents:
margin, indent = self.get_margin_indent(margin_level, indent_level)
tag = self.text_buffer.create_tag(
"margin_indent_{}_{}".format(margin_level, indent_level),
left_margin=margin, indent=indent)
self.margins_indents[level] = tag
self.tags_margins_indents[level] = tag
return tag
else:
return self.margins_indents[level]
return self.tags_margins_indents[level]
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
if baseline_margin is None:
@ -307,6 +377,9 @@ class MarkupHandler:
self.text_view.set_tabs(tab_array)
# Adjust margins and indents
for level, tag in self.margins_indents.items():
for level, tag in self.tags_margins_indents.items():
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
tag.set_properties(left_margin=margin, indent=indent)
def stop(self, *_):
self.parent_conn.close()

View File

@ -6,6 +6,10 @@ class TextViewScroller:
self.scrolled_window = scrolled_window
self.smooth_scroller = None
def can_scroll(self):
vap = self.scrolled_window.get_vadjustment().props
return vap.upper > vap.page_size
def get_scroll_scale(self):
vap = self.scrolled_window.get_vadjustment().props
if vap.upper > vap.page_size:

View File

@ -1,3 +1,8 @@
import logging
LOGGER = logging.getLogger('uberwriter')
class UndoableInsert:
"""Something has been inserted into text_buffer"""
@ -7,6 +12,41 @@ class UndoableInsert:
self.length = length
self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " "))
def undo(self, text_buffer):
offset = self.offset
start = text_buffer.get_iter_at_offset(offset)
stop = text_buffer.get_iter_at_offset(offset + self.length)
text_buffer.place_cursor(start)
text_buffer.delete(start, stop)
def redo(self, text_buffer):
start = text_buffer.get_iter_at_offset(self.offset)
text_buffer.insert(start, self.text)
new_cursor_pos = text_buffer.get_iter_at_offset(self.offset + self.length)
text_buffer.place_cursor(new_cursor_pos)
def merge(self, next_action):
"""Merge a following action into this insert, if possible
can't merge if prev is not another insert
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"""
if not isinstance(next_action, UndoableInsert):
return False
if not self.mergeable or not next_action.mergeable:
return False
if self.offset + self.length != next_action.offset:
return False
whitespace = (' ', '\t')
if self.text in whitespace != next_action.text in whitespace:
return False
self.length += next_action.length
self.text += next_action.text
return True
class UndoableDelete:
"""Something has been deleted from text_buffer"""
@ -20,6 +60,67 @@ class UndoableDelete:
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", " "))
def undo(self, text_buffer):
start = text_buffer.get_iter_at_offset(self.start)
text_buffer.insert(start, self.text)
if self.delete_key_used:
text_buffer.place_cursor(start)
else:
stop = text_buffer.get_iter_at_offset(self.end)
text_buffer.place_cursor(stop)
def redo(self, text_buffer):
start = text_buffer.get_iter_at_offset(self.start)
stop = text_buffer.get_iter_at_offset(self.end)
text_buffer.delete(start, stop)
text_buffer.place_cursor(start)
def merge(self, next_action):
"""Check if this delete can be merged with a previous action
can't merge if prev is not another delete
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"""
if not isinstance(next_action, UndoableDelete):
return False
if not self.mergeable or not next_action.mergeable:
return False
if self.delete_key_used != next_action.delete_key_used:
return False
if self.start != next_action.start and self.start != next_action.end:
return False
whitespace = (' ', '\t')
if self.text in whitespace != next_action.text in whitespace:
return False
if self.delete_key_used:
self.text += next_action.text
self.end += (next_action.end - next_action.start)
else:
self.text = "%s%s" % (next_action.text, next_action.text)
self.start = next_action.start
return True
class UndoableGroup(list):
"""A list of undoable actions, usually corresponding to a single user action"""
def undo(self, text_buffer):
for undoable in reversed(self):
undoable.undo(text_buffer)
def redo(self, text_buffer):
for undoable in self:
undoable.redo(text_buffer)
def merge(self, next_action):
if len(self) == 1:
return self[0].merge(next_action)
else:
return False
class UndoRedoHandler:
"""Manages undo/redo for a given text_buffer.
@ -29,7 +130,7 @@ class UndoRedoHandler:
def __init__(self):
self.undo_stack = []
self.redo_stack = []
self.not_undoable_action = False
self.current_undo_group = None
self.undo_in_progress = False
def undo(self, text_view, _data=None):
@ -39,28 +140,10 @@ class UndoRedoHandler:
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()
undo_action.undo(text_view.get_buffer())
self.undo_in_progress = False
def redo(self, text_view, _data=None):
@ -70,135 +153,71 @@ class UndoRedoHandler:
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()
redo_action.redo(text_view.get_buffer())
self.undo_in_progress = False
def clear(self):
self.undo_stack = []
self.redo_stack = []
def on_begin_user_action(self, _text_buffer):
"""Start of a user action. Refer to TextBuffer's "begin-user-action" signal.
This method must be registered to TextBuffer's "begin-user-action" signal, or called
manually followed by on_end_user_action."""
self.current_undo_group = UndoableGroup()
def on_end_user_action(self, _text_buffer):
"""End of a user action. Refer to TextBuffer's "end-user-action" signal.
This method must be registered to TextBuffer's "end-user-action" signal, or called
manually preceded by on_start_user_action."""
if self.current_undo_group:
self.undo_stack.append(self.current_undo_group)
self.current_undo_group = None
def on_insert_text(self, _text_buffer, text_iter, text, _length):
"""Registers a text insert. Refer to TextBuffer's "insert-text" signal.
"""Records a text insert. Refer to TextBuffer's "insert-text" signal.
This method must be registered to TextBuffer's "insert-text" signal, or called manually."""
This method must be registered to TextBuffer's "insert-text" signal, or called manually
in between on_begin_user_action and on_end_user_action."""
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)
self.__record_undoable(UndoableInsert(text_iter, text, len(text)))
def on_delete_range(self, text_buffer, start_iter, end_iter):
"""Registers a range deletion. Refer to TextBuffer's "delete-range" signal.
"""Records a range deletion. Refer to TextBuffer's "delete-range" signal.
This method must be registered to TextBuffer's "delete-range" signal, or called manually."""
This method must be registered to TextBuffer's "delete-range" signal, or called manually
in between on_begin_user_action and on_end_user_action."""
def can_be_merged(prev, cur):
"""Check if multiple deletions can be merged
self.__record_undoable(UndoableDelete(text_buffer, start_iter, end_iter))
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
def __record_undoable(self, undoable):
"""Records a change, merging it to a previous one if possible."""
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)
return
def __begin_not_undoable_action(self):
"""Toggle to stop recording actions"""
prev_group_undoable = self.current_undo_group[-1] if self.current_undo_group else None
prev_stack_undoable = self.undo_stack[-1] if self.undo_stack else None
self.not_undoable_action = True
if prev_group_undoable:
merged = prev_group_undoable.merge(undoable)
elif prev_stack_undoable:
merged = prev_stack_undoable.merge(undoable)
else:
merged = False
def __end_not_undoable_action(self):
"""Toggle to start recording actions"""
self.not_undoable_action = False
if not merged:
if self.current_undo_group is None:
LOGGER.warning("Recording a change without a user action.")
self.undo_stack.append(undoable)
else:
self.current_undo_group.append(undoable)

View File

@ -1,218 +0,0 @@
#!/usr/bin/env python
# webkit2png - makes screenshots of webpages
# http://www.paulhammond.org/webkit2png
# modified and updated version by @somas95 (Manuel Genovés)
__version__=''
# Copyright (c) 2009 Paul Hammond
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import optparse
import sys
import os
try:
import gi
gi.require_version('WebKit2', '4.0')
gi.require_version('Gtk', '3.0')
from gi.repository import GObject as gobject
from gi.repository import Gtk as gtk
from gi.repository import Pango as pango
from gi.repository import WebKit2 as webkit
mode = "pygtk"
except ImportError:
print("Cannot find python-webkit library files. Are you sure they're installed?")
sys.exit()
class PyGTKBrowser:
def _save_image(self, webview, res, data):
try:
original_surface = webview.get_snapshot_finish(res)
import cairo
new_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1024, 768)
ctx = cairo.Context(new_surface)
ctx.set_source_surface(original_surface, 0, 0)
ctx.paint()
new_surface.write_to_png("test.png")
self.thumbnail = os.path.abspath("test.png")
#return new_surface
except Exception as e:
print("Could not draw thumbnail for %s: %s" % (self.title, str(e)))
def _view_load_finished_cb(self, view, event):
print(event)
if event == webkit.LoadEvent.FINISHED:
pixmap = view.get_snapshot(webkit.SnapshotRegion(1),
webkit.SnapshotOptions(0),
None, self._save_image, None)
#size = get_size()
URL = view.get_main_frame().get_uri()
filename = makeFilename(URL, self.options)
pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, size[0], size[1])
pixbuf.get_from_drawable(pixmap, pixmap.get_colormap(),0,0,0,0,-1,-1)
if self.options.fullsize:
pixbuf.save(filename + "-full.png", "png")
if self.options.thumb or self.options.clipped:
thumbWidth = int(size[0] * self.options.scale)
thumbHeight = int(size[1] * self.options.scale)
thumbbuf = pixbuf.scale_simple(thumbWidth, thumbHeight, gtk.gdk.INTERP_BILINEAR)
if self.options.thumb:
thumbbuf.save(filename + "-thumb.png", "png")
if self.options.clipped:
clipbuf = thumbbuf.subpixbuf(0,thumbHeight-int(self.options.clipheight),
int(self.options.clipwidth),
int(self.options.clipheight))
clipbuf.save(filename + "-clip.png", "png")
gtk.main_quit()
def __init__(self, options, args):
self.options = options
if options.delay:
print("--delay is only supported on Mac OS X (for now). Sorry!")
window = gtk.Window()
window.resize(int(options.initWidth),int(options.initHeight))
self.view = webkit.WebView()
settings = self.view.get_settings()
settings.set_property("auto-load-images", not options.noimages)
self.view.set_settings(settings)
self.view.connect("load_changed", self._view_load_finished_cb)
# window.add(self.view)
# window.show_all()
self.view.load_uri(args[0])
# go go go
gtk.main()
def main():
# parse the command line
usage = """%prog [options] [http://example.net/ ...]
examples:
%prog http://google.com/ # screengrab google
%prog -W 1000 -H 1000 http://google.com/ # bigger screengrab of google
%prog -T http://google.com/ # just the thumbnail screengrab
%prog -TF http://google.com/ # just thumbnail and fullsize grab
%prog -o foo http://google.com/ # save images as "foo-thumb.png" etc
%prog - # screengrab urls from stdin
%prog -h | less # full documentation"""
cmdparser = optparse.OptionParser(usage,version=("webkit2png "+__version__))
# TODO: add quiet/verbose options
cmdparser.add_option("-W", "--width",type="float",default=800.0,
help="initial (and minimum) width of browser (default: 800)")
cmdparser.add_option("-H", "--height",type="float",default=600.0,
help="initial (and minimum) height of browser (default: 600)")
cmdparser.add_option("--clipwidth",type="float",default=200.0,
help="width of clipped thumbnail (default: 200)",
metavar="WIDTH")
cmdparser.add_option("--clipheight",type="float",default=150.0,
help="height of clipped thumbnail (default: 150)",
metavar="HEIGHT")
cmdparser.add_option("-s", "--scale",type="float",default=0.25,
help="scale factor for thumbnails (default: 0.25)")
cmdparser.add_option("-m", "--md5", action="store_true",
help="use md5 hash for filename (like del.icio.us)")
cmdparser.add_option("-o", "--filename", type="string",default="",
metavar="NAME", help="save images as NAME-full.png,NAME-thumb.png etc")
cmdparser.add_option("-F", "--fullsize", action="store_true",
help="only create fullsize screenshot")
cmdparser.add_option("-T", "--thumb", action="store_true",
help="only create thumbnail sreenshot")
cmdparser.add_option("-C", "--clipped", action="store_true",
help="only create clipped thumbnail screenshot")
cmdparser.add_option("-d", "--datestamp", action="store_true",
help="include date in filename")
cmdparser.add_option("-D", "--dir",type="string",default="./",
help="directory to place images into")
cmdparser.add_option("--delay",type="float",default=0,
help="delay between page load finishing and screenshot")
cmdparser.add_option("--noimages", action="store_true",
help="don't load images")
cmdparser.add_option("--debug", action="store_true",
help=optparse.SUPPRESS_HELP)
(options, args) = cmdparser.parse_args()
if len(args) == 0:
cmdparser.print_usage()
return
if options.filename:
if len(args) != 1 or args[0] == "-":
print("--filename option requires exactly one url")
return
if options.scale == 0:
cmdparser.error("scale cannot be zero")
# make sure we're outputing something
if not (options.fullsize or options.thumb or options.clipped):
options.fullsize = True
options.thumb = True
options.clipped = True
# work out the initial size of the browser window
# (this might need to be larger so clipped image is right size)
options.initWidth = (options.clipwidth / options.scale)
options.initHeight = (options.clipheight / options.scale)
if options.width>options.initWidth:
options.initWidth = options.width
if options.height>options.initHeight:
options.initHeight = options.height
PyGTKBrowser(options, args)
def makeFilename(self, URL, options):
# make the filename
if options.filename:
filename = options.filename
elif options.md5:
try:
import md5
except ImportError:
print("--md5 requires python md5 library")
filename = md5.new(URL).hexdigest()
else:
import re
filename = re.sub('\W','',URL)
filename = re.sub('^http','',filename)
if options.datestamp:
import time
now = time.strftime("%Y%m%d")
filename = now + "-" + filename
import os
dir = os.path.abspath(os.path.expanduser(options.dir))
return os.path.join(dir,filename)
if __name__ == "__main__": main()