Commit 60ae4358 authored by Lains's avatar Lains
Browse files

Merge branch 'master' into 'file-image-embed-syntax'

parents cac65da9 6c262974
Pipeline #264197 passed with stage
in 4 minutes and 2 seconds
include: 'https://gitlab.gnome.org/GNOME/citemplates/raw/master/flatpak/flatpak_ci_initiative.yml'
flatpak:
image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:3.36'
image: 'registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:3.38'
variables:
MANIFEST_PATH: "build-aux/flatpak/org.gnome.gitlab.somas.Apostrophe.json"
MESON_ARGS: "-Dprofile=Devel"
......
Version 2.3
~~~~~~~~~~~~~~~
Released: 2021-03-01
Features:
- New export dialog
- Improved Reveal.js slideshow exports
- Better input/output handling
- Inhibit logout if the document is not saved
- Migrate to xelatex
- Better internal path handling
- New animated progressbar on saving
- Strict sandboxing support
Bugfixes:
- Undo/redo improvements
- Fixes for bad markdown parsing of unordered lists
Version 2.2.0.3
~~~~~~~~~~~~~~~
Released: 2020-05-04
......
......@@ -12,12 +12,12 @@
import gi
from apostrophe.main_window import MainWindow
gi.require_version('Gtk', '3.0')
gi.require_version('Handy', '1')
from gi.repository import GLib, Gio, Gtk, Handy
from apostrophe.main_window import MainWindow
from apostrophe.settings import Settings
from apostrophe.helpers import set_up_logging
from apostrophe.preferences_dialog import PreferencesDialog
......@@ -176,7 +176,6 @@ class Application(Gtk.Application):
if not self.window:
# Windows are associated with the application
# when the last one is closed the application shuts down
# self.window = Window(application=self, title="Apostrophe")
self.window = MainWindow(self)
if self._application_id == 'org.gnome.gitlab.somas.Apostrophe.Devel':
......@@ -184,8 +183,8 @@ class Application(Gtk.Application):
self.window.present()
def do_handle_local_options(self, options):
if (options.contains("verbose") or
self._application_id == 'org.gnome.gitlab.somas.Apostrophe.Devel'):
if options.contains("verbose") or self._application_id \
== 'org.gnome.gitlab.somas.Apostrophe.Devel':
set_up_logging(1)
return -1
......@@ -266,7 +265,7 @@ class Application(Gtk.Application):
def on_export(self, _action, value):
self.window.open_export(value.get_string())
def on_advanced_export(self, _action, value):
def on_advanced_export(self, _action, _value):
self.window.open_advanced_export()
def on_copy_html(self, _action, _value):
......
# ApostropheAutoCorrect
# The Apostrophe Auto Correct is a auto correction
# mechanism to prevent stupid typos
# CURRENTLY DISABLED
import os
import pickle
import configparser
import xml.etree.ElementTree as ET
from gi.repository import Gtk, Gdk
import enchant
from apostrophe import pressagio
# import apostrophe.pressagio.predictor
# import apostrophe.pressagio.tokenizer
# import apostrophe.pressagio.dbconnector
# import apostrophe.pressagio.context_tracker
# import apostrophe.pressagio.callback
# from Levenshtein import distance
from apostrophe.helpers import get_media_path
# Define and create PresageCallback object
class PressagioCallback(pressagio.callback.Callback):
def __init__(self, buffer):
super().__init__()
self.buffer = buffer
def past_stream(self):
return self.buffer
def future_stream(self):
return ''
class AutoCorrect:
def show_bubble(self, iterator, suggestion):
self.suggestion = suggestion
if self.bubble:
self.bubble_label.set_text(suggestion)
else:
pos = self.text_view.get_iter_location(iterator)
pos_adjusted = self.text_view.buffer_to_window_coords(
Gtk.TextWindowType.TEXT, pos.x, pos.y + pos.height)
self.bubble_eventbox = Gtk.EventBox.new()
self.bubble = Gtk.Grid.new()
self.bubble.set_name("AutoCorrect")
self.bubble_eventbox.add(self.bubble)
self.bubble_eventbox.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.bubble_eventbox.connect("button_press_event", self.clicked_bubble)
self.text_view.add_child_in_window(self.bubble_eventbox,
Gtk.TextWindowType.TEXT,
pos_adjusted[0], pos_adjusted[1])
self.bubble_label = Gtk.Label.new(suggestion)
self.bubble.attach(self.bubble_label, 0, 0, 1, 1)
self.bubble_close_eventbox = Gtk.EventBox.new()
self.bubble_close_eventbox.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.bubble_close_eventbox.connect("button_press_event", self.clicked_close)
close = Gtk.Image.new_from_icon_name('dialog-close', Gtk.IconSize.SMALL_TOOLBAR)
self.bubble_close_eventbox.add(close)
self.bubble.attach(self.bubble_close_eventbox, 1, 0, 1, 1)
self.bubble_eventbox.show_all()
def clicked_bubble(self, _widget, _data=None):
self.accept_suggestion()
def clicked_close(self, _widget, _data=None):
self.destroy_bubble()
def suggest(self, stump, context):
if self.enchant_dict.check(stump):
self.destroy_bubble()
return
self.callback.buffer = ' '.join(context) + ' ' + stump
self.callback.buffer = self.callback.buffer.lstrip().rstrip()
predictions = []
if self.use_pressagio:
predictions = self.prsgio.predict(6, None)
prediction = None
if not predictions:
if self.enchant_dict.check(stump):
self.destroy_bubble()
return
predictions = self.enchant_dict.suggest(stump)
suggestions_map = []
for suggestion in predictions:
if suggestion in self.frequency_dict:
suggestions_map.append({'suggestion': suggestion,
'freq': self.frequency_dict[suggestion]})
else:
suggestions_map.append({'suggestion': suggestion, 'freq': 0})
suggestions_map.sort(key=lambda x: x['freq'])
suggestions_map.reverse()
prediction = suggestions_map[0]
print(predictions)
prediction = predictions[0]
else:
prediction = predictions[0].word
anchor_iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
anchor_iter.backward_visible_word_start()
if len(stump) >= 1:
self.show_bubble(anchor_iter, prediction)
def destroy_bubble(self, *_args):
if not self.bubble:
return
self.bubble.destroy()
self.bubble = None
self.suggestion = ''
def get_frequency_dict(self, _language):
self.frequency_dict = {}
pp_pickled = get_media_path("frequency_dict_" + self.language + ".pickle")
if pp_pickled and os.path.isfile(pp_pickled):
frequency_file = open(pp_pickled, 'rb')
self.frequency_dict = pickle.load(frequency_file)
frequency_file.close()
else:
pp = get_media_path('wordlists/en_us_wordlist.xml')
frequencies = ET.parse(pp)
root = frequencies.getroot()
for child in root:
self.frequency_dict[child.text] = int(child.attrib['f'])
frequency_file = open('pickled_dict', 'wb+')
pickle.dump(self.frequency_dict, frequency_file)
frequency_file.close()
def accept_suggestion(self, append=""):
print("called")
curr_iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
start_iter = curr_iter.copy()
start_iter.backward_visible_word_start()
self.buffer.delete(start_iter, curr_iter)
self.buffer.insert_at_cursor(self.suggestion + append)
self.destroy_bubble()
def key_pressed(self, _widget, event):
if not self.bubble:
return False
if event.keyval in [Gdk.KEY_Escape, Gdk.KEY_BackSpace]:
self.destroy_bubble()
return False
def text_insert(self, buffer, location,
text, _length, _data=None):
# check if at end of a word
# if yes, check if suggestion available
# then display suggetion
if self.suggestion and text in [' ', '\t', '\n', '.', '?', '!',
',', ';', '\'', '"', ')', ':']:
self.accept_suggestion(append=text)
location.assign(self.buffer.get_iter_at_mark(self.buffer.get_insert()))
elif location.ends_word():
iter_start = location.copy()
iter_start.backward_visible_word_starts(3)
text = buffer.get_text(iter_start, location, False)
words = text.split()
self.suggest(words[-1], words[0:-1])
def disable(self):
self.disabled = True
def enable(self):
self.disabled = False
def set_language(self, language):
print("Language changed to: %s" % language)
# handle 2 char cases e.g. "en"
if len(language) == 2:
if "en":
language = "en_US"
if self.language == language:
return
self.language = language
print("Language changing")
config_file = get_media_path("pressagio_config.ini")
pres_config = configparser.ConfigParser()
pres_config.read(config_file)
pres_config.set("Database", "database",
get_media_path("corpora/" + self.language + ".sqlite"))
self.context_tracker = pressagio.context_tracker.ContextTracker(
pres_config, self.predictor_registry, self.callback)
self.prsgio = self.predictor_registry[0]
self.enchant_dict = enchant.Dict(self.language)
def __init__(self, textview, textbuffer):
self.text_view = textview
self.buffer = textbuffer
self.suggestion = ""
self.bubble = self.bubble_label = None
self.buffer.connect_after('insert-text', self.text_insert)
self.text_view.connect('key-press-event', self.key_pressed)
self.language = "en_US"
self.frequency_dict = {}
self.get_frequency_dict(self.language)
self.enchant_dict = enchant.Dict(self.language)
self.use_pressagio = False
config_file = get_media_path("pressagio_config.ini")
pres_config = configparser.ConfigParser()
pres_config.read(config_file)
pres_config.set("Database", "database",
get_media_path("corpora/" + self.language + ".sqlite"))
self.callback = PressagioCallback("")
self.predictor_registry = pressagio.predictor.PredictorRegistry(pres_config)
self.context_tracker = pressagio.context_tracker.ContextTracker(
pres_config, self.predictor_registry, self.callback)
self.prsgio = self.predictor_registry[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
### DO NOT EDIT THIS FILE ###
'''Enhances builder connections, provides object to access glade objects'''
import inspect
import functools
import logging
from xml.etree.cElementTree import ElementTree
import gi
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
from gi.repository import GObject, Gtk # pylint: disable=E0611
LOGGER = logging.getLogger('apostrophe')
# this module is big so uses some conventional prefixes and postfixes
# *s list, except self.widgets is a dictionary
# *_dict dictionary
# *name string
# ele_* element in a ElementTree
# pylint: disable=R0904
# the many public methods is a feature of Gtk.Builder
class Builder(Gtk.Builder):
''' extra features
connects glade defined handler to default_handler if necessary
auto connects widget to handler with matching name or alias
auto connects several widgets to a handler via multiple aliases
allow handlers to lookup widget name
logs every connection made, and any on_* not made
'''
def __init__(self):
Gtk.Builder.__init__(self)
self.widgets = {}
self.glade_handler_dict = {}
self.connections = []
self._reverse_widget_dict = {}
# pylint: disable=R0201
# this is a method so that a subclass of Builder can redefine it
def default_handler(self,
handler_name, filename, *args, **kwargs):
'''helps the apprentice guru
glade defined handlers that do not exist come here instead.
An apprentice guru might wonder which signal does what he wants,
now he can define any likely candidates in glade and notice which
ones get triggered when he plays with the project.
this method does not appear in Gtk.Builder'''
LOGGER.debug('''tried to call non-existent function:%s()
expected in %s
args:%s
kwargs:%s''', handler_name, filename, args, kwargs)
# pylint: enable=R0201
def get_name(self, widget):
''' allows a handler to get the name (id) of a widget
this method does not appear in Gtk.Builder'''
return self._reverse_widget_dict.get(widget)
def add_from_file(self, filename):
'''parses xml file and stores wanted details'''
Gtk.Builder.add_from_file(self, filename)
# extract data for the extra interfaces
tree = ElementTree()
tree.parse(filename)
ele_widgets = tree.getiterator("object")
for ele_widget in ele_widgets:
name = ele_widget.attrib['id']
widget = self.get_object(name)
# populate indexes - a dictionary of widgets
self.widgets[name] = widget
# populate a reversed dictionary
self._reverse_widget_dict[widget] = name
# populate connections list
ele_signals = ele_widget.findall("signal")
connections = [
(name,
ele_signal.attrib['name'],
ele_signal.attrib['handler']) for ele_signal in ele_signals]
if connections:
self.connections.extend(connections)
ele_signals = tree.getiterator("signal")
for ele_signal in ele_signals:
self.glade_handler_dict.update(
{ele_signal.attrib["handler"]: None})
def connect_signals(self, callback_obj):
'''connect the handlers defined in glade
reports successful and failed connections
and logs call to missing handlers'''
filename = inspect.getfile(callback_obj.__class__)
callback_handler_dict = dict_from_callback_obj(callback_obj)
connection_dict = {}
connection_dict.update(self.glade_handler_dict)
connection_dict.update(callback_handler_dict)
for item in connection_dict.items():
if item[1] is None:
# the handler is missing so reroute to default_handler
handler = functools.partial(
self.default_handler, item[0], filename)
connection_dict[item[0]] = handler
# replace the run time warning
LOGGER.warning("expected handler '%s' in %s",
item[0], filename)
# connect glade define handlers
Gtk.Builder.connect_signals(self, connection_dict)
# let's tell the user how we applied the glade design
for connection in self.connections:
widget_name, signal_name, handler_name = connection
LOGGER.debug("connect builder by design '%s', '%s', '%s'",
widget_name, signal_name, handler_name)
def get_ui(self, callback_obj=None, by_name=True):
'''Creates the ui object with widgets as attributes
connects signals by 2 methods
this method does not appear in Gtk.Builder'''
result = UiFactory(self.widgets)
# Hook up any signals the user defined in glade
if callback_obj is not None:
# connect glade define handlers
self.connect_signals(callback_obj)
if by_name:
auto_connect_by_name(callback_obj, self)
return result
# pylint: disable=R0903
# this class deliberately does not provide any public interfaces
# apart from the glade widgets
class UiFactory():
''' provides an object with attributes as glade widgets'''
def __init__(self, widget_dict):
self._widget_dict = widget_dict
for (widget_name, widget) in widget_dict.items():
setattr(self, widget_name, widget)
# Mangle any non-usable names (like with spaces or dashes)
# into pythonic ones
cannot_message = """cannot bind ui.%s, name already exists
consider using a pythonic name instead of design name '%s'"""
consider_message = """consider using a pythonic name instead of design name '%s'"""
for (widget_name, widget) in widget_dict.items():
pyname = make_pyname(widget_name)
if pyname != widget_name:
if hasattr(self, pyname):
LOGGER.debug(cannot_message, pyname, widget_name)
else:
LOGGER.debug(consider_message, widget_name)
setattr(self, pyname, widget)
def iterator():
'''Support 'for o in self' '''
return iter(widget_dict.values())
setattr(self, '__iter__', iterator)
def __getitem__(self, name):
'access as dictionary where name might be non-pythonic'
return self._widget_dict[name]
# pylint: enable=R0903
def make_pyname(name):
''' mangles non-pythonic names into pythonic ones'''
pyname = ''
for character in name:
if (character.isalpha() or character == '_' or
(pyname and character.isdigit())):
pyname += character
else:
pyname += '_'
return pyname
# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
# need to reimplement inspect.getmembers. GObject introspection doesn't
# play nice with it.
def getmembers(obj, check):
members = []
for k in dir(obj):
try:
attr = getattr(obj, k)
except:
continue
if check(attr):
members.append((k, attr))
members.sort()
return members
def dict_from_callback_obj(callback_obj):
'''a dictionary interface to callback_obj'''
methods = getmembers(callback_obj, inspect.ismethod)
aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
# a method may have several aliases
# ~ @alias('on_btn_foo_clicked')
# ~ @alias('on_tool_foo_activate')
# ~ on_menu_foo_activate():
# ~ pass
alias_groups = [(x.aliases, x) for x in aliased_methods]
aliases = []
for item in alias_groups:
for alias in item[0]:
aliases.append((alias, item[1]))
dict_methods = dict(methods)
dict_aliases = dict(aliases)
results = {}
results.update(dict_methods)
results.update(dict_aliases)
return results
def auto_connect_by_name(callback_obj, builder):
'''finds handlers like on_<widget_name>_<signal> and connects them
i.e. find widget,signal pair in builder and call
widget.connect(signal, on_<widget_name>_<signal>)'''
callback_handler_dict = dict_from_callback_obj(callback_obj)
for item in builder.widgets.items():
(widget_name, widget) = item
signal_ids = []
try:
widget_type = type(widget)
while widget_type:
signal_ids.extend(GObject.signal_list_ids(widget_type))
widget_type = GObject.type_parent(widget_type)
except RuntimeError: # pylint wants a specific error
pass
signal_names = [GObject.signal_name(sid) for sid in signal_ids]
# Now, automatically find any the user didn't specify in glade
for sig in signal_names:
# using convention suggested by glade
sig = sig.replace("-", "_")
handler_names = ["on_%s_%s" % (widget_name, sig)]
# Using the convention that the top level window is not
# specified in the handler name. That is use
# on_destroy() instead of on_windowname_destroy()
if widget is callback_obj:
handler_names.append("on_%s" % sig)
do_connect(item, sig, handler_names,
callback_handler_dict, builder.connections)
log_unconnected_functions(callback_handler_dict, builder.connections)
def do_connect(item, signal_name, handler_names,
callback_handler_dict, connections):
'''connect this signal to an unused handler'''
widget_name, widget = item
for handler_name in handler_names:
target = handler_name in callback_handler_dict.keys()
connection = (widget_name, signal_name, handler_name)
duplicate = connection in connections
if target and not duplicate:
widget.connect(signal_name, callback_handler_dict[handler_name])
connections.append(connection)
LOGGER.debug("connect builder by name '%s','%s', '%s'",
widget_name, signal_name, handler_name)
def log_unconnected_functions(callback_handler_dict, connections):