even with master

ft.librem5
Manuel Genovés 2019-05-04 21:15:24 +02:00
commit bc24251461
70 changed files with 2513 additions and 4078 deletions

8
.gitignore vendored
View File

@ -5,15 +5,13 @@ build/
debian/uberwriter/DEBIAN
debian/uberwriter/opt
debian/uberwriter/usr
bin/
flatpak/*
!flatpak/fonts-download
!flatpak/pandoc-download
!flatpak/pip-download
!flatpak/uberwriter.json
!flatpak/de.wolfvollprecht.UberWriter.*
!flatpak/flatpak_texlive.json
!flatpak/texlive_install.sh
!flatpak/python3-enchant.json
!flatpak/python3-packages.json
*.py~
data/ui/shortcut_handlers
*.ui~
@ -21,3 +19,5 @@ data/ui/shortcut_handlers
*.glade~
dist/uberwriter-2.0b0-py3.7.egg
builddir/*
dist/
uberwriter.egg-info

View File

@ -1,6 +1,9 @@
all:
python3 ./setup.py build
.PHONY: flatpak-user-install flatpak-generate-python-modules
install:
python3 ./setup.py install --prefix=/app --skip-build --optimize=1
flatpak-user-install:
cd flatpak; flatpak-builder --force-clean --install --user _build uberwriter.json
flatpak-generate-python-modules:
# gtkspellcheck's setup.py wants enchant to already be installed
flatpak-pip-generator --output flatpak/python3-enchant.json pyenchant
flatpak-pip-generator --output flatpak/python3-packages.json `grep -v enchant requirements.txt`

View File

@ -6,7 +6,7 @@ pkgdesc='A distraction free Markdown editor for GNU/Linux made with GTK+'
arch=('any')
url='http://uberwriter.github.io/uberwriter/'
license=('GPL3')
depends=('gtk3' 'pandoc' 'python-gtkspellcheck')
depends=('gtk3' 'pandoc' 'gspell')
makedepends=('python-setuptools')
optdepends=('texlive-core' 'otf-fira-mono: Recommended font')
provides=("$_pkgname")

View File

@ -34,8 +34,7 @@ but you'll need to install and compile the schemas before:
It's also possible to build, run and debug a flatpak package. You'll need flatpak-builder for this:
- cd to the flatpak dir of the repo
- `flatpak-builder --install --force-clean some_folder_name uberwriter.json` (this installs and cleans the build folder)
- `make flatpak-user-install` (this installs the Flatpak)
- `flatpak run de.wolfvollprecht.UberWriter`
If you can't find Uberwriter after this, it's due to a Flatpak bug. Try to export it to a local repo before installing it:
@ -54,4 +53,4 @@ If you want to update an existing installation, just run
You can also debug it with the following: `flatpak-builder --run --share=network some_folder_name uberwriter.json sh`
If you want to install it using setuptools, simply run `python3 setup.py build install`
If you want to install it using setuptools, simply run `python3 setup.py build install`

View File

@ -1 +0,0 @@

59
bin/uberwriter 100755
View File

@ -0,0 +1,59 @@
#!/usr/bin/python3
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
### 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()

1
configure vendored
View File

@ -1 +0,0 @@

View File

@ -28,6 +28,13 @@
</screenshot>
</screenshots>
<releases>
<release date="2019-04-17" version="2.2.0-beta1">
<description>
<ul>
<li>...</li>
</ul>
</description>
</release>
<release date="2019-03-10" version="2.1.5">
<description>
<ul>

View File

@ -2,37 +2,57 @@
<schemalist>
<enum id='de.wolfvollprecht.UberWriter.Stat'>
<value nick='characters' value='0' />
<value nick='words' value='1' />
<value nick='sentences' value='2' />
<value nick='paragraphs' value='3' />
<value nick='read_time' value='4' />
</enum>
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
<key name='dark-mode-auto' type='b'>
<default>true</default>
<summary>Set dark mode automatically</summary>
<description>
Whether dark mode depends on the system theme, or is set to what the user specifies.
</description>
</key>
<key name='dark-mode' type='b'>
<default>false</default>
<summary>Dark mode</summary>
<summary>Force dark mode</summary>
<description>
If enabled, the window will be dark themed
If disabled, the window will be light themed
asked to install them manually.
Enable or disable the dark mode.
</description>
</key>
<key name='spellcheck' type='b'>
<default>true</default>
<summary>Spellcheck</summary>
<summary>Check spelling while typing</summary>
<description>
Enable/disable spellchecking in the application.
Enable or disable spellchecking.
</description>
</key>
<key name='gradient-overlay' type='b'>
<default>false</default>
<summary>Show gradient overlay</summary>
<summary>Draw scroll gradient</summary>
<description>
Show a gradient overlay over the text at the top anf bottom of the window.
It can cause performance problems to some users
It can cause performance problems to some users.
</description>
</key>
<key name='input-format' type='s'>
<default>"markdown"</default>
<summary>Input format</summary>
<description>
Input format to use when previewing and exporting using Pandoc.
</description>
</key>
<key name='poll-motion' type='b'>
<default>true</default>
<summary>Allow Uberwriter to poll cursor motion</summary>
<description>
Used for hidding the headerbar after 3 seconds if the cursor is not moving.
Hide the header and status bars if the cursor is not moving.
</description>
</key>
<key name='open-file-path' type='s'>
@ -42,6 +62,13 @@
Open file paths of the current session
</description>
</key>
<key name='stat-default' enum='de.wolfvollprecht.UberWriter.Stat'>
<default>"words"</default>
<summary>Default statistic</summary>
<description>
Which statistic is shown on the main window.
</description>
</key>
</schema>

View File

@ -1,213 +0,0 @@
/*
TODO:
Look into compiling resources with glib-compile-resource etc. for
inclusion in templates
*/
@binding-set editor-bindings {
bind "<ctl>i" { "insert-italic" () };
bind "<ctl>b" { "insert-bold" () };
bind "<ctl>r" { "insert-hrule" () };
bind "<ctl>u" { "insert-ulistitem" () };
bind "<ctl>h" { "insert-heading" () };
bind "<ctl>z" { "undo" () };
bind "<ctl>y" { "redo" () };
bind "<ctl><shift>d" { "insert-strikeout" () };
/*bind "<ctl>t" { "insert-at-cursor" ('[ ] ') };*/
bind "<ctl><shift>z" { "redo" () };
}
/* Main window and text colors */
.uberwriter_window {
/*border-radius: 7px 7px 3px 3px;*/
background: @background_color;
caret-color: @foreground_color;
}
.uberwriter_window.small .uberwriter-editor {
font-family: 'Fira Mono', monospace;
font-size: 12px;
}
.uberwriter_window grid {
background-color: @background_color;
}
#UberwriterWindow.medium .uberwriter-editor {
font-family: 'Fira Mono', monospace;
font-size: 15px;
}
#UberwriterWindow.large .uberwriter-editor {
font-family: 'Fira Mono', monospace;
font-size: 18px;
}
#titlebar_revealer {
padding: 0;
}
.scrollbars-junction,
.scrollbar.trough {
background: transparent;
}
#titlebar_container {
background: @background_color;
}
.uberwriter-editor {
border: none;
background-color: transparent;
text-decoration-color: #ff0000;
/*-GtkWidget-cursor-color: shade(#4D9FCE, 0.9);*/
/*-GtkWidget-cursor-aspect-ratio: 0.1;*/
-gtk-key-bindings: editor-bindings;
}
.uberwriter-editor text {
background-color: @background_color;
color: @foreground_color;
caret-color: @foreground_color;
}
.uberwriter-editor:selected {
background-color: #4D9FCE;
color: #FFF;
}
.uberwriter-editor button {
margin: 0;
padding: 0;
/*background: #CCC;*/
}
.uberwriter-editor toolbar {
/*background: transparent;*/
border: none;
padding: 0;
}
.status_bar_box label {
color: #666;
}
.status_bar_box button {
/* finding reset */
background-color: @background_color;
text-shadow: inherit;
/*icon-shadow: inherit;*/
box-shadow: initial;
background-clip: initial;
background-origin: initial;
background-size: initial;
background-position: initial;
background-repeat: initial;
background-image: initial;
border-image-source: initial;
border-image-repeat: initial;
border-image-slice: initial;
border-image-width: initial;
border-style: none;
-button-images: true;
border-radius: 2px;
color: #666;
padding: 3px 5px;
transition: 100ms ease-in;
}
.status_bar_box button:hover,
.status_bar_box button:checked {
transition: 0s ease-in;
color: @background_color;
background-color: #666;
}
.status_bar_box button:hover label,
.status_bar_box button:checked label {
color: @background_color;
}
.status_bar_box button:active {
color: #EEE;
background-color: #EEE;
background-image: none;
box-shadow: 0 0 2px rgba(0,0,0,0.4)
}
.status_bar_box separator {
border-color: #999;
border-right: none;
}
#PreviewMenuItem image {
border-radius: 2px;
color: #666;
padding: 3px 5px;
border: none;
background: #FFF;
}
#UberwriterWindow treeview {
padding: 3px 3px 3px 3px;
}
#LexikonBubble {
/*font: serif 10;*/
font-family: serif;
font-size: 10px;
background: @background_color;
border-radius: 4px;
border-color: @background_color;
margin: 5px;
padding: 5px;
}
/* .QuickPreviewPopup {
padding: 5px;
margin: 5px;
border: 1px solid #333;
background: @ligth_bg;
border-radius: 3px;
border-color: @background_color;
} */
#LexikonBubble label {
/*padding: 5px;*/
}
#LexikonBubble {
background-color: @background_color;
border: 5px solid @background_color;
}
#LexikonBubble .lexikon_heading {
/*font: serif 12;*/
font-family: serif;
font-size: 12px;
padding-bottom: 5px;
padding-top: 5px;
font-weight: bold;
padding-left: 10px;
}
#LexikonBubble .lexikon_num {
padding-right: 5px;
padding-left: 20px;
}
.QuickPreviewPopup {
background-color: @background_color;
}
.QuickPreviewPopup grid {
background-color: @background_color;
color: @foreground_color;
border-color: @background_color;
}
.QuickPreviewPopup label {
color: @foreground_color;
}

View File

@ -0,0 +1,180 @@
/*
TODO:
Look into compiling resources with glib-compile-resource etc. for
inclusion in templates
*/
@binding-set editor-bindings {
bind "<ctl>i" { "insert-italic" () };
bind "<ctl>b" { "insert-bold" () };
bind "<ctl>r" { "insert-hrule" () };
bind "<ctl>u" { "insert-listitem" () };
bind "<ctl>h" { "insert-header" () };
bind "<ctl>z" { "undo" () };
bind "<ctl>y" { "redo" () };
bind "<ctl><shift>d" { "insert-strikethrough" () };
/*bind "<ctl>t" { "insert-at-cursor" ('[ ] ') };*/
bind "<ctl><shift>z" { "redo" () };
}
@define-color math_text_color mix(@theme_fg_color, #00b5ff, 0.15);
/* Main window and text colors */
.uberwriter-window {
/*border-radius: 7px 7px 3px 3px;*/
background: @theme_base_color;
color: @theme_fg_color;
caret-color: @theme_fg_color;
}
.uberwriter-window .uberwriter-editor {
font-family: 'Fira Mono', monospace;
font-size: 16px;
}
.uberwriter-window.small .uberwriter-editor {
font-size: 14px;
}
.uberwriter-window.large .uberwriter-editor {
font-size: 18px;
}
#titlebar-revealer {
padding: 0;
}
.scrollbars-junction,
.scrollbar.trough {
background: transparent;
}
#titlebar-container {
background: @theme_base_color;
}
.uberwriter-editor {
border: none;
background-color: transparent;
text-decoration-color: @error_color;
-gtk-key-bindings: editor-bindings;
}
.uberwriter-editor text {
background-color: @theme_base_color;
color: @theme_fg_color;
caret-color: @theme_fg_color;
}
.uberwriter-editor text selection {
background-color: @theme_selected_bg_color;
color: @theme_selected_fg_color;
}
.uberwriter-editor button {
margin: 0;
padding: 0;
/*background: #CCC;*/
}
.uberwriter-editor toolbar {
/*background: transparent;*/
border: none;
padding: 0;
}
.stats-counter {
color: alpha(@theme_fg_color, 0.6);
background-color: @theme_base_color;
text-shadow: inherit;
box-shadow: initial;
background-clip: initial;
background-origin: initial;
background-size: initial;
background-position: initial;
background-repeat: initial;
background-image: initial;
border-image-source: initial;
border-image-repeat: initial;
border-image-slice: initial;
border-image-width: initial;
border-style: none;
padding: 0px 16px;
transition: 100ms ease-in;
}
.stats-counter:hover,
.stats-counter:checked {
color: @theme_fg_color;
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;
}
#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 {
font-family: serif;
font-size: 12px;
padding-bottom: 5px;
padding-top: 5px;
font-weight: bold;
padding-left: 10px;
}
#LexikonBubble .lexikon-num {
padding-right: 5px;
padding-left: 20px;
}
.quick-preview-popup {
background-color: @theme_bg_color;
}
.quick-preview-popup grid {
background-color: @theme_bg_color;
color: @theme_fg_color;
border-color: @theme_bg_color;
}
.quick-preview-popup label {
color: @theme_fg_color;
}

View File

@ -1,5 +0,0 @@
@define-color foreground_color #2e3436;
@define-color background_color #f6f5f4;
@define-color math_text_color #00364c;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #eeeeec;
@define-color background_color #353535;
@define-color math_text_color #ffc9b3;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #3b3e45;
@define-color background_color #f5f6f7;
@define-color math_text_color #00364c;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #d3dae3;
@define-color background_color #383c4a;
@define-color math_text_color #ffc9b3;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #3b3e45;
@define-color background_color #f5f6f7;
@define-color math_text_color #00364C;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #000000;
@define-color background_color #ffffff;
@define-color math_text_color #000000;
@import url("_gtk_base.css");

View File

@ -1,5 +0,0 @@
@define-color foreground_color #ffffff;
@define-color background_color #000000;
@define-color math_text_color #ffffff;
@import url("_gtk_base.css");

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("base.css");
:root {
--text-color: #2e3436;

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("web/web__base.css");
:root {
--text-color: #eeeeec;

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("web/web__base.css");
:root {
--text-color: #3b3e45;

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("web/web__base.css");
:root {
--text-color: #d3dae3;

View File

@ -0,0 +1 @@
@import url("web/web_arc.css");

View File

@ -1,11 +1,11 @@
@font-face {
font-family: fira-sans;
src: url("../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2");
src: url("../../fonts/fira-sans-v9-vietnamese_latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2");
}
@font-face {
font-family: fira-mono;
src: url("../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2");
src: url("../../fonts/fira-mono-v7-latin_cyrillic-ext_cyrillic_greek-ext_latin-ext_greek-regular.woff2") format("woff2");
}
@font-face {
@ -56,8 +56,9 @@ body {
background-color: var(--background-color);
font-family: "Fira Sans", fira-sans, sans-serif, color-emoji;
line-height: 1.5;
text-size-adjust: 100%;
word-wrap: break-word;
max-width: 978px;
margin: auto;
padding: 2em;
}

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("web/web__base.css");
a {
text-decoration: underline;

View File

@ -1,4 +1,4 @@
@import url("_web_base.css");
@import url("web/web__base.css");
a {
text-decoration: underline;

View File

@ -1 +0,0 @@
@import url("web_arc.css");

View File

@ -6,7 +6,12 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="hexpand">True</property>
<property name="row_spacing">2</property>
<child>
<object class="GtkGrid" id="advanced_export_grid">
<property name="visible">True</property>
@ -19,6 +24,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_right">8</property>
<property name="label_xalign">0</property>
<property name="shadow_type">out</property>
<child>
@ -30,8 +36,9 @@
<object class="GtkBox" id="box6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="margin_right">8</property>
<property name="margin_top">4</property>
<property name="margin_bottom">4</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="smart">
@ -42,7 +49,6 @@
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Pandoc can automatically make "--" to a long dash and more</property>
<property name="halign">start</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
@ -120,7 +126,7 @@
</child>
<child>
<object class="GtkCheckButton" id="incremental">
<property name="label" translatable="yes">Slideshow incremental bullets</property>
<property name="label" translatable="yes">Slideshow Incremental Bullets</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
@ -158,6 +164,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_right">8</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="frame1">
@ -174,18 +181,16 @@
<object class="GtkBox" id="box3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="margin_top">4</property>
<property name="margin_bottom">4</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="highlight">
<property name="label" translatable="yes">Highlight syntax</property>
<property name="use_action_appearance">False</property>
<property name="label" translatable="yes">Highlight Syntax</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
@ -198,11 +203,13 @@
<object class="GtkBox" id="box2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">4</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a color theme for syntax highlighting</property>
<property name="margin_left">4</property>
<property name="label" translatable="yes">Highlight style </property>
</object>
<packing>
@ -216,6 +223,8 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a color theme for syntax highlighting</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="active">0</property>
<property name="active_id">0</property>
<items>
@ -250,7 +259,7 @@
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Syntax highlighting&lt;/b&gt; (HTML, LaTeX)</property>
<property name="label" translatable="yes">&lt;b&gt;Syntax Highlighting&lt;/b&gt; (HTML, LaTeX)</property>
<property name="use_markup">True</property>
</object>
</child>
@ -271,13 +280,40 @@
<object class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">4</property>
<property name="margin_bottom">4</property>
<property name="left_padding">12</property>
<child>
<object class="GtkFileChooserButton" id="bib_filechooser">
<object class="GtkBox" id="box7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<child>
<object class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a bibliography file</property>
<property name="label" translatable="yes">File</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFileChooserButton" id="bib_filechooser">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a bibliography file</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
@ -286,7 +322,7 @@
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Bibliography File&lt;/b&gt;</property>
<property name="label" translatable="yes">&lt;b&gt;Bibliography &lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
@ -320,13 +356,13 @@
<object class="GtkBox" id="box4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="margin_top">4</property>
<property name="margin_bottom">4</property>
<property name="orientation">vertical</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkCheckButton" id="self_contained">
<property name="label" translatable="yes">Self Contained</property>
<property name="label" translatable="yes">Self-contained</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
@ -343,13 +379,14 @@
</child>
<child>
<object class="GtkCheckButton" id="html5">
<property name="label" translatable="yes">HTML 5</property>
<property name="label" translatable="yes">HTML5</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Use HTML 5 syntax</property>
<property name="tooltip_text" translatable="yes">Use HTML5 syntax</property>
<property name="halign">start</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
@ -367,6 +404,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a CSS File that you want to use</property>
<property name="margin_left">4</property>
<property name="label" translatable="yes">CSS File</property>
</object>
<packing>
@ -380,6 +418,8 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a CSS File that you want to use</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
</object>
<packing>
<property name="expand">True</property>
@ -426,7 +466,7 @@
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="relief">none</property>
<property name="uri">http://johnmacfarlane.net/pandoc/README.html</property>
<property name="uri">https://pandoc.org/MANUAL.html</property>
</object>
<packing>
<property name="left_attach">0</property>
@ -547,6 +587,19 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkFileChooserWidget" id="html">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="action">save</property>
<property name="filter">html_filter</property>
</object>
<packing>
<property name="name">html</property>
<property name="title" translatable="yes">HTML</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFileChooserWidget" id="pdf">
<property name="visible">True</property>
@ -570,32 +623,6 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFileChooserWidget" id="html">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="action">save</property>
<property name="filter">html_filter</property>
</object>
<packing>
<property name="name">html</property>
<property name="title" translatable="yes">HTML</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFileChooserWidget" id="odt">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="action">save</property>
<property name="filter">odt_filter</property>
</object>
<packing>
<property name="name">odt</property>
<property name="title" translatable="yes">ODT</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkFileChooserWidget" id="advanced">
<property name="visible">True</property>

View File

@ -30,17 +30,7 @@
</item>
<item>
<attribute name="label" translatable="yes">Copy HTML</attribute>
<attribute name="action">app.HTML_copy</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Open Tutorial</attribute>
<attribute name="action">app.open_examples</attribute>
</item>
<item>
<attribute name="action">app.help</attribute>
<attribute name="label" translatable="yes">Pandoc _Help</attribute>
<attribute name="action">app.copy_html</attribute>
</item>
</section>
<section>
@ -52,6 +42,10 @@
<attribute name="action">app.shortcuts</attribute>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Open Tutorial</attribute>
<attribute name="action">app.open_tutorial</attribute>
</item>
<item>
<attribute name="action">app.about</attribute>
<attribute name="label" translatable="yes">_About UberWriter</attribute>

View File

@ -2,6 +2,11 @@
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="help">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-information-symbolic</property>
</object>
<object class="GtkWindow" id="PreferencesWindow">
<property name="can_focus">False</property>
<property name="modal">True</property>
@ -30,19 +35,18 @@
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">30</property>
<property name="margin_right">30</property>
<property name="margin_top">30</property>
<property name="margin_bottom">30</property>
<property name="row_spacing">10</property>
<property name="column_spacing">10</property>
<property name="margin_left">16</property>
<property name="margin_right">16</property>
<property name="margin_top">16</property>
<property name="margin_bottom">16</property>
<property name="row_spacing">8</property>
<property name="column_spacing">8</property>
<child>
<object class="GtkLabel" id="Dark_mode_label">
<object class="GtkLabel" id="dark_mode_auto_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Use dark mode</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Set dark mode automatically</property>
<property name="justify">right</property>
</object>
<packing>
@ -51,11 +55,22 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="Spellcheck_label">
<object class="GtkSwitch" id="dark_mode_auto_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="dark_mode_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Autospellcheck</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Force dark mode</property>
<property name="justify">right</property>
</object>
<packing>
@ -64,67 +79,122 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="Gradient_label">
<object class="GtkSwitch" id="dark_mode_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="spellcheck_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Check spelling while typing</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="spellcheck_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="gradient_overlay_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Draw scroll gradient</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="Dark_mode_switch">
<object class="GtkSwitch" id="gradient_overlay_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="action_name">app.dark_mode</property>
<property name="halign">end</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="left_attach">2</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="Spellcheck_switch">
<object class="GtkLabel" id="format_label">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="action_name">app.spellcheck</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Input format</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="Gradient_switch">
<object class="GtkButton" id="input_format_help_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">start</property>
<property name="action_name">app.draw_gradient</property>
<property name="receives_default">False</property>
<property name="image">help</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="top_attach">4</property>
</packing>
</child>
</object>
<packing>
<property name="tab_expand">True</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="Label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">page 1</property>
<child>
<object class="GtkComboBox" id="input_format_combobox">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">end</property>
<property name="active">0</property>
<property name="active_id">0</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child type="tab">
<placeholder/>
</child>
<child>
<placeholder/>
</child>

View File

@ -4,6 +4,7 @@
<requires lib="gtk+" version="3.20"/>
<object class="GtkRecentFilter" id="recent_md_filter">
<mime-types>
<mime-type>text/markdown</mime-type>
<mime-type>text/x-markdown</mime-type>
</mime-types>
</object>

View File

@ -1,548 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<!-- interface-requires uberwriter_advanced_export_dialog 1.0 -->
<object class="UberwriterAdvancedExportDialog" id="uberwriter_advanced_export_dialog">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area1">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button2">
<property name="label">gtk-cancel</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button1">
<property name="label" translatable="yes">Export</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="is_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkComboBox" id="choose_format">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">out</property>
<child>
<object class="GtkAlignment" id="alignment4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkBox" id="box6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="smart">
<property name="label" translatable="yes">Smart</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Pandoc can automatically make "--" to a long dash and more</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="normalize">
<property name="label" translatable="yes">Normalize</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Removes things like double spaces or spaces at the beginning of a paragraph</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="toc">
<property name="label" translatable="yes">Table of Contents</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="standalone">
<property name="label" translatable="yes">Standalone</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Use a header and footer to include things like stylesheets and meta information</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="number_sections">
<property name="label" translatable="yes">Number Sections</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="strict">
<property name="label" translatable="yes">Strict Markdown</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Use "strict" markdown instead of "pandoc" markdown</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="incremental">
<property name="label" translatable="yes">Slideshow incremental bullets</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Show one bullet point after another in a slideshow</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;General Options&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">out</property>
<child>
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkBox" id="box3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="highlight">
<property name="label" translatable="yes">Highlight syntax</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a color theme for syntax highlighting</property>
<property name="label" translatable="yes">Highlight style </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="highlight_style">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a color theme for syntax highlighting</property>
<property name="active">0</property>
<property name="entry_text_column">0</property>
<property name="id_column">1</property>
<property name="active_id">0</property>
<items>
<item>pygments</item>
<item>kate</item>
<item>monochrome</item>
<item>espresso</item>
<item>zenburn</item>
<item>haddock</item>
<item>tango</item>
</items>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Syntax highlighting&lt;/b&gt; (HTML, LaTeX)</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">out</property>
<child>
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkBox" id="box4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkCheckButton" id="self_contained">
<property name="label" translatable="yes">Self Contained</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Produces a HTML that has no external dependencies (all images and stylesheets are included)</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="html5">
<property name="label" translatable="yes">HTML 5</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Use HTML 5 syntax</property>
<property name="use_action_appearance">False</property>
<property name="xalign">0</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a CSS File that you want to use</property>
<property name="margin_right">10</property>
<property name="label" translatable="yes">CSS File</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFileChooserButton" id="css_filechooser">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Choose a CSS File that you want to use</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;HTML Options&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="frame3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">out</property>
<child>
<object class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<object class="GtkFileChooserButton" id="bib_filechooser">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Bibliography File&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkLinkButton" id="linkbutton1">
<property name="label" translatable="yes">Commandline Reference</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="use_action_appearance">False</property>
<property name="relief">none</property>
<property name="uri">http://johnmacfarlane.net/pandoc/README.html</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="0">button2</action-widget>
<action-widget response="1">button1</action-widget>
</action-widgets>
</object>
</interface>

View File

@ -16,7 +16,6 @@
<object class="GtkImage" id="avall">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Next Match</property>
<property name="icon_name">go-down-symbolic</property>
</object>
<object class="GtkImage" id="ortografia1">
@ -33,7 +32,6 @@
<object class="GtkImage" id="reemplaza">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Open Replace</property>
<property name="icon_name">edit-find-replace-symbolic</property>
</object>
<object class="GtkOverlay" id="FullscreenOverlay">
@ -77,97 +75,20 @@
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<child>
<object class="GtkRevealer" id="status_bar_revealer">
<object class="GtkRevealer" id="stats_counter_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<property name="transition_duration">750</property>
<property name="reveal_child">True</property>
<child>
<object class="GtkGrid" id="status_bar_box">
<object class="GtkButton" id="stats_counter">
<property name="label" translatable="yes">0 Words</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Words:</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="word_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label">0</property>
<property name="justify">right</property>
<property name="width_chars">4</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Characters:</property>
</object>
<packing>
<property name="left_attach">6</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="char_count">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="margin_right">11</property>
<property name="margin_end">11</property>
<property name="label">0</property>
<property name="width_chars">6</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">7</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Show Statistics</property>
<property name="halign">end</property>
</object>
</child>
</object>
@ -186,23 +107,16 @@
<property name="vscroll_policy">natural</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkAlignment" id="editor_alignment">
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="vadjustment">adjustment1</property>
<child>
<object class="GtkScrolledWindow" id="editor_scrolledwindow">
<property name="height_request">500</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="vadjustment">adjustment1</property>
<child>
<placeholder/>
</child>
</object>
<placeholder/>
</child>
</object>
</child>
@ -290,6 +204,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Next Match</property>
<property name="image">avall</property>
</object>
<packing>
@ -335,6 +250,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Regular Expression</property>
</object>
<packing>
<property name="expand">False</property>
@ -347,6 +263,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="image">reemplaza</property>
</object>
<packing>

View File

@ -1,9 +0,0 @@
<glade-catalog name="uberwriter_advanced_export_dialog" domain="glade-3"
depends="gtk+" version="1.0">
<glade-widget-classes>
<glade-widget-class title="Advanced Export Dialog" name="UberwriterAdvancedExportDialog"
generic-name="uberwriter_advanced_export_dialog" parent="GtkDialog"
icon-name="widget-gtk-dialog"/>
</glade-widget-classes>
</glade-catalog>

View File

@ -0,0 +1,35 @@
{
"name": "pipdeps",
"buildsystem": "simple",
"build-commands": [
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz",
"sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz",
"sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz",
"sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz",
"sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz",
"sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0"
}
]
}

View File

@ -1,134 +1,125 @@
{
"app-id": "de.wolfvollprecht.UberWriter",
"runtime": "org.gnome.Platform",
"runtime-version": "3.28",
"sdk": "org.gnome.Sdk",
"command": "/app/usr/bin/uberwriter",
"finish-args": [
"app-id":"de.wolfvollprecht.UberWriter",
"runtime":"org.gnome.Platform",
"runtime-version":"3.32",
"sdk":"org.gnome.Sdk",
"command":"start-uberwriter",
"finish-args":[
"--socket=x11",
"--socket=wayland",
"--share=ipc",
"--share=network",
"--filesystem=host",
"--env=IN_FLATPAK=1",
"--filesystem=xdg-run/dconf",
"--filesystem=~/.config/dconf:ro",
"--talk-name=ca.desrt.dconf",
"--env=DCONF_USER_CONFIG_DIR=.config/dconf",
"--env=XDG_DATA_DIRS=/app/usr/share",
"--env=PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/usr/bin:/app/bin"
"--env=DCONF_USER_CONFIG_DIR=.config/dconf"
],
"build-options" : {
"env": {
"PYTHON": "python3",
"IN_FLATPAK": "1"
"add-extensions":{
"de.wolfvollprecht.UberWriter.Plugin":{
"directory":"extensions",
"version":"stable",
"subdirectories":true,
"no-autodownload":true,
"autodelete":true
}
},
"add-extensions": {
"de.wolfvollprecht.UberWriter.Plugin": {
"directory": "extensions",
"version": "stable",
"subdirectories": true,
"no-autodownload": true,
"autodelete": true
}
},
"modules": [
"modules":[
{
"name": "uberwriter",
"sources": [
"name":"enchant",
"config-opts":[
"--disable-static",
"--with-myspell-dir=/usr/share/hunspell"
],
"cleanup":[
"/bin"
],
"sources":[
{
"type" : "git",
"url" : "../",
"branch" : "refactoring"
}
],
"build-commands": [
"install -Dm644 flatpak/de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml "
],
"post-install": [
"glib-compile-schemas /app/usr/share/glib-2.0/schemas",
"install -d /app/extensions"
]
},
{
"name": "pandoc",
"only-arches": [
"x86_64"
],
"buildsystem": "simple",
"build-commands": [
"cp bin/pandoc /app/usr/bin/pandoc",
"cp bin/pandoc-citeproc /app/usr/bin/pandoc-citeproc"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz",
"sha256": "06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed"
"type":"archive",
"url":"https://github.com/AbiWord/enchant/releases/download/enchant-1-6-1/enchant-1.6.1.tar.gz",
"sha256":"bef0d9c0fef2e4e8746956b68e4d6c6641f6b85bd2908d91731efb68eba9e3f5"
}
]
},
{
"name": "pipdeps",
"buildsystem": "simple",
"build-commands": [
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} pyenchant regex pypandoc"
],
"sources": [
"name":"gspell",
"sources":[
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/5d/c1/45947333669b31bc6b4933308dd07c2aa2fedcec0a95b14eedae993bd449/wheel-0.31.0.tar.gz",
"sha256": "1ae8153bed701cb062913b72429bcf854ba824f973735427681882a688cb55ce"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/ae/e8/2340d46ecadb1692a1e455f13f75e596d4eab3d11a57446f08259dee8f02/pip-10.0.1.tar.gz",
"sha256": "f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/71/81/00184643e5a10a456b4118fc12c96780823adb8ed974eb2289f29703b29b/pypandoc-1.4.tar.gz",
"sha256": "e914e6d5f84a76764887e4d909b09d63308725f0cbb5293872c2c92f07c11a5b"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/a2/51/c39562cfed3272592c60cfd229e5464d715b78537e332eac2b695422dc49/regex-2018.02.21.tar.gz",
"sha256": "b44624a38d07d3c954c84ad302c29f7930f4bf01443beef5589e9157b14e2a29"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/9e/54/04d88a59efa33fefb88133ceb638cdf754319030c28aadc5a379d82140ed/pyenchant-2.0.0.tar.gz",
"sha256": "fc31cda72ace001da8fe5d42f11c26e514a91fa8c70468739216ddd8de64e2a0"
"type":"archive",
"url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.1.tar.xz",
"sha256":"819a1d23c7603000e73f5e738bdd284342e0cd345fb0c7650999c31ec741bbe5"
}
]
},
{
"name": "fonts",
"buildsystem": "simple",
"build-commands": [
"mkdir -p /app/share/fonts/",
"cp ttf/* /app/share/fonts/"
"name":"fonts",
"buildsystem":"simple",
"build-commands":[
"mkdir -p /app/share/fonts/",
"cp ttf/* /app/share/fonts/"
],
"sources": [
"sources":[
{
"type": "git",
"url": "https://github.com/mozilla/Fira",
"tag": "4.202"
"type":"git",
"url":"https://github.com/mozilla/Fira",
"tag":"4.202"
}
]
},
{
"name": "appdata",
"buildsystem": "simple",
"build-commands": [
"mkdir -p /app/share/appdata",
"install -Dm644 de.wolfvollprecht.UberWriter.appdata.xml /app/share/appdata/de.wolfvollprecht.UberWriter.appdata.xml"
"name":"pandoc",
"only-arches":[
"x86_64"
],
"sources": [
"buildsystem":"simple",
"build-commands":[
"install -Dm 755 bin/pandoc /app/bin/pandoc",
"install -Dm 755 bin/pandoc-citeproc /app/bin/pandoc-citeproc"
],
"sources":[
{
"type": "file",
"path": "de.wolfvollprecht.UberWriter.appdata.xml"
"type":"archive",
"url":"https://github.com/jgm/pandoc/releases/download/2.2/pandoc-2.2-linux.tar.gz",
"sha256":"06ecd882e42ef9b7390b1c82e1e71b3ea48679181289b9b810a8797825bed8ed"
}
]
},
"de.wolfvollprecht.UberWriter.pipdeps.json",
{
"name":"uberwriter",
"buildsystem":"simple",
"build-commands":[
"desktop-file-edit --set-key=Exec --set-value='uberwriter.in %U' data/de.wolfvollprecht.UberWriter.desktop",
"python3 -m pip install --prefix=/app --install-option=--optimize=1 ."
],
"sources":[
{
"type":"dir",
"path":"../"
}
],
"post-install":[
"install -d /app/extensions",
"glib-compile-schemas /app/share/glib-2.0/schemas"
]
},
{
"name":"scripts",
"buildsystem":"simple",
"build-commands":[
"install -Dm 755 start-uberwriter.sh /app/bin/start-uberwriter"
],
"sources":[
{
"type":"script",
"dest-filename":"start-uberwriter.sh",
"commands":[
"export PATH=/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/$(uname -a)-linux:$PATH",
"exec uberwriter.in \"$@\""
]
}
]
}
]
}
}

View File

@ -13,7 +13,7 @@
</info>
<title>UberWriter Preview</title>
<p>There are 2 different ways to preview your MarkDown files in UberWriter and
<p>There are 2 different ways to preview your Markdown files in UberWriter and
quickly check, what you have written.
</p>
<section id="inline-preview">
@ -29,6 +29,6 @@ quickly check, what you have written.
<title>Complete Preview</title>
<p>If you want a complete Preview of your document, you just need to hit the
preview Button on the statusbar at the bottom of the UberWriter window.
It will render the complete HTML Output of your MarkDown file.</p>
It will render the complete HTML Output of your Markdown file.</p>
</section>
</page>

View File

@ -619,9 +619,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte
feas*ible*, not feas*able*.
#### Strikeout
#### Strikethrough
To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
This ~~is deleted text.~~
@ -1183,7 +1183,7 @@ Sergey Astanin.
[Slidy]: http://www.w3.org/Talks/Tools/Slidy/
[Slideous]: http://goessner.net/articles/slideous/
[HTML]: http://www.w3.org/TR/html40/
[HTML 5]: http://www.w3.org/TR/html5/
[HTML5]: http://www.w3.org/TR/html5/
[XHTML]: http://www.w3.org/TR/xhtml1/
[LaTeX]: http://www.latex-project.org/
[beamer]: http://www.tex.ac.uk/CTAN/macros/latex/contrib/beamer

View File

@ -47,8 +47,8 @@ is *emphasized with asterisks*.</code></pre>
<pre><code>This is * not emphasized *, and \*neither is this\*.</code></pre>
<p>Because <code>_</code> is sometimes used inside words and identifiers, pandoc does not interpret a <code>_</code> surrounded by alphanumeric characters as an emphasis marker. If you want to emphasize just part of a word, use <code>*</code>:</p>
<pre><code>feas*ible*, not feas*able*.</code></pre>
<h4 id="strikeout">Strikeout</h4>
<p>To strikeout a section of text with a horizontal line, begin and end it with <code>~~</code>. Thus, for example,</p>
<h4 id="strikethrough">Strikethrough</h4>
<p>To strikethrough a section of text with a horizontal line, begin and end it with <code>~~</code>. Thus, for example,</p>
<pre><code>This ~~is deleted text.~~</code></pre>
<h3 id="block-quotations">Block quotations</h3>
<p>Markdown uses email conventions for quoting blocks of text. A block quotation is one or more paragraphs or other block elements (such as lists or headers), with each line preceded by a <code>&gt;</code> character and a space.</p>

View File

@ -67,9 +67,9 @@ Because `_` is sometimes used inside words and identifiers, pandoc does not inte
feas*ible*, not feas*able*.
### Strikeout
### Strikethrough
To strikeout a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
To strikethrough a section of text with a horizontal line, begin and end it with `~~`. Thus, for example,
This ~~is deleted text.~~

View File

@ -1,4 +1,5 @@
regex
enchant
python-gtkspellcheck
pandoc
pypandoc==1.4
pyenchant
pygtkspellcheck

View File

@ -2,16 +2,16 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# 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
#
# 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
#
# 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
@ -22,37 +22,33 @@
from setuptools import setup
import os
def package_files(directory):
paths = []
for (path, directories, filenames) in os.walk(directory):
def data_files(basename):
data = os.path.join('.', 'data')
root = os.path.join(data, basename)
extra_files = []
for path, directories, filenames in os.walk(root):
paths = []
for filename in filenames:
paths.append(os.path.join(path, filename))
return paths
extra_files.append(('share/uberwriter/data/{}'.format(os.path.relpath(path, data)), paths))
return extra_files
extra_files_ui = package_files('./data/ui')
extra_files_media = package_files('./data/media')
extra_files_scripts = package_files('./data/lua')
from pprint import pprint
pprint(extra_files_ui)
pprint(extra_files_media)
if os.path.isfile("/.flatpak-info"):
app_prefix = '/app/'
else:
app_prefix = '/usr/'
extra_files_ui = data_files('ui')
extra_files_media = data_files('media')
extra_files_scripts = data_files('lua')
setup(
name='uberwriter',
version='2.1.4',
version='2.2.0-beta1',
license='GPL-3',
author='Wolf Vollprecht',
author_email='w.vollprecht@gmail.com',
description='A beautiful, simple and distraction free markdown editor.',
long_description="""UberWriter, beautiful distraction free writing
With UberWriter you get only one thing: An empty textbox, that is to
fill with your ideas. There are no settings, you don't have to choose a
font, it is only for writing.You can use markdown for all your markup
needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it
With UberWriter you get only one thing: An empty textbox, that is to
fill with your ideas. There are no settings, you don't have to choose a
font, it is only for writing.You can use markdown for all your markup
needs. PDF, RTF and HTML are generated with pandoc. For PDF generation it
is also required that you choose to install the texlive-luatex package.""",
url='https://github.com/wolfv/uberwriter/',
# cmdclass={'install': InstallAndUpdateDataDirectory},
@ -60,11 +56,9 @@ setup(
# "": '/opt/uberwriter/'
},
packages=[
"uberwriter.gtkspellcheck",
"uberwriter.pylocales",
# "uberwriter.pressagio",
"uberwriter",
"uberwriter",
"po"
# "uberwriter.plugins"
# "uberwriter.plugins.bibtex"
@ -75,13 +69,12 @@ setup(
'uberwriter.pylocales' : ['locales.db'],
},
data_files=[
(app_prefix + 'bin', ['bin/uberwriter']),
(app_prefix + 'share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']),
(app_prefix + 'share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']),
(app_prefix + 'share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']),
(app_prefix + 'share/applications', ['de.wolfvollprecht.UberWriter.desktop']),
(app_prefix + 'share/uberwriter/data/ui', extra_files_ui),
(app_prefix + 'share/uberwriter/data/media', extra_files_media),
(app_prefix + 'share/uberwriter/data/lua', extra_files_scripts)
('bin', ['uberwriter.in']),
('share/applications', ['data/de.wolfvollprecht.UberWriter.desktop']),
('share/metainfo', ['data/de.wolfvollprecht.UberWriter.appdata.xml']),
('share/icons/hicolor/scalable/apps', ['data/media/de.wolfvollprecht.UberWriter.svg']),
('share/icons/hicolor/symbolic/apps', ['data/media/de.wolfvollprecht.UberWriter-symbolic.svg']),
('share/glib-2.0/schemas', ['data/de.wolfvollprecht.UberWriter.gschema.xml']),
*(extra_files_ui + extra_files_media + extra_files_scripts)
]
)

View File

@ -15,36 +15,21 @@
### END LICENSE
import sys
import locale
import os
import gettext
from gettext import gettext as _
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # pylint: disable=E0611
from uberwriter import window
from uberwriter import application
from uberwriter.helpers import set_up_logging
from uberwriter.uberwriterconfig import get_version
from uberwriter.config import get_version
def main():
'constructor for your class instances'
# (options, args) = parse_options()
# Run the application.
app = application.Application()
# ~ if args:
# ~ for arg in args:
# ~ pass
# ~ else:
# ~ pass
# ~ if options.experimental_features:
# ~ window.use_experimental_features(True)
app.run(sys.argv)

View File

@ -11,17 +11,14 @@
# with this program. If not, see &lt;http://www.gnu.org/licenses/&gt;.
import argparse
import webbrowser
from gettext import gettext as _
import gi
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
from gi.repository import GLib, Gio, Gtk, Gdk, GdkPixbuf
from gi.repository import GLib, Gio, Gtk, GdkPixbuf
from uberwriter import window
from uberwriter.theme import Theme
from uberwriter.settings import Settings
from uberwriter.helpers import set_up_logging
from uberwriter.preferences_dialog import PreferencesDialog
@ -41,74 +38,9 @@ class Application(Gtk.Application):
Gtk.Application.do_startup(self)
# Actions
self.settings.connect("changed", self.on_settings_changed)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
action = Gio.SimpleAction.new("shortcuts", None)
action.connect("activate", self.on_shortcuts)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
set_dark_mode = self.settings.get_value("dark-mode")
action = Gio.SimpleAction.new_stateful("dark_mode",
None,
GLib.Variant.new_boolean(set_dark_mode))
action.connect("change-state", self.on_dark_mode)
self.add_action(action)
action = Gio.SimpleAction.new_stateful("focus_mode",
None,
GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_focus_mode)
self.add_action(action)
action = Gio.SimpleAction.new_stateful("hemingway_mode",
None,
GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_hemingway_mode)
self.add_action(action)
action = Gio.SimpleAction.new_stateful("fullscreen",
None,
GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_fullscreen)
self.add_action(action)
action = Gio.SimpleAction.new_stateful("preview",
None,
GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_preview)
self.add_action(action)
action = Gio.SimpleAction.new("search", None)
action.connect("activate", self.on_search)
self.add_action(action)
set_spellcheck = self.settings.get_value("spellcheck")
action = Gio.SimpleAction.new_stateful("spellcheck",
None,
GLib.Variant.new_boolean(set_spellcheck))
action.connect("change-state", self.on_spellcheck)
self.add_action(action)
set_gradient_overlay = self.settings.get_value("gradient-overlay")
action = Gio.SimpleAction.new_stateful("draw_gradient",
None,
GLib.Variant.new_boolean(set_gradient_overlay))
action.connect("change-state", self.on_draw_gradient)
self.add_action(action)
# Menu Actions
# Header bar
action = Gio.SimpleAction.new("new", None)
action.connect("activate", self.on_new)
@ -122,14 +54,36 @@ class Application(Gtk.Application):
action.connect("activate", self.on_open_recent)
self.add_action(action)
action = Gio.SimpleAction.new("open_examples", None)
action.connect("activate", self.on_example)
self.add_action(action)
action = Gio.SimpleAction.new("save", None)
action.connect("activate", self.on_save)
self.add_action(action)
action = Gio.SimpleAction.new("search", None)
action.connect("activate", self.on_search)
self.add_action(action)
# App Menu
action = Gio.SimpleAction.new_stateful(
"focus_mode", None, GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_focus_mode)
self.add_action(action)
action = Gio.SimpleAction.new_stateful(
"hemingway_mode", None, GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_hemingway_mode)
self.add_action(action)
action = Gio.SimpleAction.new_stateful(
"preview", None, GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_preview)
self.add_action(action)
action = Gio.SimpleAction.new_stateful(
"fullscreen", None, GLib.Variant.new_boolean(False))
action.connect("change-state", self.on_fullscreen)
self.add_action(action)
action = Gio.SimpleAction.new("save_as", None)
action.connect("activate", self.on_save_as)
self.add_action(action)
@ -138,17 +92,41 @@ class Application(Gtk.Application):
action.connect("activate", self.on_export)
self.add_action(action)
action = Gio.SimpleAction.new("HTML_copy", None)
action.connect("activate", self.on_html_copy)
action = Gio.SimpleAction.new("copy_html", None)
action.connect("activate", self.on_copy_html)
self.add_action(action)
action = Gio.SimpleAction.new("preferences", None)
action.connect("activate", self.on_preferences)
self.add_action(action)
action = Gio.SimpleAction.new("shortcuts", None)
action.connect("activate", self.on_shortcuts)
self.add_action(action)
action = Gio.SimpleAction.new("open_tutorial", None)
action.connect("activate", self.on_open_tutorial)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
# Stats Menu
stat_default = self.settings.get_string("stat-default")
action = Gio.SimpleAction.new_stateful(
"stat_default", GLib.VariantType.new('s'), GLib.Variant.new_string(stat_default))
action.connect("activate", self.on_stat_default)
self.add_action(action)
# Shortcuts
# TODO: be aware that a couple of shortcuts are defined in _gtk_base.css
# TODO: be aware that a couple of shortcuts are defined in base.css
self.set_accels_for_action("app.focus_mode", ["<Ctl>d"])
self.set_accels_for_action("app.hemingway_mode", ["<Ctl>t"])
@ -163,8 +141,6 @@ class Application(Gtk.Application):
self.set_accels_for_action("app.save_as", ["<Ctl><shift>s"])
self.set_accels_for_action("app.quit", ["<Ctl>w", "<Ctl>q"])
self.apply_current_theme()
def do_activate(self, *args, **kwargs):
# We only allow a single window and raise any existing ones
if not self.window:
@ -174,8 +150,6 @@ class Application(Gtk.Application):
self.window = window.Window(self)
if self.args:
self.window.load_file(self.args[0])
if self.options.experimental_features:
self.window.use_experimental_features(True)
self.window.present()
@ -187,8 +161,7 @@ class Application(Gtk.Application):
help=_("Show debug messages (-vv debugs uberwriter also)"))
parser.add_argument(
"-e", "--experimental-features", help=_("Use experimental features"),
action='store_true'
)
action='store_true')
(self.options, self.args) = parser.parse_known_args()
set_up_logging(self.options)
@ -196,90 +169,17 @@ class Application(Gtk.Application):
self.activate()
return 0
def apply_current_theme(self):
# get current theme
theme = Theme.get_current()
# set theme variant (dark/light)
Gtk.Settings.get_default().set_property(
"gtk-application-prefer-dark-theme",
GLib.Variant("b", theme.is_dark))
# set theme css
style_provider = Gtk.CssProvider()
style_provider.load_from_path(theme.gtk_css_path)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
def on_about(self, _action, _param):
builder = get_builder('About')
about_dialog = builder.get_object("AboutDialog")
about_dialog.set_transient_for(self.window)
logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg")
logo = GdkPixbuf.Pixbuf.new_from_file(logo_file)
about_dialog.set_logo(logo)
about_dialog.present()
def on_help(self, _action, _param):
"""open pandoc markdown web
"""
webbrowser.open(
"http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown")
def on_shortcuts(self, _action, _param):
builder = get_builder('Shortcuts')
builder.get_object("shortcuts").set_transient_for(self.window)
builder.get_object("shortcuts").show()
def on_dark_mode(self, action, value):
action.set_state(value)
self.settings.set_value("dark-mode", GLib.Variant("b", value))
# this changes the headerbar theme accordingly
self.apply_current_theme()
# adjust window for theme
self.window.apply_current_theme()
def on_focus_mode(self, action, value):
action.set_state(value)
self.window.set_focus_mode(value)
def on_hemingway_mode(self, action, value):
action.set_state(value)
self.window.set_hemingway_mode(value)
def on_fullscreen(self, action, value):
action.set_state(value)
self.window.set_fullscreen(value)
def on_preview(self, action, value):
action.set_state(value)
self.window.toggle_preview(value)
def on_search(self, _action, _value):
self.window.open_search_and_replace()
def on_spellcheck(self, action, value):
action.set_state(value)
self.settings.set_value("spellcheck",
GLib.Variant("b", value))
self.window.toggle_spellcheck(value)
def on_draw_gradient(self, action, value):
action.set_state(value)
self.settings.set_value("gradient-overlay",
GLib.Variant("b", value))
if value:
self.window.overlay = self.window.scrolled_window.connect_after(
"draw", self.window.draw_gradient)
else:
self.window.scrolled_window.disconnect(self.window.overlay)
def on_settings_changed(self, settings, key):
if key == "dark-mode-auto" or key == "dark-mode":
self.window.apply_current_theme()
elif key == "spellcheck":
self.window.toggle_spellcheck(settings.get_value(key))
elif key == "gradient-overlay":
self.window.toggle_gradient_overlay(settings.get_value(key))
elif key == "input-format":
self.window.reload_preview()
elif key == "stat-default":
self.window.update_default_stat()
def on_new(self, _action, _value):
self.window.new_document()
@ -290,30 +190,66 @@ class Application(Gtk.Application):
def on_open_recent(self, file):
self.window.load_file(file.get_current_uri())
def on_example(self, _action, _value):
self.window.open_uberwriter_markdown()
def on_save(self, _action, _value):
self.window.save_document()
def on_search(self, _action, _value):
self.window.open_search_and_replace()
def on_focus_mode(self, action, value):
action.set_state(value)
self.window.set_focus_mode(value)
def on_hemingway_mode(self, action, value):
action.set_state(value)
self.window.set_hemingway_mode(value)
def on_preview(self, action, value):
action.set_state(value)
self.window.toggle_preview(value)
def on_fullscreen(self, action, value):
action.set_state(value)
self.window.set_fullscreen(value)
def on_save_as(self, _action, _value):
self.window.save_document_as()
def on_export(self, _action, _value):
self.window.open_advanced_export()
def on_html_copy(self, _action, _value):
def on_copy_html(self, _action, _value):
self.window.copy_html_to_clipboard()
def on_preferences(self, _action, _value):
preferences_window = PreferencesDialog()
preferences_window.set_application(self)
preferences_window.set_transient_for(self.window)
preferences_window.show()
PreferencesDialog(self.settings).show(self.window)
def on_shortcuts(self, _action, _param):
builder = get_builder('Shortcuts')
builder.get_object("shortcuts").set_transient_for(self.window)
builder.get_object("shortcuts").show()
def on_open_tutorial(self, _action, _value):
self.window.open_uberwriter_markdown()
def on_about(self, _action, _param):
builder = get_builder('About')
about_dialog = builder.get_object("AboutDialog")
about_dialog.set_transient_for(self.window)
logo_file = get_media_path("de.wolfvollprecht.UberWriter.svg")
logo = GdkPixbuf.Pixbuf.new_from_file(logo_file)
about_dialog.set_logo(logo)
about_dialog.present()
def on_quit(self, _action, _param):
self.quit()
def on_stat_default(self, action, value):
action.set_state(value)
self.settings.set_string("stat-default", value.get_string())
# ~ if __name__ == "__main__":
# ~ app = Application()
# ~ app.run(sys.argv)

View File

@ -53,7 +53,6 @@ def get_data_path():
"""
# Get pathname absolute or relative.
# TODO: Abstract this (the old env IN_FLATPAK)
if os.path.isfile("/.flatpak-info"):
return '/app/share/uberwriter/data/'

View File

@ -17,14 +17,11 @@
"""
import os
import subprocess
import logging
# import gettext
import os
from gettext import gettext as _
import gi
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
@ -35,6 +32,7 @@ from uberwriter.helpers import get_builder
LOGGER = logging.getLogger('uberwriter')
class Export:
"""
Manages all the export operations and dialogs
@ -42,6 +40,114 @@ class Export:
__gtype_name__ = "export_dialog"
formats = [
{
"name": "LaTeX (pdf)",
"ext": "pdf",
"to": "pdf"
},
{
"name": "LaTeX Beamer Slideshow (pdf)",
"ext": "pdf",
"to": "beamer"
},
{
"name": "LaTeX (tex)",
"ext": "tex",
"to": "latex"
},
{
"name": "LaTeX Beamer Slideshow (tex)",
"ext": "tex",
"to": "beamer"
},
{
"name": "ConTeXt",
"ext": "tex",
"to": "context"
},
{
"name": "HTML",
"ext": "html",
"to": "html"
},
{
"name": "HTML and JavaScript Slideshow (Slidy)",
"ext": "html",
"to": "slidy"
},
{
"name": "HTML and JavaScript Slideshow (Slideous)",
"ext": "html",
"to": "slideous"
},
{
"name": "HTML5 and JavaScript Slideshow (DZSlides)",
"ext": "html",
"to": "dzslides"
},
{
"name": "HTML5 and JavaScript Slideshow (reveal.js)",
"ext": "html",
"to": "revealjs"
},
{
"name": "HTML and JavaScript Slideshow (S5)",
"ext": "html",
"to": "s5"
},
{
"name": "Textile",
"ext": "txt",
"to": "textile"
},
{
"name": "reStructuredText",
"ext": "txt",
"to": "rst"
},
{
"name": "MediaWiki Markup",
"ext": "txt",
"to": "mediawiki"
},
{
"name": "OpenDocument (xml)",
"ext": "xml",
"to": "opendocument"
},
{
"name": "OpenDocument (texi)",
"ext": "texi",
"to": "texinfo"
},
{
"name": "OpenOffice Text Document",
"ext": "odt",
"to": "odt"
},
{
"name": "Microsoft Word (docx)",
"ext": "docx",
"to": "docx"
},
{
"name": "Rich Text Format",
"ext": "rtf",
"to": "rtf"
},
{
"name": "Groff Man",
"ext": "man",
"to": "man"
},
{
"name": "EPUB v3",
"ext": "epub",
"to": "epub"
}
]
def __init__(self, filename):
"""Set up the about dialog"""
self.builder = get_builder('Export')
@ -52,8 +158,8 @@ class Export:
stack_pdf_disabled = self.builder.get_object("pdf_disabled")
filename = filename or _("Untitled document.md")
self.filechoosers = {export_format:self.stack.get_child_by_name(export_format)\
for export_format in ["pdf", "html", "odt", "advanced"]}
self.filechoosers = {export_format: self.stack.get_child_by_name(export_format)
for export_format in ["pdf", "html", "advanced"]}
for export_format, filechooser in self.filechoosers.items():
filechooser.set_do_overwrite_confirmation(True)
filechooser.set_current_folder(os.path.dirname(filename))
@ -75,9 +181,12 @@ class Export:
self.builder.get_object("highlight_style").set_active(0)
self.builder.get_object("css_filechooser").set_uri(
helpers.path_to_file(Theme.get_current().web_css_path))
format_store = Gtk.ListStore(int, str)
for fmt_id in self.formats_dict:
format_store.append([fmt_id, self.formats_dict[fmt_id]["name"]])
for i, fmt in enumerate(self.formats):
format_store.append([i, fmt["name"]])
self.format_field = self.builder.get_object('choose_format')
self.format_field.set_model(format_store)
@ -86,171 +195,56 @@ class Export:
self.format_field.add_attribute(format_renderer, "text", 1)
self.format_field.set_active(0)
formats_dict = {
1: {
"name": "LaTeX Source",
"ext": "tex",
"to": "latex"
},
2: {
"name": "LaTeX PDF",
"ext": "pdf",
"to": "pdf"
},
3: {
"name": "LaTeX beamer slide show Source .tex",
"ext": "tex",
"to": "beamer"
},
4: {
"name": "LaTeX beamer slide show PDF",
"ext": "pdf",
"to": "beamer"
},
5: {
"name": "HTML",
"ext": "html",
"to": "html"
},
6: {
"name": "Textile",
"ext": "txt",
"to": "textile"
},
7: {
"name": "OpenOffice text document",
"ext": "odt",
"to": "odt"
},
8: {
"name": "Word docx",
"ext": "docx",
"to": "docx"
},
9: {
"name": "reStructuredText txt",
"ext": "txt",
"to": "rst"
},
10: {
"name": "ConTeXt tex",
"ext": "tex",
"to": "context"
},
11: {
"name": "groff man",
"ext": "man",
"to": "man"
},
12: {
"name": "MediaWiki markup",
"ext": "txt",
"to": "mediawiki"
},
13: {
"name": "OpenDocument XML",
"ext": "xml",
"to": "opendocument"
},
14: {
"name": "OpenDocument XML",
"ext": "texi",
"to": "texinfo"
},
15: {
"name": "Slidy HTML and javascript slide show",
"ext": "html",
"to": "slidy"
},
16: {
"name": "Slideous HTML and javascript slide show",
"ext": "html",
"to": "slideous"
},
17: {
"name": "HTML5 + javascript slide show",
"ext": "html",
"to": "dzslides"
},
18: {
"name": "S5 HTML and javascript slide show",
"ext": "html",
"to": "s5"
},
19: {
"name": "EPub electronic publication",
"ext": "epub",
"to": "epub"
},
20: {
"name": "RTF Rich Text Format",
"ext": "rtf",
"to": "rtf"
}
}
def export(self, text=""):
"""Export to pdf, html or odt the given text
"""Export the given text using the specified format.
For advanced export, this includes special flags for the enabled options.
Keyword Arguments:
text {str} -- Text to export (default: {""})
"""
export_format = self.stack.get_visible_child_name()
export_type = self.stack.get_visible_child_name()
args = []
if export_type == "advanced":
filename = self.adv_export_name.get_text()
output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder())
basename = os.path.basename(filename)
fmt = self.formats[self.format_field.get_active()]
to = fmt["to"]
ext = fmt["ext"]
if self.builder.get_object("html5").get_active() and to == "html":
to = "html5"
if self.builder.get_object("smart").get_active():
to += "+smart"
args.extend(self.get_advanced_arguments())
if export_format == "advanced":
self.advanced_export(text)
else:
filename = self.filechoosers[export_format].get_filename()
if filename.endswith("." + export_format):
filename = filename[:-len(export_format)-1]
filename = self.filechoosers[export_type].get_filename()
if filename.endswith("." + export_type):
filename = filename[:-len(export_type)-1]
output_dir = os.path.abspath(os.path.join(filename, os.path.pardir))
basename = os.path.basename(filename)
args = ['pandoc', '--from=markdown', '-s']
to = export_type
ext = export_type
if export_format == "pdf":
args.append("-o%s.pdf" % basename)
elif export_format == "odt":
args.append("-o%s.odt" % basename)
elif export_format == "html":
css = Theme.ADWAITA.get_gtk_css_file()
relativize = helpers.get_script_path('relative_to_absolute.lua')
task_list = helpers.get_script_path('task-list.lua')
args.append("-c%s" % css)
args.append("-o%s.html" % basename)
if export_type == "html":
to = "html5"
args.append("--standalone")
args.append("--css=%s" % Theme.get_current().web_css_path)
args.append("--mathjax")
args.append("--lua-filter=" + relativize)
args.append("--lua-filter=" + task_list)
args.append("--lua-filter=%s" % helpers.get_script_path('relative_to_absolute.lua'))
args.append("--lua-filter=%s" % helpers.get_script_path('task-list.lua'))
proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, cwd=output_dir)
_ = proc.communicate(text)[0]
helpers.pandoc_convert(
text, to=to, args=args,
outputfile="%s/%s.%s" % (output_dir, basename, ext))
def advanced_export(self, text=""):
"""Export the given text to special formats with the enabled flags
Keyword Arguments:
text {str} -- The text to export (default: {""})
"""
filename = self.adv_export_name.get_text()
output_dir = os.path.abspath(self.filechoosers["advanced"].get_current_folder())
basename = os.path.basename(filename)
args = self.set_arguments(basename)
LOGGER.info(args)
proc = subprocess.Popen(args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, cwd=output_dir)
_ = proc.communicate(text)[0]
def set_arguments(self, basename):
"""Retrieve a list of the selected arguments
def get_advanced_arguments(self):
"""Retrieve a list of the selected advanced arguments
For most of the advanced option checkboxes, returns a list
of the related pandoc flags
@ -264,78 +258,49 @@ class Export:
highlight_style = self.builder.get_object("highlight_style").get_active_text()
conditions_dict = {
1: {
conditions = [
{
"condition": self.builder.get_object("toc").get_active(),
"yes": "--toc",
"no": None
},
2: {
{
"condition": self.builder.get_object("highlight").get_active(),
"yes": "--highlight-style=%s" % highlight_style,
"no": "--no-highlight"
},
3: {
{
"condition": self.builder.get_object("standalone").get_active(),
"yes": "--standalone",
"no": None
},
4: {
{
"condition": self.builder.get_object("number_sections").get_active(),
"yes": "--number-sections",
"no": None
},
5: {
{
"condition": self.builder.get_object("strict").get_active(),
"yes": "--strict",
"no": None
},
6: {
{
"condition": self.builder.get_object("incremental").get_active(),
"yes": "--incremental",
"no": None
},
7: {
{
"condition": self.builder.get_object("self_contained").get_active(),
"yes": "--self-contained",
"no": None
}
}
]
tree_iter = self.format_field.get_active_iter()
if tree_iter is not None:
model = self.format_field.get_model()
row_id, _ = model[tree_iter][:2]
args = []
fmt = self.formats_dict[row_id]
args.extend([c["yes"] if c["condition"] else c["no"] for c in conditions])
args = ['pandoc', '--from=markdown']
extension = "--to=%s" % fmt["to"]
if basename.endswith("." + fmt["ext"]):
output_file = "--output=%s" % basename
else:
output_file = "--output=%s.%s" % (basename, fmt["ext"])
args.extend([conditions_dict[c_id]["yes"]\
if conditions_dict[c_id]["condition"]\
else conditions_dict[c_id]["no"]\
for c_id in conditions_dict])
args = list(filter(None, args))
if self.builder.get_object("html5").get_active():
if fmt["to"] == "html":
extension = "--to=%s" % "html5"
if self.builder.get_object("smart").get_active():
extension += '+smart'
else:
extension += '-smart'
if fmt["to"] != "pdf":
args.append(extension)
args = list(filter(lambda arg: arg is not None, args))
css_uri = self.builder.get_object("css_filechooser").get_uri()
if css_uri:
@ -349,8 +314,6 @@ class Export:
bib_uri = bib_uri[7:]
args.append("--bibliography=%s" % bib_uri)
args.append(output_file)
return args
def allow_export(self, widget, data, signal):

View File

@ -17,18 +17,14 @@
from gettext import gettext as _
from uberwriter.markup_buffer import MarkupBuffer
class FormatShortcuts():
"""Manage the insertion of formatting for insert them using shortcuts
"""
def __init__(self, textbuffer, texteditor):
self.text_buffer = textbuffer
self.text_editor = texteditor
self.regex = MarkupBuffer.regex
def rule(self):
"""insert ruler at cursor

View File

@ -1,53 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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/>.
from __future__ import unicode_literals
__version__ = '4.0.5'
__project__ = 'Python GTK Spellcheck'
__short_name__ = 'pygtkspellcheck'
__authors__ = 'Maximilian Köhl & Carlos Jenkins'
__emails__ = 'linuxmaxi@googlemail.com & carlos@jenkins.co.cr'
__website__ = 'http://koehlma.github.com/projects/pygtkspellcheck.html'
__download_url__ = 'https://github.com/koehlma/pygtkspellcheck/tarball/master'
__source__ = 'https://github.com/koehlma/pygtkspellcheck/'
__vcs__ = 'git://github.com/koehlma/pygtkspellcheck.git'
__copyright__ = '2012, Maximilian Köhl & Carlos Jenkins'
__desc_short__ = ('a simple but quite powerful Python spell checking library '
'for GtkTextViews based on Enchant')
__desc_long__ = ('A simple but quite powerful spellchecking library written in '
'pure Python for Gtk based on Enchant. It supports PyGObject '
'as well as PyGtk for Python 2 and 3 with automatic switching '
'and binding detection. For automatic translation of the user '
'interface it can use Gedits translation files.')
__metadata__ = {'__version__' : __version__,
'__project__' : __project__,
'__short_name__' : __short_name__,
'__authors__' : __authors__,
'__emails__' : __emails__,
'__website__' : __website__,
'__download_url__' : __download_url__,
'__source__' : __source__,
'__vcs__' : __vcs__,
'__copyright__' : __copyright__,
'__desc_short__' : __desc_short__,
'__desc_long__' : __desc_long__}
from .spellcheck import (SpellChecker, NoDictionariesFound,
NoGtkBindingFound)

View File

@ -1,294 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
# Copyright (C) 2012-2016, Maximilian Köhl <mail@koehlma.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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/>.
"""
This module extracts the .dic and .aff (Hunspell) dictionaries from any given
.oxt extension.
Extensions could be found at:
http://extensions.services.openoffice.org/dictionary
"""
import functools
import gettext
import logging
import os
import shutil
import sys
import warnings
import xml.dom.minidom
import xml.parsers.expat
import zipfile
# enable deprecation warnings
warnings.simplefilter('always', DeprecationWarning)
# public objects
__all__ = ['extract_oxt', 'batch_extract', 'BadXml', 'BadExtensionFile',
'ExtractPathIsNoDirectory', 'BATCH_SUCCESS', 'BATCH_ERROR',
'BATCH_WARNING']
# logger
logger = logging.getLogger(__name__)
# translation
locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
_ = gettext.translation(locale_name, fallback=True).gettext
class BadXml(Exception):
"""
The XML dictionary registry is not valid XML.
"""
class BadExtensionFile(Exception):
"""
The extension has a wrong file format, should be a ZIP file.
"""
class ExtractPathIsNoDirectory(Exception):
"""
The given `extract_path` is no directory.
"""
def find_dictionaries(registry):
def oor_name(name, element):
return element.attributes['oor:name'].value.lower() == name
def get_property(name, properties):
property = list(filter(functools.partial(oor_name, name),
properties))
if property:
return property[0].getElementsByTagName('value')[0]
result = []
# find all "node" elements which have "dictionaries" as "oor:name" attribute
for dictionaries in filter(functools.partial(oor_name, 'dictionaries'),
registry.getElementsByTagName('node')):
# for all "node" elements in this dictionary nodes
for dictionary in dictionaries.getElementsByTagName('node'):
# get all "prop" elements
properties = dictionary.getElementsByTagName('prop')
# get the format property as text
format = get_property('format', properties).firstChild.data.strip()
if format and format == 'DICT_SPELL':
# find the locations property
locations = get_property('locations', properties)
# if the location property is text:
# %origin%/dictionary.aff %origin%/dictionary.dic
if locations.firstChild.nodeType == xml.dom.Node.TEXT_NODE:
locations = locations.firstChild.data
locations = locations.replace('%origin%/', '').strip()
result.append(locations.split())
# otherwise:
# <i>%origin%/dictionary.aff</i> <i>%origin%/dictionary.dic</i>
else:
locations = [item.firshChild.data.replace('%origin%/', '') \
.strip() for item in
locations.getElementsByTagName('it')]
result.append(locations)
return result
def extract(filename, target, override=False):
"""
Extract Hunspell dictionaries out of LibreOffice ``.oxt`` extensions.
:param filename: path to the ``.oxt`` extension
:param target: path to extract Hunspell dictionaries to
:param override: override existing files in the target directory
:rtype: list of the extracted dictionaries
This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
files) from the given ``.oxt`` extension found to ``target``.
Extensions could be found at:
http://extensions.services.openoffice.org/dictionary
"""
# TODO 5.0: remove this function
warnings.warn(('call to deprecated function "{}", '
'moved to separate package "oxt_extract", '
'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
category=DeprecationWarning)
try:
with zipfile.ZipFile(filename, 'r') as extension:
files = extension.namelist()
registry = 'dictionaries.xcu'
if not registry in files:
for filename in files:
if filename.lower().endswith(registry):
registry = filename
if registry in files:
registry = xml.dom.minidom.parse(extension.open(registry))
dictionaries = find_dictionaries(registry)
extracted = []
for dictionary in dictionaries:
for filename in dictionary:
dict_file = os.path.join(target,
os.path.basename(filename))
if (not os.path.exists(dict_file)
or (override and os.path.isfile(dict_file))):
if filename in files:
with open(dict_file, 'wb') as _target:
with extension.open(filename, 'r') as _source:
extracted.append(os.path.basename(filename))
_target.write(_source.read())
else:
logger.warning('dictionary exists in registry '
'but not in the extension zip')
else:
logging.warning(('dictionary file "{}" already exists '
'and not overriding it'
).format(dict_file))
return extracted
except zipfile.BadZipfile:
raise BadExtensionFile('extension is not a valid ZIP file')
except xml.parsers.expat.ExpatError:
raise BadXml('dictionary registry is not valid XML')
BATCH_SUCCESS = 'success'
BATCH_ERROR = 'error'
BATCH_WARNING = 'warning'
def batch_extract(oxt_path, extract_path, override=False, move_path=None):
"""
Uncompress, read and install LibreOffice ``.oxt`` dictionaries extensions.
:param oxt_path: path to a directory containing the ``.oxt`` extensions
:param extract_path: path to extract Hunspell dictionaries files to
:param override: override already existing files
:param move_path: optional path to move the ``.oxt`` files after processing
:rtype: generator over all extensions, yielding result, extension name,
error, extracted dictionaries and translated error message - result
would be :const:`BATCH_SUCCESS` for success, :const:`BATCH_ERROR` if
some error happened or :const:`BATCH_WARNING` which contain some warning
messages instead of errors
This function extracts the Hunspell dictionaries (``.dic`` and ``.aff``
files) from all the ``.oxt`` extensions found on ``oxt_path`` directory to
the ``extract_path`` directory.
Extensions could be found at:
http://extensions.services.openoffice.org/dictionary
In detail, this functions does the following:
1. find all the ``.oxt`` extension files within ``oxt_path``
2. open (unzip) each extension
3. find the dictionary definition file within (*dictionaries.xcu*)
4. parse the dictionary definition file and locate the dictionaries files
5. uncompress those files to ``extract_path``
By default file overriding is disabled, set ``override`` parameter to True
if you want to enable it. As additional option, each processed extension can
be moved to ``move_path``.
Example::
for result, name, error, dictionaries, message in oxt_extract.batch_extract(...):
if result == oxt_extract.BATCH_SUCCESS:
print('successfully extracted extension "{}"'.format(name))
elif result == oxt_extract.BATCH_ERROR:
print('could not extract extension "{}"'.format(name))
print(message)
print('error {}'.format(error))
elif result == oxt_extract.BATCH_WARNING:
print('warning during processing extension "{}"'.format(name))
print(message)
print(error)
"""
# TODO 5.0: remove this function
warnings.warn(('call to deprecated function "{}", '
'moved to separate package "oxt_extract", '
'will be removed in pygtkspellcheck 5.0').format(extract.__name__),
category=DeprecationWarning)
# get the real, absolute and normalized path
oxt_path = os.path.normpath(os.path.abspath(os.path.realpath(oxt_path)))
# check that the input directory exists
if not os.path.isdir(oxt_path):
return
# create extract directory if not exists
if not os.path.exists(extract_path):
os.makedirs(extract_path)
# check that the extract path is a directory
if not os.path.isdir(extract_path):
raise ExtractPathIsNoDirectory('extract path is not a valid directory')
# get all .oxt extension at given path
oxt_files = [extension for extension in os.listdir(oxt_path)
if extension.lower().endswith('.oxt')]
for extension_name in oxt_files:
extension_path = os.path.join(oxt_path, extension_name)
try:
dictionaries = extract(extension_path, extract_path, override)
yield BATCH_SUCCESS, extension_name, None, dictionaries, ''
except BadExtensionFile as error:
logger.error(('extension "{}" is not a valid ZIP file'
).format(extension_name))
yield (BATCH_ERROR, extension_name, error, [],
_('extension "{}" is not a valid ZIP file'
).format(extension_name))
except BadXml as error:
logger.error(('extension "{}" has no valid XML dictionary registry'
).format(extension_name))
yield (BATCH_ERROR, extension_name, error, [],
_('extension "{}" has no valid XML dictionary registry'
).format(extension_name))
# move the extension after processing if user requires it
if move_path is not None:
# create move path if it doesn't exists
if not os.path.exists(move_path):
os.makedirs(move_path)
# move to the given path only if it is a directory and target
# doesn't exists
if os.path.isdir(move_path):
if (not os.path.exists(os.path.join(move_path, extension_name))
or override):
shutil.move(extension_path, move_path)
else:
logger.warning(('unable to move extension, file with same '
'name exists within move_path'))
yield (BATCH_WARNING, extension_name,
('unable to move extension, file with same name '
'exists within move_path'), [],
_('unable to move extension, file with same name '
'exists within move_path'))
else:
logger.warning(('unable to move extension, move_path is not a '
'directory'))
yield (BATCH_WARNING, extension_name,
('unable to move extension, move_path is not a '
'directory'), [],
_('unable to move extension, move_path is not a '
'directory'))

View File

@ -1,660 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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/>.
"""
A simple but quite powerful spellchecking library written in pure Python for Gtk
based on Enchant. It supports PyGObject as well as PyGtk for Python 2 and 3 with
automatic switching and binding detection. For automatic translation of the user
interface it can use Gedits translation files.
"""
import enchant
import gettext
import logging
import re
import sys
from uberwriter.pylocales import code_to_name as _code_to_name
from uberwriter.pylocales import LanguageNotFound, CountryNotFound
# public objects
__all__ = ['SpellChecker', 'NoDictionariesFound', 'NoGtkBindingFound']
# logger
logger = logging.getLogger(__name__)
class NoDictionariesFound(Exception):
"""
There aren't any dictionaries installed on the current system so
spellchecking could not work in any way.
"""
class NoGtkBindingFound(Exception):
"""
Could not find any loaded Gtk binding.
"""
if sys.version_info.major == 3:
_py3k = True
else:
_py3k = False
if _py3k:
# there is only the gi binding for Python 3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk as gtk
_pygobject = True
else:
# find any loaded gtk binding
if 'gi.repository.Gtk' in sys.modules:
gtk = sys.modules['gi.repository.Gtk']
_pygobject = True
elif 'gtk' in sys.modules:
gtk = sys.modules['gtk']
_pygobject = False
else:
raise NoGtkBindingFound('could not find any loaded Gtk binding')
# select base list class
try:
from collections import UserList
_list = UserList
except ImportError:
_list = list
# select base string
if _py3k:
basestring = str
# map between Gedit's translation and PyGtkSpellcheck's
_GEDIT_MAP = {'Languages' : 'Languages',
'Ignore All' : 'Ignore _All',
'Suggestions' : 'Suggestions',
'(no suggestions)' : '(no suggested words)',
'Add "{}" to Dictionary' : 'Add w_ord',
'Unknown' : 'Unknown'}
# translation
if gettext.find('gedit'):
_gedit = gettext.translation('gedit', fallback=True).gettext
_ = lambda message: _gedit(_GEDIT_MAP[message]).replace('_', '')
else:
locale_name = 'py{}gtkspellcheck'.format(sys.version_info.major)
_ = gettext.translation(locale_name, fallback=True).gettext
def code_to_name(code, separator='_'):
try:
return _code_to_name(code, separator)
except (LanguageNotFound, CountryNotFound):
return '{} ({})'.format(_('Unknown'), code)
class SpellChecker(object):
"""
Main spellchecking class, everything important happens here.
:param view: GtkTextView the SpellChecker should be attached to.
:param language: The language which should be used for spellchecking.
Use a combination of two letter lower-case ISO 639 language code with a
two letter upper-case ISO 3166 country code, for example en_US or de_DE.
:param prefix: A prefix for some internal GtkTextMarks.
:param collapse: Enclose suggestions in its own menu.
:param params: Dictionary with Enchant broker parameters that should be set
e.g. `enchant.myspell.dictionary.path`.
.. attribute:: languages
A list of supported languages.
.. function:: exists(language)
Checks if a language exists.
:param language: language to check
"""
FILTER_WORD = 'word'
FILTER_LINE = 'line'
FILTER_TEXT = 'text'
DEFAULT_FILTERS = {FILTER_WORD : [r'[0-9.,]+'],
FILTER_LINE : [(r'(https?|ftp|file):((//)|(\\\\))+[\w\d:'
r'#@%/;$()~_?+-=\\.&]+'),
r'[\w\d]+@[\w\d.]+'],
FILTER_TEXT : []}
class _LanguageList(_list):
def __init__(self, *args, **kwargs):
if sys.version_info.major == 3:
super().__init__(*args, **kwargs)
else:
_list.__init__(self, *args, **kwargs)
self.mapping = dict(self)
@classmethod
def from_broker(cls, broker):
return cls(sorted([(language, code_to_name(language))
for language in broker.list_languages()],
key=lambda language: language[1]))
def exists(self, language):
return language in self.mapping
class _Mark():
def __init__(self, buffer, name, start):
self._buffer = buffer
self._name = name
self._mark = self._buffer.create_mark(self._name, start, True)
@property
def iter(self):
return self._buffer.get_iter_at_mark(self._mark)
@property
def inside_word(self):
return self.iter.inside_word()
@property
def word(self):
start = self.iter
if not start.starts_word():
start.backward_word_start()
end = self.iter
if end.inside_word():
end.forward_word_end()
return start, end
def move(self, location):
self._buffer.move_mark(self._mark, location)
def __init__(self, view, language='en', prefix='gtkspellchecker',
collapse=True, params={}):
self._view = view
self.collapse = collapse
self._view.connect('populate-popup',
lambda entry, menu:self._extend_menu(menu))
self._view.connect('popup-menu', self._click_move_popup)
self._view.connect('button-press-event', self._click_move_button)
self._prefix = prefix
self._broker = enchant.Broker()
for param, value in params.items(): self._broker.set_param(param, value)
self.languages = SpellChecker._LanguageList.from_broker(self._broker)
if self.languages.exists(language):
self._language = language
elif self.languages.exists('en'):
logger.warning(('no installed dictionary for language "{}", '
'fallback to english'.format(language)))
self._language = 'en'
else:
if self.languages:
self._language = self.languages[0][0]
logger.warning(('no installed dictionary for language "{}" '
'and english, fallback to first language in'
'language list ("{}")').format(language,
self._language))
else:
logger.critical('no dictionaries found')
raise NoDictionariesFound()
self._dictionary = self._broker.request_dict(self._language)
self._deferred_check = False
self._filters = dict(SpellChecker.DEFAULT_FILTERS)
self._regexes = {SpellChecker.FILTER_WORD : re.compile('|'.join(
self._filters[SpellChecker.FILTER_WORD])),
SpellChecker.FILTER_LINE : re.compile('|'.join(
self._filters[SpellChecker.FILTER_LINE])),
SpellChecker.FILTER_TEXT : re.compile('|'.join(
self._filters[SpellChecker.FILTER_TEXT]),
re.MULTILINE)}
self._enabled = True
self.buffer_initialize()
@property
def language(self):
"""
The language used for spellchecking.
"""
return self._language
@language.setter
def language(self, language):
if language != self._language and self.languages.exists(language):
self._language = language
self._dictionary = self._broker.request_dict(language)
self.recheck()
@property
def enabled(self):
"""
Enable or disable spellchecking.
"""
return self._enabled
@enabled.setter
def enabled(self, enabled):
if enabled and not self._enabled:
self.enable()
elif not enabled and self._enabled:
self.disable()
def buffer_initialize(self):
"""
Initialize the GtkTextBuffer associated with the GtkTextView. If you
have associated a new GtkTextBuffer with the GtkTextView call this
method.
"""
if _pygobject:
self._misspelled = gtk.TextTag.new('{}-misspelled'\
.format(self._prefix))
else:
self._misspelled = gtk.TextTag('{}-misspelled'.format(self._prefix))
self._misspelled.set_property('underline', 4)
self._buffer = self._view.get_buffer()
self._buffer.connect('insert-text', self._before_text_insert)
self._buffer.connect_after('insert-text', self._after_text_insert)
self._buffer.connect_after('delete-range', self._range_delete)
self._buffer.connect_after('mark-set', self._mark_set)
start = self._buffer.get_bounds()[0]
self._marks = {'insert-start' : SpellChecker._Mark(self._buffer,
'{}-insert-start'.format(self._prefix), start),
'insert-end' : SpellChecker._Mark(self._buffer,
'{}-insert-end'.format(self._prefix), start),
'click' : SpellChecker._Mark(self._buffer,
'{}-click'.format(self._prefix), start)}
self._table = self._buffer.get_tag_table()
self._table.add(self._misspelled)
self.ignored_tags = []
def tag_added(tag, *args):
if hasattr(tag, 'spell_check') and not getattr(tag, 'spell_check'):
self.ignored_tags.append(tag)
def tag_removed(tag, *args):
if tag in self.ignored_tags:
self.ignored_tags.remove(tag)
self._table.connect('tag-added', tag_added)
self._table.connect('tag-removed', tag_removed)
self._table.foreach(tag_added, None)
self.no_spell_check = self._table.lookup('no-spell-check')
if not self.no_spell_check:
if _pygobject:
self.no_spell_check = gtk.TextTag.new('no-spell-check')
else:
self.no_spell_check = gtk.TextTag('no-spell-check')
self._table.add(self.no_spell_check)
self.recheck()
def recheck(self):
"""
Rechecks the spelling of the whole text.
"""
start, end = self._buffer.get_bounds()
self.check_range(start, end, True)
def disable(self):
"""
Disable spellchecking.
"""
self._enabled = False
start, end = self._buffer.get_bounds()
self._buffer.remove_tag(self._misspelled, start, end)
def enable(self):
"""
Enable spellchecking.
"""
self._enabled = True
self.recheck()
def append_filter(self, regex, filter_type):
"""
Append a new filter to the filter list. Filters are useful to ignore
some misspelled words based on regular expressions.
:param regex: The regex used for filtering.
:param filter_type: The type of the filter.
Filter Types:
:const:`SpellChecker.FILTER_WORD`: The regex must match the whole word
you want to filter. The word separation is done by Pango's word
separation algorithm so, for example, urls won't work here because
they are split in many words.
:const:`SpellChecker.FILTER_LINE`: If the expression you want to match
is a single line expression use this type. It should not be an open
end expression because then the rest of the line with the text you
want to filter will become correct.
:const:`SpellChecker.FILTER_TEXT`: Use this if you want to filter
multiline expressions. The regex will be compiled with the
`re.MULTILINE` flag. Same with open end expressions apply here.
"""
self._filters[filter_type].append(regex)
if filter_type == SpellChecker.FILTER_TEXT:
self._regexes[filter_type] = re.compile('|'.join(
self._filters[filter_type]), re.MULTILINE)
else:
self._regexes[filter_type] = re.compile('|'.join(
self._filters[filter_type]))
def remove_filter(self, regex, filter_type):
"""
Remove a filter from the filter list.
:param regex: The regex which used for filtering.
:param filter_type: The type of the filter.
"""
self._filters[filter_type].remove(regex)
if filter_type == SpellChecker.FILTER_TEXT:
self._regexes[filter_type] = re.compile('|'.join(
self._filters[filter_type]), re.MULTILINE)
else:
self._regexes[filter_type] = re.compile('|'.join(
self._filters[filter_type]))
def append_ignore_tag(self, tag):
"""
Appends a tag to the list of ignored tags. A string will be automatic
resolved into a tag object.
:param tag: Tag object or tag name.
"""
if isinstance(tag, basestring):
tag = self._table.lookup(tag)
self.ignored_tags.append(tag)
def remove_ignore_tag(self, tag):
"""
Removes a tag from the list of ignored tags. A string will be automatic
resolved into a tag object.
:param tag: Tag object or tag name.
"""
if isinstance(tag, basestring):
tag = self._table.lookup(tag)
self.ignored_tags.remove(tag)
def add_to_dictionary(self, word):
"""
Adds a word to user's dictionary.
:param word: The word to add.
"""
self._dictionary.add_to_pwl(word)
self.recheck()
def ignore_all(self, word):
"""
Ignores a word for the current session.
:param word: The word to ignore.
"""
self._dictionary.add_to_session(word)
self.recheck()
def check_range(self, start, end, force_all=False):
"""
Checks a specified range between two GtkTextIters.
:param start: Start iter - checking starts here.
:param end: End iter - checking ends here.
"""
if not self._enabled:
return
if end.inside_word(): end.forward_word_end()
if not start.starts_word() and (start.inside_word() or
start.ends_word()):
start.backward_word_start()
self._buffer.remove_tag(self._misspelled, start, end)
cursor = self._buffer.get_iter_at_mark(self._buffer.get_insert())
precursor = cursor.copy()
precursor.backward_char()
highlight = (cursor.has_tag(self._misspelled) or
precursor.has_tag(self._misspelled))
if not start.get_offset():
start.forward_word_end()
start.backward_word_start()
word_start = start.copy()
while word_start.compare(end) < 0:
word_end = word_start.copy()
word_end.forward_word_end()
in_word = ((word_start.compare(cursor) < 0) and
(cursor.compare(word_end) <= 0))
if in_word and not force_all:
if highlight:
self._check_word(word_start, word_end)
else:
self._deferred_check = True
else:
self._check_word(word_start, word_end)
self._deferred_check = False
word_end.forward_word_end()
word_end.backward_word_start()
if word_start.equal(word_end):
break
word_start = word_end.copy()
def _languages_menu(self):
def _set_language(item, code):
self.language = code
if _pygobject:
menu = gtk.Menu.new()
group = []
else:
menu = gtk.Menu()
group = gtk.RadioMenuItem()
connect = []
for code, name in self.languages:
if _pygobject:
item = gtk.RadioMenuItem.new_with_label(group, name)
group.append(item)
else:
item = gtk.RadioMenuItem(group, name)
if code == self.language:
item.set_active(True)
connect.append((item, code))
menu.append(item)
for item, code in connect:
item.connect('activate', _set_language, code)
return menu
def _suggestion_menu(self, word):
menu = []
suggestions = self._dictionary.suggest(word)
if not suggestions:
if _pygobject:
item = gtk.MenuItem.new()
label = gtk.Label.new('')
else:
item = gtk.MenuItem()
label = gtk.Label()
try:
label.set_halign(gtk.Align.LEFT)
except AttributeError:
label.set_alignment(0.0, 0.5)
label.set_markup('<i>{text}</i>'.format(text=_('(no suggestions)')))
item.add(label)
menu.append(item)
else:
for suggestion in suggestions:
if _pygobject:
item = gtk.MenuItem.new()
label = gtk.Label.new('')
else:
item = gtk.MenuItem()
label = gtk.Label()
label.set_markup('<b>{text}</b>'.format(text=suggestion))
try:
label.set_halign(gtk.Align.LEFT)
except AttributeError:
label.set_alignment(0.0, 0.5)
item.add(label)
item.connect('activate', self._replace_word, word, suggestion)
menu.append(item)
if _pygobject:
menu.append(gtk.SeparatorMenuItem.new())
item = gtk.MenuItem.new_with_label(
_('Add "{}" to Dictionary').format(word))
else:
menu.append(gtk.SeparatorMenuItem())
item = gtk.MenuItem(_('Add "{}" to Dictionary').format(word))
item.connect('activate', lambda *args: self.add_to_dictionary(word))
menu.append(item)
if _pygobject:
item = gtk.MenuItem.new_with_label(_('Ignore All'))
else:
item = gtk.MenuItem(_('Ignore All'))
item.connect('activate', lambda *args: self.ignore_all(word))
menu.append(item)
return menu
def _extend_menu(self, menu):
if not self._enabled:
return
if _pygobject:
separator = gtk.SeparatorMenuItem.new()
else:
separator = gtk.SeparatorMenuItem()
separator.show()
menu.prepend(separator)
if _pygobject:
languages = gtk.MenuItem.new_with_label(_('Languages'))
else:
languages = gtk.MenuItem(_('Languages'))
languages.set_submenu(self._languages_menu())
languages.show_all()
menu.prepend(languages)
if self._marks['click'].inside_word:
start, end = self._marks['click'].word
if start.has_tag(self._misspelled):
if _py3k:
word = self._buffer.get_text(start, end, False)
else:
word = self._buffer.get_text(start, end,
False).decode('utf-8')
items = self._suggestion_menu(word)
if self.collapse:
if _pygobject:
suggestions = gtk.MenuItem.new_with_label(
_('Suggestions'))
submenu = gtk.Menu.new()
else:
suggestions = gtk.MenuItem(_('Suggestions'))
submenu = gtk.Menu()
for item in items:
submenu.append(item)
suggestions.set_submenu(submenu)
suggestions.show_all()
menu.prepend(suggestions)
else:
items.reverse()
for item in items:
menu.prepend(item)
menu.show_all()
def _click_move_popup(self, *args):
self._marks['click'].move(self._buffer.get_iter_at_mark(
self._buffer.get_insert()))
return False
def _click_move_button(self, widget, event):
if event.button == 3:
if self._deferred_check: self._check_deferred_range(True)
x, y = self._view.window_to_buffer_coords(2, int(event.x),
int(event.y))
iter = self._view.get_iter_at_location(x, y)
if isinstance(iter, tuple):
iter = iter[1]
self._marks['click'].move(iter)
return False
def _before_text_insert(self, textbuffer, location, text, length):
self._marks['insert-start'].move(location)
def _after_text_insert(self, textbuffer, location, text, length):
start = self._marks['insert-start'].iter
self.check_range(start, location)
self._marks['insert-end'].move(location)
def _range_delete(self, textbuffer, start, end):
self.check_range(start, end)
def _mark_set(self, textbuffer, location, mark):
if mark == self._buffer.get_insert() and self._deferred_check:
self._check_deferred_range(False)
def _replace_word(self, item, old_word, new_word):
start, end = self._marks['click'].word
offset = start.get_offset()
self._buffer.begin_user_action()
self._buffer.delete(start, end)
self._buffer.insert(self._buffer.get_iter_at_offset(offset), new_word)
self._buffer.end_user_action()
self._dictionary.store_replacement(old_word, new_word)
def _check_deferred_range(self, force_all):
start = self._marks['insert-start'].iter
end = self._marks['insert-end'].iter
self.check_range(start, end, force_all)
def _check_word(self, start, end):
if start.has_tag(self.no_spell_check):
return
for tag in self.ignored_tags:
if start.has_tag(tag):
return
if _py3k:
word = self._buffer.get_text(start, end, False).strip()
else:
word = self._buffer.get_text(start, end, False).decode('utf-8').strip()
if not word:
return
if len(self._filters[SpellChecker.FILTER_WORD]):
if self._regexes[SpellChecker.FILTER_WORD].match(word):
return
if len(self._filters[SpellChecker.FILTER_LINE]):
line_start = self._buffer.get_iter_at_line(start.get_line())
line_end = end.copy()
line_end.forward_to_line_end()
if _py3k:
line = self._buffer.get_text(line_start, line_end, False)
else:
line = self._buffer.get_text(line_start, line_end,
False).decode('utf-8')
for match in self._regexes[SpellChecker.FILTER_LINE].finditer(line):
if match.start() <= start.get_line_offset() <= match.end():
start = self._buffer.get_iter_at_line_offset(
start.get_line(), match.start())
end = self._buffer.get_iter_at_line_offset(start.get_line(),
match.end())
self._buffer.remove_tag(self._misspelled, start, end)
return
if len(self._filters[SpellChecker.FILTER_TEXT]):
text_start, text_end = self._buffer.get_bounds()
if _py3k:
text = self._buffer.get_text(text_start, text_end, False)
else:
text = self._buffer.get_text(text_start, text_end,
False).decode('utf-8')
for match in self._regexes[SpellChecker.FILTER_TEXT].finditer(text):
if match.start() <= start.get_offset() <= match.end():
start = self._buffer.get_iter_at_offset(match.start())
end = self._buffer.get_iter_at_offset(match.end())
self._buffer.remove_tag(self._misspelled, start, end)
return
if not self._dictionary.check(word):
self._buffer.apply_tag(self._misspelled, start, end)

View File

@ -20,12 +20,12 @@ from collections import namedtuple
from gettext import gettext as _
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from uberwriter.helpers import get_builder
from uberwriter.helpers import get_descendant
from uberwriter.application import Application as app
class MainHeaderbar: #pylint: disable=too-few-public-methods
"""Sets up the main application headerbar
@ -36,14 +36,14 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
self.hb.props.show_close_button = True
self.hb.get_style_context().add_class("titlebar")
self.hb_revealer = Gtk.Revealer(name='titlebar_revealer')
self.hb_revealer = Gtk.Revealer(name='titlebar-revealer')
self.hb_revealer.add(self.hb)
self.hb_revealer.props.transition_duration = 1000
self.hb_revealer.props.transition_duration = 750
self.hb_revealer.props.transition_type = Gtk.RevealerTransitionType.CROSSFADE
self.hb_revealer.show()
self.hb_revealer.set_reveal_child(True)
self.hb_container = Gtk.Frame(name='titlebar_container')
self.hb_container = Gtk.Frame(name='titlebar-container')
self.hb_container.set_shadow_type(Gtk.ShadowType.NONE)
self.hb_container.add(self.hb_revealer)
self.hb_container.show()
@ -54,7 +54,7 @@ class MainHeaderbar: #pylint: disable=too-few-public-methods
self.hb.show_all()
class FsHeaderbar:
class FullscreenHeaderbar:
"""Sets up and manages the fullscreen headerbar and his events
"""
@ -141,7 +141,7 @@ def 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(_("Search and Replace"))
btn.menu.set_tooltip_text(_("Menu"))
btn.menu.set_image(Gtk.Image.new_from_icon_name("open-menu-symbolic",
Gtk.IconSize.BUTTON))
@ -153,6 +153,7 @@ def buttons(app):
return btn
def pack_buttons(headerbar, btn, btn_exit=None):
"""Pack the given buttons in the given headerbar

View File

@ -21,12 +21,16 @@ import logging
import os
import shutil
import gi
import pypandoc
from gi.overrides.Pango import Pango
from uberwriter.settings import Settings
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # pylint: disable=E0611
from uberwriter.uberwriterconfig import get_data_file
from uberwriter.config import get_data_file
from uberwriter.builder import Builder
@ -48,13 +52,18 @@ def get_builder(builder_file_name):
return builder
# Owais Lone : To get quick access to icons and stuff.
def path_to_file(path):
"""Return a file path (file:///) for the given path"""
return "file:///" + path
def get_media_file(media_file_path):
"""Return the full path of a given filename under the media dir
(starts with file:///)
"""
return "file:///" + get_media_path(media_file_path)
return path_to_file(get_media_path(media_file_path))
def get_media_path(media_file_name):
@ -160,6 +169,7 @@ def exist_executable(command):
return shutil.which(command) is not None
def get_descendant(widget, child_name, level, doPrint=False):
if widget is not None:
if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) +
@ -188,3 +198,14 @@ def get_descendant(widget, child_name, level, doPrint=False):
if child is not None:
found = get_descendant(child, child_name, level+1, doPrint) # //search the child
if found: return found
def get_char_width(widget):
return Pango.units_to_double(
widget.get_pango_context().get_metrics().get_approximate_char_width())
def pandoc_convert(text, to="html5", args=[], outputfile=None):
fr = Settings.new().get_value('input-format').get_string() or "markdown"
args.extend(["--quiet"])
return pypandoc.convert_text(text, to, fr, extra_args=args, outputfile=outputfile)

View File

@ -14,32 +14,29 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
# END LICENSE
import re
import urllib
from urllib.error import URLError
import webbrowser
import subprocess
import tempfile
import logging
import threading
import re
import subprocess
import telnetlib
import tempfile
import threading
import urllib
import webbrowser
from gettext import gettext as _
from urllib.error import URLError
from urllib.parse import unquote
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject
from uberwriter import latex_to_PNG
from uberwriter import latex_to_PNG, text_view_markup_handler
from uberwriter.settings import Settings
from uberwriter.fix_table import FixTable
from uberwriter.markup_buffer import MarkupBuffer
LOGGER = logging.getLogger('uberwriter')
GObject.threads_init() # Still needed?
# 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
@ -240,7 +237,7 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
if lexikon_dict:
for entry in lexikon_dict:
vocab_label = Gtk.Label.new(vocab + ' ~ ' + entry['class'])
vocab_label.get_style_context().add_class('lexikon_heading')
vocab_label.get_style_context().add_class('lexikon-heading')
vocab_label.set_halign(Gtk.Align.START)
vocab_label.set_justify(Gtk.Justification.LEFT)
grid.attach(vocab_label, 0, i, 3, 1)
@ -248,14 +245,14 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
for definition in entry['defs']:
i = i + 1
num_label = Gtk.Label.new(definition['num'])
num_label.get_style_context().add_class('lexikon_num')
num_label.get_style_context().add_class('lexikon-num')
num_label.set_justify(Gtk.Justification.RIGHT)
grid.attach(num_label, 0, i, 1, 1)
def_label = Gtk.Label.new(' '.join(definition['description']))
def_label.set_halign(Gtk.Align.START)
def_label.set_justify(Gtk.Justification.LEFT)
def_label.get_style_context().add_class('lexikon_definition')
def_label.get_style_context().add_class('lexikon-definition')
def_label.props.wrap = True
grid.attach(def_label, 1, i, 1, 1)
i = i + 1
@ -264,11 +261,11 @@ def fill_lexikon_bubble(vocab, lexikon_dict):
return None
class InlinePreview():
class InlinePreview:
def __init__(self, view, text_buffer):
self.text_view = view
self.text_buffer = text_buffer
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
self.latex_converter = latex_to_PNG.LatexToPNG()
cursor_mark = self.text_buffer.get_insert()
cursor_iter = self.text_buffer.get_iter_at_mark(cursor_mark)
@ -307,7 +304,7 @@ class InlinePreview():
# b.show_all()
# a.show_all()
self.popover = Gtk.Popover.new(lbl)
self.popover.get_style_context().add_class("QuickPreviewPopup")
self.popover.get_style_context().add_class("quick-preview-popup")
self.popover.add(alignment)
# a.add(alignment)
_dismiss, rect = self.popover.get_pointing_to()
@ -363,8 +360,8 @@ class InlinePreview():
text = self.text_buffer.get_text(start_iter, end_iter, False)
math = MarkupBuffer.regex["MATH"]
link = MarkupBuffer.regex["LINK"]
math = text_view_markup_handler.regex["MATH"]
link = text_view_markup_handler.regex["LINK"]
footnote = re.compile(r'\[\^([^\s]+?)\]')
image = re.compile(r"!\[(.*?)\]\((.+?)\)")

View File

@ -1,321 +0,0 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Pango
class MarkupBuffer():
def __init__(self, Parent, TextBuffer, base_leftmargin):
self.multiplier = 10
self.parent = Parent
self.text_buffer = TextBuffer
# Styles
self.italic = self.text_buffer.create_tag("italic",
style=Pango.Style.ITALIC)
self.emph = self.text_buffer.create_tag("emph",
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.bolditalic = self.text_buffer.create_tag("bolditalic",
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.headline_two = self.text_buffer.create_tag("headline_two",
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.normal_indent = self.text_buffer.create_tag('normal_indent', indent=100)
self.math_text = self.text_buffer.create_tag('math_text')
self.unfocused_text = self.text_buffer.create_tag('graytag',
foreground="gray")
self.underline = self.text_buffer.create_tag("underline",
underline=Pango.Underline.SINGLE)
self.underline.set_property('weight', Pango.Weight.BOLD)
self.strikethrough = self.text_buffer.create_tag("strikethrough",
strikethrough=True)
self.centertext = self.text_buffer.create_tag("centertext",
justification=Gtk.Justification.CENTER)
self.text_buffer.apply_tag(
self.normal_indent,
self.text_buffer.get_start_iter(),
self.text_buffer.get_end_iter()
)
self.rev_leftmargin = []
for i in range(0, 6):
name = "rev_marg_indent_left" + str(i)
self.rev_leftmargin.append(self.text_buffer.create_tag(name))
self.rev_leftmargin[i].set_property("left-margin", 90 - 10 * (i + 1))
self.rev_leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
#self.leftmargin[i].set_property("background", "gray")
self.leftmargin = []
for i in range(0, 6):
name = "marg_indent_left" + str(i)
self.leftmargin.append(self.text_buffer.create_tag(name))
self.leftmargin[i].set_property("left-margin", base_leftmargin + 10 + 10 * (i + 1))
self.leftmargin[i].set_property("indent", - 10 * (i + 1) - 10)
self.leftindent = []
for i in range(0, 15):
name = "indent_left" + str(i)
self.leftindent.append(self.text_buffer.create_tag(name))
self.leftindent[i].set_property("indent", - 10 * (i + 1) - 20)
self.table_env = self.text_buffer.create_tag('table_env')
self.table_env.set_property('wrap-mode', Gtk.WrapMode.NONE)
# self.table_env.set_property('font', 'Ubuntu Mono 13px')
self.table_env.set_property('pixels-above-lines', 0)
self.table_env.set_property('pixels-below-lines', 0)
self.update_style()
regex = {
"ITALIC": re.compile(r"(\*|_)(.*?)\1", re.UNICODE), # *asdasd* // _asdasd asd asd_
"STRONG": re.compile(r"(\*\*|__)(.*?)\1", re.UNICODE), # **as das** // __asdasd asd ad a__
"STRONGITALIC": re.compile(r"(\*\*\*|___)(.*?)\1"),
"BLOCKQUOTE": re.compile(r"^([\>]+ )", re.MULTILINE),
"STRIKETHROUGH": re.compile(r"~~[^ `~\n].+?~~"),
"LIST": re.compile(r"^[\-\*\+] ", re.MULTILINE),
"NUMERICLIST": re.compile(r"^((\d|[a-z]|\#)+[\.\)]) ", re.MULTILINE),
"INDENTEDLIST": re.compile(r"^(\t{1,6})((\d|[a-z]|\#)+[\.\)]|[\-\*\+]) ", re.MULTILINE),
"HEADINDICATOR": re.compile(r"^(#{1,6}) ", re.MULTILINE),
"HEADLINE": re.compile(r"^(#{1,6} [^\n]+)", re.MULTILINE),
"HEADLINE_TWO": re.compile(r"^\w.+\n[\=\-]{3,}", re.MULTILINE),
"MATH": re.compile(r"[\$]{1,2}([^` ].+?[^`\\ ])[\$]{1,2}"),
"HORIZONTALRULE": re.compile(r"(\n\n[\*\-]{3,}\n)"),
"TABLE": re.compile(r"^[\-\+]{5,}\n(.+?)\n[\-\+]{5,}\n", re.DOTALL),
"LINK": re.compile(r"\(http(.+?)\)")
}
def update_style(self):
(found, color) = self.parent.get_style_context().lookup_color('math_text_color')
if not found:
(_, color) = self.parent.get_style_context().lookup_color('foreground_color')
self.math_text.set_property("foreground", color.to_string())
def markup_buffer(self, mode=0):
buf = self.text_buffer
# Test for shifting first line
# bbs = buf.get_start_iter()
# bbb = buf.get_iter_at_offset(3)
# buf.apply_tag(self.ftag, bbs, bbb)
# Modes:
# 0 -> start to end
# 1 -> around the cursor
# 2 -> n.d.
if mode == 0:
context_start = buf.get_start_iter()
context_end = buf.get_end_iter()
context_offset = 0
elif mode == 1:
cursor_mark = buf.get_insert()
context_start = buf.get_iter_at_mark(cursor_mark)
context_start.backward_lines(3)
context_end = buf.get_iter_at_mark(cursor_mark)
context_end.forward_lines(2)
context_offset = context_start.get_offset()
text = buf.get_slice(context_start, context_end, False)
self.text_buffer.remove_tag(self.italic, context_start, context_end)
matches = re.finditer(self.regex["ITALIC"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.italic, start_iter, end_iter)
self.text_buffer.remove_tag(self.emph, context_start, context_end)
matches = re.finditer(self.regex["STRONG"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
matches = re.finditer(self.regex["STRONGITALIC"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.bolditalic, start_iter, end_iter)
self.text_buffer.remove_tag(self.strikethrough, context_start, context_end)
matches = re.finditer(self.regex["STRIKETHROUGH"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.strikethrough, start_iter, end_iter)
self.text_buffer.remove_tag(self.math_text, context_start, context_end)
matches = re.finditer(self.regex["MATH"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.math_text, start_iter, end_iter)
for margin in self.rev_leftmargin:
self.text_buffer.remove_tag(margin, context_start, context_end)
matches = re.finditer(self.regex["LIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.rev_leftmargin[0], start_iter, end_iter)
matches = re.finditer(self.regex["NUMERICLIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 1
if index < len(self.rev_leftmargin):
margin = self.rev_leftmargin[index]
self.text_buffer.apply_tag(margin, start_iter, end_iter)
matches = re.finditer(self.regex["BLOCKQUOTE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 2
if index < len(self.leftmargin):
self.text_buffer.apply_tag(self.leftmargin[index], start_iter, end_iter)
for leftindent in self.leftindent:
self.text_buffer.remove_tag(leftindent, context_start, context_end)
matches = re.finditer(self.regex["INDENTEDLIST"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = (len(match.group(1)) - 1) * 2 + len(match.group(2))
if index < len(self.leftindent):
self.text_buffer.apply_tag(self.leftindent[index], start_iter, end_iter)
matches = re.finditer(self.regex["HEADINDICATOR"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
index = len(match.group(1)) - 1
if index < len(self.rev_leftmargin):
margin = self.rev_leftmargin[index]
self.text_buffer.apply_tag(margin, start_iter, end_iter)
matches = re.finditer(self.regex["HORIZONTALRULE"], text)
rulecontext = context_start.copy()
rulecontext.forward_lines(3)
self.text_buffer.remove_tag(self.centertext, rulecontext, context_end)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
start_iter.forward_chars(2)
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.centertext, start_iter, end_iter)
matches = re.finditer(self.regex["HEADLINE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.emph, start_iter, end_iter)
matches = re.finditer(self.regex["HEADLINE_TWO"], text)
self.text_buffer.remove_tag(self.headline_two, rulecontext, context_end)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.headline_two, start_iter, end_iter)
matches = re.finditer(self.regex["TABLE"], text)
for match in matches:
start_iter = buf.get_iter_at_offset(context_offset + match.start())
end_iter = buf.get_iter_at_offset(context_offset + match.end())
self.text_buffer.apply_tag(self.table_env, start_iter, end_iter)
if self.parent.focusmode:
self.focusmode_highlight()
def focusmode_highlight(self):
start_document = self.text_buffer.get_start_iter()
end_document = self.text_buffer.get_end_iter()
self.text_buffer.remove_tag(
self.unfocused_text,
start_document,
end_document)
cursor = self.text_buffer.get_mark("insert")
cursor_iter = self.text_buffer.get_iter_at_mark(cursor)
end_sentence = cursor_iter.copy()
end_sentence.forward_sentence_end()
end_line = cursor_iter.copy()
end_line.forward_to_line_end()
comp = end_line.compare(end_sentence)
# if comp < 0, end_line is BEFORE end_sentence
if comp <= 0:
end_sentence = end_line
start_sentence = cursor_iter.copy()
start_sentence.backward_sentence_start()
# grey out everything before
self.text_buffer.apply_tag(
self.unfocused_text,
self.text_buffer.get_start_iter(), start_sentence)
self.text_buffer.apply_tag(
self.unfocused_text,
end_sentence, self.text_buffer.get_end_iter())
def set_multiplier(self, multiplier):
self.multiplier = multiplier
def recalculate(self, lm):
multiplier = self.multiplier
for i in range(0, 6):
new_margin = (lm - multiplier) - multiplier * (i + 1)
self.rev_leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
self.rev_leftmargin[i].set_property("indent", - multiplier * (i + 1) - multiplier)
for i in range(0, 6):
new_margin = (lm - multiplier) + multiplier + multiplier * (i + 1)
self.leftmargin[i].set_property("left-margin", 0 if new_margin < 0 else new_margin)
self.leftmargin[i].set_property("indent", - (multiplier - 1) * (i + 1) - multiplier)

View File

@ -18,23 +18,119 @@
"""this dialog adjusts values in gsettings
"""
import webbrowser
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # pylint: disable=E0611
from gi.repository import Gtk, Pango, GLib # pylint: disable=E0611
import logging
logger = logging.getLogger('uberwriter')
from uberwriter.helpers import get_builder, show_uri, get_help_uri
from uberwriter.helpers import get_builder
class PreferencesDialog:
class PreferencesDialog(Gtk.Window):
__gtype_name__ = "PreferencesDialog"
def __new__(cls):
"""Special static method that's automatically called by Python when
constructing a new instance of this class.
Returns a fully instantiated PreferencesDialog object.
"""
builder = get_builder('Preferences')
new_object = builder.get_object("PreferencesWindow")
return new_object
formats = [
{
"name": "Pandoc's Markdown",
"format": "markdown",
"help": "https://pandoc.org/MANUAL.html#pandocs-markdown"
},
{
"name": "CommonMark",
"format": "commonmark",
"help": "https://commonmark.org"
},
{
"name": "GitHub Flavored Markdown",
"format": "gfm",
"help": "https://help.github.com/en/categories/writing-on-github"
},
{
"name": "MultiMarkdown",
"format": "markdown_mmd",
"help": "https://fletcherpenney.net/multimarkdown"
},
{
"name": "Plain Markdown",
"format": "markdown_strict",
"help": "https://daringfireball.net/projects/markdown"
}
]
def __init__(self, settings):
self.settings = settings
self.builder = get_builder("Preferences")
self.dark_mode_auto_switch = self.builder.get_object("dark_mode_auto_switch")
self.dark_mode_auto_switch.set_active(self.settings.get_value("dark-mode-auto"))
self.dark_mode_auto_switch.connect("state-set", self.on_dark_mode_auto)
self.dark_mode_switch = self.builder.get_object("dark_mode_switch")
self.dark_mode_switch.set_active(self.settings.get_value("dark-mode"))
self.dark_mode_switch.connect("state-set", self.on_dark_mode)
self.spellcheck_switch = self.builder.get_object("spellcheck_switch")
self.spellcheck_switch.set_active(self.settings.get_value("spellcheck"))
self.spellcheck_switch.connect("state-set", self.on_spellcheck)
self.gradient_overlay_switch = self.builder.get_object("gradient_overlay_switch")
self.gradient_overlay_switch.set_active(self.settings.get_value("gradient-overlay"))
self.gradient_overlay_switch.connect("state-set", self.on_gradient_overlay)
input_format_store = Gtk.ListStore(int, str)
input_format = self.settings.get_value("input-format").get_string()
input_format_active = 0
for i, fmt in enumerate(self.formats):
input_format_store.append([i, fmt["name"]])
if fmt["format"] == input_format:
input_format_active = i
self.input_format_combobox = self.builder.get_object("input_format_combobox")
self.input_format_combobox.set_model(input_format_store)
input_format_renderer = Gtk.CellRendererText()
self.input_format_combobox.pack_start(input_format_renderer, True)
self.input_format_combobox.add_attribute(input_format_renderer, "text", 1)
self.input_format_combobox.set_active(input_format_active)
self.input_format_combobox.connect("changed", self.on_input_format)
self.input_format_help_button = self.builder.get_object("input_format_help_button")
self.input_format_help_button.connect('clicked', self.on_input_format_help)
def show(self, window):
preferences_window = self.builder.get_object("PreferencesWindow")
preferences_window.set_application(window.get_application())
preferences_window.set_transient_for(window)
preferences_window.show()
def on_dark_mode_auto(self, _, state):
self.settings.set_value("dark-mode-auto", GLib.Variant.new_boolean(state))
if state and self.dark_mode_switch.get_active():
self.dark_mode_switch.set_active(GLib.Variant.new_boolean(False))
return False
def on_dark_mode(self, _, state):
self.settings.set_value("dark-mode", GLib.Variant.new_boolean(state))
if state and self.dark_mode_auto_switch.get_active():
self.dark_mode_auto_switch.set_active(GLib.Variant.new_boolean(False))
return False
def on_spellcheck(self, _, state):
self.settings.set_value("spellcheck", GLib.Variant.new_boolean(state))
return False
def on_gradient_overlay(self, _, state):
self.settings.set_value("gradient-overlay", GLib.Variant.new_boolean(state))
return False
def on_input_format(self, combobox):
fmt = self.formats[combobox.get_active()]
self.settings.set_value("input-format", GLib.Variant.new_string(fmt["format"]))
def on_input_format_help(self, _):
fmt = self.formats[self.input_format_combobox.get_active()]
webbrowser.open(fmt["help"])

View File

@ -0,0 +1,48 @@
class Scroller:
def __init__(self, scrolled_window, source_pos, target_pos):
super().__init__()
self.scrolled_window = scrolled_window
self.source_pos = source_pos
self.target_pos = target_pos
self.duration = max(200, (target_pos - source_pos) / 50) * 1000
self.is_started = False
self.is_setup = False
self.start_time = 0
self.end_time = 0
self.tick_callback_id = 0
def start(self):
self.is_started = True
self.tick_callback_id = self.scrolled_window.add_tick_callback(self.on_tick)
def end(self):
self.scrolled_window.remove_tick_callback(self.tick_callback_id)
self.is_started = False
def setup(self, time):
self.start_time = time
self.end_time = time + self.duration
self.is_setup = True
def on_tick(self, widget, frame_clock):
def ease_out_cubic(value):
return pow(value - 1, 3) + 1
now = frame_clock.get_frame_time()
if not self.is_setup:
self.setup(now)
if now < self.end_time:
time = float(now - self.start_time) / float(self.end_time - self.start_time)
else:
time = 1
self.end()
time = ease_out_cubic(time)
pos = self.source_pos + (time * (self.target_pos - self.source_pos))
widget.get_vadjustment().props.value = pos
return True

View File

@ -14,22 +14,30 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import logging
import re
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository import Gdk
# from plugins import plugins
LOGGER = logging.getLogger('uberwriter')
class SearchAndReplace():
class SearchAndReplace:
"""
Adds (regex) search and replace functionality to
uberwriter
"""
def __init__(self, parentwindow):
def __init__(self, parentwindow, textview):
self.parentwindow = parentwindow
self.textview = textview
self.textbuffer = textview.get_buffer()
self.box = parentwindow.builder.get_object("searchbar_placeholder")
self.box.set_reveal_child(False)
self.searchbar = parentwindow.builder.get_object("searchbar")
@ -41,9 +49,6 @@ class SearchAndReplace():
self.open_replace_button = parentwindow.builder.get_object("replace")
self.open_replace_button.connect("toggled", self.toggle_replace)
self.textbuffer = parentwindow.text_buffer
self.texteditor = parentwindow.text_editor
self.nextbutton = parentwindow.builder.get_object("next_result")
self.prevbutton = parentwindow.builder.get_object("previous_result")
self.regexbutton = parentwindow.builder.get_object("regex")
@ -63,18 +68,17 @@ class SearchAndReplace():
self.prevbutton.connect('clicked', self.scrolltoprev)
self.regexbutton.connect('toggled', self.search)
self.casesensitivebutton.connect('toggled', self.search)
self.highlight = self.textbuffer.create_tag('search_highlight',
background="yellow")
self.highlight = self.textbuffer.create_tag('search_highlight', background="yellow")
self.texteditor.connect("focus-in-event", self.focused_texteditor)
self.textview.connect("focus-in-event", self.focused_texteditor)
self.matches = []
self.active = 0
def toggle_replace(self, widget, _data=None):
"""toggle the replace box
"""
if widget.get_active():
self.replacebox.set_reveal_child(True)
else:
self.replacebox.set_reveal_child(False)
self.replacebox.set_reveal_child(widget.get_active())
# TODO: refactorize!
def key_pressed(self, _widget, event, _data=None):
@ -100,13 +104,11 @@ class SearchAndReplace():
self.hide()
self.open_replace_button.set_active(False)
def search(self, _widget=None, _data=None, scroll=True):
searchtext = self.searchentry.get_text()
buf = self.textbuffer
context_start = buf.get_start_iter()
context_end = buf.get_end_iter()
text = buf.get_slice(context_start, context_end, False)
context_start = self.textbuffer.get_start_iter()
context_end = self.textbuffer.get_end_iter()
text = self.textbuffer.get_slice(context_start, context_end, False)
self.textbuffer.remove_tag(self.highlight, context_start, context_end)
@ -121,12 +123,12 @@ class SearchAndReplace():
matches = re.finditer(searchtext, text, flags)
self.matchiters = []
self.matches = []
self.active = 0
for match in matches:
start_iter = buf.get_iter_at_offset(match.start())
end_iter = buf.get_iter_at_offset(match.end())
self.matchiters.append((start_iter, end_iter))
self.matches.append((match.start(), match.end()))
start_iter = self.textbuffer.get_iter_at_offset(match.start())
end_iter = self.textbuffer.get_iter_at_offset(match.end())
self.textbuffer.apply_tag(self.highlight, start_iter, end_iter)
if scroll:
self.scrollto(self.active)
@ -139,43 +141,40 @@ class SearchAndReplace():
self.scrollto(self.active - 1)
def scrollto(self, index):
if not self.matchiters:
if not self.matches:
return
if index < len(self.matchiters):
self.active = index
else:
self.active = 0
self.active = index % len(self.matches)
matchiter = self.matchiters[self.active]
self.texteditor.get_buffer().select_range(matchiter[0], matchiter[1])
# self.texteditor.scroll_to_iter(matchiter[0], 0.0, True, 0.0, 0.5)
match = self.matches[self.active]
start_iter = self.textbuffer.get_iter_at_offset(match[0])
end_iter = self.textbuffer.get_iter_at_offset(match[1])
self.textbuffer.select_range(start_iter, end_iter)
def hide(self):
self.replacebox.set_reveal_child(False)
self.box.set_reveal_child(False)
self.textbuffer.remove_tag(self.highlight,
self.textbuffer.get_start_iter(),
self.textbuffer.get_end_iter())
self.texteditor.grab_focus()
self.textview.grab_focus()
def replace_clicked(self, _widget, _data=None):
self.replace(self.active)
def replace_all(self, _widget=None, _data=None):
while self.matchiters:
match = self.matchiters[0]
self.textbuffer.delete(match[0], match[1])
self.textbuffer.insert(match[0], self.replaceentry.get_text())
self.search(scroll=False)
for match in reversed(self.matches):
self.do_replace(match)
self.search(scroll=False)
def replace(self, searchindex, _inloop=False):
match = self.matchiters[searchindex]
self.textbuffer.delete(match[0], match[1])
self.textbuffer.insert(match[0], self.replaceentry.get_text())
self.do_replace(self.matches[searchindex])
active = self.active
self.search(scroll=False)
self.active = active
self.parentwindow.MarkupBuffer.markup_buffer()
self.scrollto(self.active)
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)
start_iter = self.textbuffer.get_iter_at_offset(match[0])
self.textbuffer.insert(start_iter, self.replaceentry.get_text())

View File

@ -27,7 +27,8 @@ class Settings(Gio.Settings):
"""
Gio.Settings.__init__(self)
def new():
@classmethod
def new(cls):
"""
Return a new Settings object
"""

View File

@ -58,7 +58,7 @@ class Sidebar():
Presentational class for shelves and files managed by the "sidebar"
parentwindow:
Reference to UberwriterWindow
Reference to Window
"""
def __init__(self, parentwindow):
"""

View File

@ -0,0 +1,75 @@
import math
import re
from queue import Queue
from threading import Thread
from gi.repository import GLib
from uberwriter import helpers
class StatsCounter:
"""Counts characters, words, sentences and read time using a background thread."""
# 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.
WORDS = re.compile(r"[\u3040-\uffff]|(?:\w+\S?\w*)+", re.UNICODE)
# Regexp that matches sentence-ending punctuation characters, ie. full stop, question mark,
# 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?")
def __init__(self):
super().__init__()
self.queue = Queue()
worker = Thread(target=self.__do_count, name="stats-counter")
worker.daemon = True
worker.start()
def count(self, text, callback):
"""Count stats for text, calling callback with a result when done.
The callback argument contains the result, in the form:
(characters, words, sentences, (hours, minutes, seconds))"""
self.queue.put((text, callback))
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:
return
if self.queue.empty():
break
text = helpers.pandoc_convert(text, to="plain")
character_count = len(re.findall(self.CHARACTERS, text))
word_count = len(re.findall(self.WORDS, text))
sentence_count = len(re.findall(self.SENTENCES, text))
paragraph_count = len(re.findall(self.PARAGRAPHS, text))
read_m, read_s = divmod(word_count / 200 * 60, 60)
read_h, read_m = divmod(read_m, 60)
read_time = (int(read_h), int(read_m), int(read_s))
GLib.idle_add(
callback,
(character_count, word_count, sentence_count, paragraph_count, read_time))

View File

@ -0,0 +1,98 @@
import math
import re
from gettext import gettext as _
from queue import Queue
from threading import Thread
from gi.repository import GLib, Gio, Gtk
from uberwriter import helpers
from uberwriter.helpers import get_builder
from uberwriter.settings import Settings
from uberwriter.stats_counter import StatsCounter
class StatsHandler:
"""Shows a default statistic on the stats button, and allows the user to toggle which one."""
def __init__(self, stats_button, text_view):
super().__init__()
self.stats_button = stats_button
self.stats_button.connect("clicked", self.on_stats_button_clicked)
self.stats_button.connect("destroy", self.on_destroy)
self.text_view = text_view
self.text_view.get_buffer().connect("changed", self.on_text_changed)
self.popover = None
self.characters = 0
self.words = 0
self.sentences = 0
self.paragraphs = 0
self.read_time = (0, 0, 0)
self.settings = Settings.new()
self.default_stat = self.settings.get_enum("stat-default")
self.stats_counter = StatsCounter()
self.update_default_stat()
def on_stats_button_clicked(self, _button):
self.stats_button.set_state_flags(Gtk.StateFlags.CHECKED, False)
menu = Gio.Menu()
stats = self.settings.props.settings_schema.get_key("stat-default").get_range()[1]
for i, stat in enumerate(stats):
menu_item = Gio.MenuItem.new(self.get_text_for_stat(i), None)
menu_item.set_action_and_target_value("app.stat_default", GLib.Variant.new_string(stat))
menu.append_item(menu_item)
self.popover = Gtk.Popover.new_from_model(self.stats_button, menu)
self.popover.connect('closed', self.on_popover_closed)
self.popover.popup()
def on_popover_closed(self, _popover):
self.stats_button.unset_state_flags(Gtk.StateFlags.CHECKED)
self.popover = None
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)
def get_text_for_stat(self, stat):
if stat == 0:
return _("{:n} Characters".format(self.characters))
elif stat == 1:
return _("{:n} Words".format(self.words))
elif stat == 2:
return _("{:n} Sentences".format(self.sentences))
elif stat == 3:
return _("{:n} Paragraphs".format(self.paragraphs))
elif stat == 4:
return _("{:d}:{:02d}:{:02d} Read Time".format(*self.read_time))
else:
raise ValueError("Unknown stat {}".format(stat))
def update_stats(self, stats):
(characters, words, sentences, paragraphs, read_time) = stats
self.characters = characters
self.words = words
self.sentences = sentences
self.paragraphs = paragraphs
self.read_time = read_time
self.update_default_stat(False)
def update_default_stat(self, close_popover=True):
stat = self.settings.get_enum("stat-default")
text = self.get_text_for_stat(stat)
self.stats_button.set_label(text)
if close_popover and self.popover:
self.popover.popdown()
def on_destroy(self, _widget):
self.stats_counter.stop()

View File

@ -1,487 +0,0 @@
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
"""Module for the TextView widgth wich encapsulates management of TextBuffer
and TextIter for common functionality, such as cut, copy, paste, undo, redo,
and highlighting of text.
Using
#create the TextEditor and set the text
editor = TextEditor()
editor.text = "Text to add to the editor"
#use cut, works the same for copy, paste, undo, and redo
def __handle_on_cut(self, widget, data=None):
self.editor.cut()
#add string to highlight
self.editor.add_highlight("Ubuntu")
self.editor.add_highlight("Quickly")
#remove highlights
self.editor.clear_highlight("Ubuntu")
self.editor.clear_all_highlight()
Configuring
#Configure as a TextView
self.editor.set_wrap_mode(Gtk.WRAP_CHAR)
#Access the Gtk.TextBuffer if needed
buffer = self.editor.get_buffer()
Extending
A TextEditor is Gtk.TextView
"""
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GObject
from uberwriter.format_shortcuts import FormatShortcuts
import logging
LOGGER = logging.getLogger('uberwriter')
class UndoableInsert:
"""something that has been inserted into our textbuffer"""
def __init__(self, text_iter, text, length):
self.offset = text_iter.get_offset()
self.text = text
self.length = length
if self.length > 1 or self.text in ("\r", "\n", " "):
self.mergeable = False
else:
self.mergeable = True
class UndoableDelete:
"""something that has ben deleted from our textbuffer"""
def __init__(self, text_buffer, start_iter, end_iter):
self.text = text_buffer.get_text(start_iter, end_iter, False)
self.start = start_iter.get_offset()
self.end = end_iter.get_offset()
# need to find out if backspace or delete key has been used
# so we don't mess up during redo
insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
self.mergeable = not bool(self.end - self.start > 1
or self.text in ("\r", "\n", " "))
class TextEditor(Gtk.TextView):
"""TextEditor encapsulates management of TextBuffer and TextIter for
common functionality, such as cut, copy, paste, undo, redo, and
highlighting of text.
"""
__gsignals__ = {
'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
'insert-ulistitem': (GObject.SignalFlags.ACTION, None, ()),
'insert-heading': (GObject.SignalFlags.ACTION, None, ()),
'insert-strikeout': (GObject.SignalFlags.ACTION, None, ()),
'undo': (GObject.SignalFlags.ACTION, None, ()),
'redo': (GObject.SignalFlags.ACTION, None, ())
}
def scroll_to_iter(self, iterable, *args):
self.get_buffer().place_cursor(iterable)
def __init__(self):
"""
Create a TextEditor
"""
Gtk.TextView.__init__(self)
self.undo_max = None
self.insert_event = self.get_buffer().connect("insert-text",
self.on_insert_text)
self.delete_event = self.get_buffer().connect("delete-range",
self.on_delete_range)
display = self.get_display()
self.clipboard = Gtk.Clipboard.get_for_display(display,
Gdk.SELECTION_CLIPBOARD)
self.undo_stack = []
self.redo_stack = []
self.not_undoable_action = False
self.undo_in_progress = False
self.can_delete = True
self.connect('key-press-event', self.on_key_press_event)
self.format_shortcuts = FormatShortcuts(self.get_buffer(), self)
self.connect('insert-italic', self.set_italic)
self.connect('insert-bold', self.set_bold)
self.connect('insert-strikeout', self.set_strikeout)
self.connect('insert-hrule', self.insert_horizontal_rule)
self.connect('insert-ulistitem', self.insert_unordered_list_item)
self.connect('insert-heading', self.insert_heading)
self.connect('redo', self.redo)
self.connect('undo', self.undo)
self.get_style_context().add_class("uberwriter-editor")
@property
def text(self):
"""
text - a string specifying all the text currently
in the TextEditor's buffer.
This property is read/write.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
end_iter = self.get_buffer().get_iter_at_offset(-1)
return self.get_buffer().get_text(start_iter, end_iter, False)
@property
def can_undo(self):
return bool(self.undo_stack)
@property
def can_redo(self):
return bool(self.redo_stack)
@text.setter
def text(self, text):
self.get_buffer().set_text(text)
def append(self, text):
"""append: appends text to the end of the textbuffer.
arguments:
text - a string to add to the buffer. The text will be the
last text in the buffer. The insertion cursor will not be moved.
"""
end_iter = self.get_buffer().get_iter_at_offset(-1)
self.get_buffer().insert(end_iter, text)
def prepend(self, text):
"""prepend: appends text to the start of the textbuffer.
arguments:
text - a string to add to the buffer. The text will be the
first text in the buffer. The insertion cursor will not be moved.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
self.get_buffer().insert(start_iter, text)
insert_iter = self.get_buffer().get_iter_at_offset(len(text)-1)
self.get_buffer().place_cursor(insert_iter)
def cursor_to_end(self):
"""cursor_to_end: moves the insertion curson to the last position
in the buffer.
"""
end_iter = self.get_buffer().get_iter_at_offset(-1)
self.get_buffer().place_cursor(end_iter)
def cursor_to_start(self):
"""cursor_to_start: moves the insertion curson to the first position
in the buffer.
"""
start_iter = self.get_buffer().get_iter_at_offset(0)
self.get_buffer().place_cursor(start_iter)
def cut(self, _widget=None, _data=None):
"""cut: cut currently selected text and put it on the clipboard.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().cut_clipboard(self.clipboard, True)
def copy(self, _widget=None, _data=None):
"""copy: copy currently selected text to the clipboard.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().copy_clipboard(self.clipboard)
def paste(self, _widget=None, _data=None):
"""paste: Insert any text currently on the clipboard into the
buffer.
This function can be called as a function, or assigned as a signal
handler.
"""
self.get_buffer().paste_clipboard(self.clipboard, None, True)
def undo(self, _widget=None, _data=None):
"""undo inserts or deletions
undone actions are being moved to redo stack"""
if not self.undo_stack:
return
self.begin_not_undoable_action()
self.undo_in_progress = True
undo_action = self.undo_stack.pop()
self.redo_stack.append(undo_action)
buf = self.get_buffer()
if isinstance(undo_action, UndoableInsert):
offset = undo_action.offset
start = buf.get_iter_at_offset(offset)
stop = buf.get_iter_at_offset(
offset + undo_action.length
)
buf.place_cursor(start)
buf.delete(start, stop)
else:
start = buf.get_iter_at_offset(undo_action.start)
buf.insert(start, undo_action.text)
if undo_action.delete_key_used:
buf.place_cursor(start)
else:
stop = buf.get_iter_at_offset(undo_action.end)
buf.place_cursor(stop)
self.end_not_undoable_action()
self.undo_in_progress = False
def redo(self, _widget=None, _data=None):
"""redo inserts or deletions
redone actions are moved to undo stack"""
if not self.redo_stack:
return
self.begin_not_undoable_action()
self.undo_in_progress = True
redo_action = self.redo_stack.pop()
self.undo_stack.append(redo_action)
buf = self.get_buffer()
if isinstance(redo_action, UndoableInsert):
start = buf.get_iter_at_offset(redo_action.offset)
buf.insert(start, redo_action.text)
new_cursor_pos = buf.get_iter_at_offset(
redo_action.offset + redo_action.length
)
buf.place_cursor(new_cursor_pos)
else:
start = buf.get_iter_at_offset(redo_action.start)
stop = buf.get_iter_at_offset(redo_action.end)
buf.delete(start, stop)
buf.place_cursor(start)
self.end_not_undoable_action()
self.undo_in_progress = False
def on_insert_text(self, _textbuffer, text_iter, text, _length):
"""
_on_insert: internal function to handle programatically inserted
text. Do not call directly.
"""
def can_be_merged(prev, cur):
"""see if we can merge multiple inserts here
will try to merge words or whitespace
can't merge if prev and cur are not mergeable in the first place
can't merge when user set the input bar somewhere else
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if cur.offset != (prev.offset + prev.length):
return False
if cur.text in whitespace and not prev.text in whitespace:
return False
if prev.text in whitespace and not cur.text in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableInsert(text_iter, text, len(text))
try:
prev_insert = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_insert, UndoableInsert):
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_insert, undo_action):
prev_insert.length += undo_action.length
prev_insert.text += undo_action.text
self.undo_stack.append(prev_insert)
else:
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
def on_delete_range(self, text_buffer, start_iter, end_iter):
"""On delete
"""
def can_be_merged(prev, cur):
"""see if we can merge multiple deletions here
will try to merge words or whitespace
can't merge if prev and cur are not mergeable in the first place
can't merge if delete and backspace key were both used
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if prev.delete_key_used != cur.delete_key_used:
return False
if prev.start != cur.start and prev.start != cur.end:
return False
if cur.text not in whitespace and \
prev.text in whitespace:
return False
if cur.text in whitespace and \
prev.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
try:
prev_delete = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_delete, UndoableDelete):
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_delete, undo_action):
if prev_delete.start == undo_action.start: # delete key used
prev_delete.text += undo_action.text
prev_delete.end += (undo_action.end - undo_action.start)
else: # Backspace used
prev_delete.text = "%s%s" % (undo_action.text,
prev_delete.text)
prev_delete.start = undo_action.start
self.undo_stack.append(prev_delete)
else:
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
def begin_not_undoable_action(self):
"""don't record the next actions
toggles self.not_undoable_action"""
self.not_undoable_action = True
def end_not_undoable_action(self):
"""record next actions
toggles self.not_undoable_action"""
self.not_undoable_action = False
def on_key_press_event(self, widget, event):
if widget == self and not self.can_delete:
return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
else:
return False
def set_italic(self, _widget, _data=None):
"""Ctrl + I"""
self.format_shortcuts.italic()
def set_bold(self, _widget, _data=None):
"""Ctrl + Shift + D"""
self.format_shortcuts.bold()
def set_strikeout(self, _widget, _data=None):
"""Ctrl + B"""
self.format_shortcuts.strikeout()
def insert_horizontal_rule(self, _widget, _data=None):
"""Ctrl + R"""
self.format_shortcuts.rule()
def insert_unordered_list_item(self, _widget, _data=None):
"""Ctrl + U"""
self.format_shortcuts.unordered_list_item()
def insert_ordered_list(self, _widget, _data=None):
"""CTRL + O"""
self.format_shortcuts.ordered_list_item()
def insert_heading(self, _widget, _data=None):
"""CTRL + H"""
self.format_shortcuts.heading()
class TestWindow(Gtk.Window):
"""For testing and demonstrating AsycnTaskProgressBox.
"""
def __init__(self):
# create a window a VBox to hold the controls
Gtk.Window.__init__(self)
self.set_title("TextEditor Test Window")
windowbox = Gtk.VBox(homogeneous=False, spacing=2)
windowbox.show()
self.add(windowbox)
self.editor = TextEditor()
self.editor.show()
windowbox.pack_end(self.editor, True, True, 0)
self.set_size_request(200, 200)
self.show()
self.maximize()
self.connect("destroy", Gtk.main_quit)
self.editor.text = "this is some inserted text"
self.editor.append("\nLine 3")
self.editor.prepend("Line1\n")
self.editor.cursor_to_end()
self.editor.cursor_to_start()
self.editor.undo_max = 100
cut_button = Gtk.Button(label="Cut")
cut_button.connect("clicked", self.editor.cut)
cut_button.show()
windowbox.pack_start(cut_button, False, False, 0)
copy_button = Gtk.Button(label="Copy")
copy_button.connect("clicked", self.editor.copy)
copy_button.show()
windowbox.pack_start(copy_button, False, False, 0)
paste_button = Gtk.Button(label="Paste")
paste_button.connect("clicked", self.editor.paste)
paste_button.show()
windowbox.pack_start(paste_button, False, False, 0)
undo_button = Gtk.Button(label="Undo")
undo_button.connect("clicked", self.editor.undo)
undo_button.show()
windowbox.pack_start(undo_button, False, False, 0)
redo_button = Gtk.Button(label="Redo")
redo_button.connect("clicked", self.editor.redo)
redo_button.show()
windowbox.pack_start(redo_button, False, False, 0)
if __name__ == "__main__":
TEST = TestWindow()
Gtk.main()

View File

@ -0,0 +1,205 @@
import gi
from uberwriter.inline_preview import InlinePreview
from uberwriter.text_view_format_inserter import FormatInserter
from uberwriter.text_view_markup_handler import MarkupHandler
from uberwriter.text_view_undo_redo_handler import UndoRedoHandler
from uberwriter.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
from uberwriter.scroller import Scroller
gi.require_version('Gtk', '3.0')
gi.require_version('Gspell', '1')
from gi.repository import Gtk, Gdk, GObject, GLib, Gspell
import logging
LOGGER = logging.getLogger('uberwriter')
class TextView(Gtk.TextView):
"""UberwriterTextView encapsulates all the features around the editor.
It combines the following:
- Undo / redo (via TextBufferUndoRedoHandler)
- Format shortcuts (via TextBufferShortcutInserter)
- Markup (via TextBufferMarkupHandler)
- Preview popover (via TextBufferMarkupHandler)
- Drag and drop (via TextViewDragDropHandler)
- Scrolling (via TextViewScroller)
- The various modes supported by UberWriter (eg. Focus Mode, Hemingway Mode)
"""
__gsignals__ = {
'insert-italic': (GObject.SignalFlags.ACTION, None, ()),
'insert-bold': (GObject.SignalFlags.ACTION, None, ()),
'insert-hrule': (GObject.SignalFlags.ACTION, None, ()),
'insert-listitem': (GObject.SignalFlags.ACTION, None, ()),
'insert-header': (GObject.SignalFlags.ACTION, None, ()),
'insert-strikethrough': (GObject.SignalFlags.ACTION, None, ()),
'undo': (GObject.SignalFlags.ACTION, None, ()),
'redo': (GObject.SignalFlags.ACTION, None, ())
}
def __init__(self):
super().__init__()
# Appearance
self.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
self.set_pixels_above_lines(4)
self.set_pixels_below_lines(4)
self.set_pixels_inside_wrap(8)
self.get_style_context().add_class('uberwriter-editor')
# General behavior
self.get_buffer().connect('changed', self.on_text_changed)
self.connect('size-allocate', self.on_size_allocate)
# Spell checking
self.gspell_view = Gspell.TextView.get_from_gtk_text_view(self)
self.gspell_view.basic_setup()
# Undo / redo
self.undo_redo = UndoRedoHandler()
self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text)
self.get_buffer().connect('delete-range', self.undo_redo.on_delete_range)
self.connect('undo', self.undo_redo.undo)
self.connect('redo', self.undo_redo.redo)
# Format shortcuts
self.shortcut = FormatInserter()
self.connect('insert-italic', self.shortcut.insert_italic)
self.connect('insert-bold', self.shortcut.insert_bold)
self.connect('insert-strikethrough', self.shortcut.insert_strikethrough)
self.connect('insert-hrule', self.shortcut.insert_horizontal_rule)
self.connect('insert-listitem', self.shortcut.insert_list_item)
self.connect('insert-header', self.shortcut.insert_header)
# Markup
self.markup = MarkupHandler(self)
self.connect('style-updated', self.markup.on_style_updated)
# Preview popover
self.preview_popover = InlinePreview(self)
# Drag and drop
self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT)
# Scrolling
self.scroller = None
self.get_buffer().connect('mark-set', self.on_mark_set)
# Focus mode
self.focus_mode = False
self.connect('button-release-event', self.on_button_release_event)
# Hemingway mode
self.hemingway_mode = False
self.connect('key-press-event', self.on_key_press_event)
def get_text(self):
text_buffer = self.get_buffer()
start_iter = text_buffer.get_start_iter()
end_iter = text_buffer.get_end_iter()
return text_buffer.get_text(start_iter, end_iter, False)
def set_text(self, text):
text_buffer = self.get_buffer()
text_buffer.set_text(text)
def on_text_changed(self, *_):
self.markup.apply()
GLib.idle_add(self.scroll_to)
def on_size_allocate(self, *_):
self.update_vertical_margin()
self.markup.update_margins_indents()
def set_focus_mode(self, focus_mode):
"""Toggle focus mode.
When in focus mode, the cursor sits in the middle of the text view,
and the surrounding text is greyed out."""
self.focus_mode = focus_mode
self.gspell_view.set_inline_spell_checking(not focus_mode)
self.update_vertical_margin()
self.markup.apply()
self.scroll_to()
def update_vertical_margin(self):
if self.focus_mode:
height = self.get_allocation().height
self.props.top_margin = height / 2
self.props.bottom_margin = height / 2
else:
self.props.top_margin = 80
self.props.bottom_margin = 64
def on_button_release_event(self, _widget, _event):
if self.focus_mode:
self.markup.apply()
return False
def set_hemingway_mode(self, hemingway_mode):
"""Toggle hemingway mode.
When in hemingway mode, the backspace and delete keys are ignored."""
self.hemingway_mode = hemingway_mode
def on_key_press_event(self, _widget, event):
if self.hemingway_mode:
return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
else:
return False
def clear(self):
"""Clear text and undo history"""
self.get_buffer().set_text('')
self.undo_redo.clear()
def scroll_to(self, mark=None):
"""Scrolls if needed to ensure mark is visible.
If mark is unspecified, the cursor is used."""
margin = 32
scrolled_window = self.get_ancestor(Gtk.ScrolledWindow.__gtype__)
if not scrolled_window:
return
va = scrolled_window.get_vadjustment()
if va.props.page_size < margin * 2:
return
text_buffer = self.get_buffer()
if mark:
mark_iter = text_buffer.get_iter_at_mark(mark)
else:
mark_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
mark_rect = self.get_iter_location(mark_iter)
pos_y = mark_rect.y + mark_rect.height + self.props.top_margin
pos = pos_y - va.props.value
target_pos = None
if self.focus_mode:
if pos != (va.props.page_size * 0.5):
target_pos = pos_y - (va.props.page_size * 0.5)
elif pos > va.props.page_size - margin:
target_pos = pos_y - va.props.page_size + margin
elif pos < margin:
target_pos = pos_y - margin
if self.scroller and self.scroller.is_started:
self.scroller.end()
if target_pos:
self.scroller = Scroller(scrolled_window, va.props.value, target_pos)
self.scroller.start()
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
if mark.get_name() == 'insert':
self.markup.apply()
if self.focus_mode:
self.scroll_to(mark)
elif mark.get_name() == 'gtk_drag_target':
self.scroll_to(mark)
return True

View File

@ -0,0 +1,62 @@
import mimetypes
import urllib
from gi.repository import Gtk
(TARGET_URI, TARGET_TEXT) = range(2)
class DragDropHandler:
TARGET_URI = None
def __init__(self, text_view, *targets):
super().__init__()
self.target_list = Gtk.TargetList.new([])
if TARGET_URI in targets:
self.target_list.add_uri_targets(TARGET_URI)
if TARGET_TEXT in targets:
self.target_list.add_text_targets(TARGET_TEXT)
text_view.drag_dest_set_target_list(self.target_list)
text_view.connect_after('drag-data-received', self.on_drag_data_received)
def on_drag_data_received(self, text_view, drag_context, _x, _y, data, info, time):
"""Handle drag and drop events"""
text_buffer = text_view.get_buffer()
if info == TARGET_URI:
uris = data.get_uris()
for uri in uris:
uri = urllib.parse.unquote_plus(uri)
mime = mimetypes.guess_type(uri)
if mime[0] is not None and mime[0].startswith('image'):
if uri.startswith("file://"):
uri = uri[7:]
text = "![Image caption](%s)" % uri
limit_left = 2
limit_right = 23
else:
text = "[Link description](%s)" % uri
limit_left = 1
limit_right = 22
text_buffer.place_cursor(text_buffer.get_iter_at_mark(
text_buffer.get_mark('gtk_drag_target')))
text_buffer.insert_at_cursor(text)
insert_mark = text_buffer.get_insert()
selection_bound = text_buffer.get_selection_bound()
cursor_iter = text_buffer.get_iter_at_mark(insert_mark)
cursor_iter.backward_chars(len(text) - limit_left)
text_buffer.move_mark(insert_mark, cursor_iter)
cursor_iter.forward_chars(limit_right)
text_buffer.move_mark(selection_bound, cursor_iter)
elif info == TARGET_TEXT:
text_buffer.place_cursor(text_buffer.get_iter_at_mark(
text_buffer.get_mark('gtk_drag_target')))
text_buffer.insert_at_cursor(data.get_text())
Gtk.drag_finish(drag_context, True, True, time)
text_view.get_window().present_with_time(time)
return False

View File

@ -0,0 +1,154 @@
from gettext import gettext as _
class FormatInserter:
"""Manages insertion of formatting.
Methods can be called directly, as well as be used as signal callbacks."""
def insert_italic(self, text_view, _data=None):
"""Insert italic or mark a selection as bold"""
self.__wrap(text_view, "_", _("italic text"))
def insert_bold(self, text_view, _data=None):
"""Insert bold or mark a selection as bold"""
self.__wrap(text_view, "**", _("bold text"))
def insert_strikethrough(self, text_view, _data=None):
"""Insert strikethrough or mark a selection as strikethrough"""
self.__wrap(text_view, "~~", _("strikethrough text"))
def insert_horizontal_rule(self, text_view, _data=None):
"""Insert horizontal rule"""
text_buffer = text_view.get_buffer()
text_buffer.insert_at_cursor("\n\n---\n")
text_view.scroll_mark_onscreen(text_buffer.get_insert())
def insert_list_item(self, text_view, _data=None):
"""Insert list item or mark a selection as list item"""
text_buffer = text_view.get_buffer()
if text_buffer.get_has_selection():
(start, end) = text_buffer.get_selection_bounds()
if start.starts_line():
text = text_buffer.get_text(start, end, False)
if text.startswith(("- ", "* ", "+ ")):
delete_end = start.forward_chars(2)
text_buffer.delete(start, delete_end)
else:
text_buffer.insert(start, "- ")
else:
helptext = _("Item")
text_length = len(helptext)
cursor_mark = text_buffer.get_insert()
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
start_ext = cursor_iter.copy()
start_ext.backward_lines(3)
text = text_buffer.get_text(cursor_iter, start_ext, False)
lines = text.splitlines()
for line in reversed(lines):
if line and line.startswith(("- ", "* ", "+ ")):
if cursor_iter.starts_line():
text_buffer.insert_at_cursor(line[:2] + helptext)
else:
text_buffer.insert_at_cursor(
"\n" + line[:2] + helptext)
break
else:
if not lines[-1] and not lines[-2]:
text_buffer.insert_at_cursor("- " + helptext)
elif not lines[-1]:
if cursor_iter.starts_line():
text_buffer.insert_at_cursor("- " + helptext)
else:
text_buffer.insert_at_cursor("\n- " + helptext)
else:
text_buffer.insert_at_cursor("\n\n- " + helptext)
break
self.__select_text(text_view, 0, text_length)
def insert_ordered_list_item(self, _text_view, _data=None):
# TODO: implement ordered lists
pass
def insert_header(self, text_view, _data=None):
"""Insert header or mark a selection as a list header"""
text_buffer = text_view.get_buffer()
if text_buffer.get_has_selection():
(start, end) = text_buffer.get_selection_bounds()
text = text_buffer.get_text(start, end, False)
text_buffer.delete(start, end)
else:
text = _("Header")
text_buffer.insert_at_cursor("#" + " " + text)
self.__select_text(text_view, 0, len(text))
@staticmethod
def __wrap(text_view, wrap, helptext=""):
"""Inserts wrap format to the selected text (helper text when nothing selected)"""
text_buffer = text_view.get_buffer()
if text_buffer.get_has_selection():
# Find current highlighting
(start, end) = text_buffer.get_selection_bounds()
moved = False
if (start.get_offset() >= len(wrap) and
end.get_offset() <= text_buffer.get_char_count() - len(wrap)):
moved = True
ext_start = start.copy()
ext_start.backward_chars(len(wrap))
ext_end = end.copy()
ext_end.forward_chars(len(wrap))
text = text_buffer.get_text(ext_start, ext_end, True)
else:
text = text_buffer.get_text(start, end, True)
if moved and text.startswith(wrap) and text.endswith(wrap):
text = text[len(wrap):-len(wrap)]
new_text = text
text_buffer.delete(ext_start, ext_end)
move_back = 0
else:
if moved:
text = text[len(wrap):-len(wrap)]
new_text = text.lstrip().rstrip()
text = text.replace(new_text, wrap + new_text + wrap)
text_buffer.delete(start, end)
move_back = len(wrap)
text_buffer.insert_at_cursor(text)
text_length = len(new_text)
else:
text_buffer.insert_at_cursor(wrap + helptext + wrap)
text_length = len(helptext)
move_back = len(wrap)
cursor_mark = text_buffer.get_insert()
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
cursor_iter.backward_chars(move_back)
text_buffer.move_mark_by_name('selection_bound', cursor_iter)
cursor_iter.backward_chars(text_length)
text_buffer.move_mark_by_name('insert', cursor_iter)
@staticmethod
def __select_text(text_view, offset, length):
"""Selects text starting at the current cursor minus offset, length characters."""
text_buffer = text_view.get_buffer()
cursor_mark = text_buffer.get_insert()
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
cursor_iter.backward_chars(offset)
text_buffer.move_mark_by_name('selection_bound', cursor_iter)
cursor_iter.backward_chars(length)
text_buffer.move_mark_by_name('insert', cursor_iter)

View File

@ -0,0 +1,307 @@
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
### BEGIN LICENSE
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
### END LICENSE
import re
import gi
from gi.overrides import GLib
from uberwriter import helpers
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
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", re.MULTILINE),
"LIST": re.compile(r"^((?:\t|[ ]{4})*)[\-*+] .+", re.MULTILINE),
"NUMERICLIST": re.compile(r"^((\d|[a-z]|#)+[.)]) ", 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"^[ ]{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}"),
}
def __init__(self, text_view):
self.text_view = text_view
self.text_buffer = text_view.get_buffer()
# Styles
buffer = self.text_buffer
self.italic = buffer.create_tag('italic',
weight=Pango.Weight.NORMAL,
style=Pango.Style.ITALIC)
self.bold = buffer.create_tag('bold',
weight=Pango.Weight.BOLD,
style=Pango.Style.NORMAL)
self.bolditalic = buffer.create_tag('bolditalic',
weight=Pango.Weight.BOLD,
style=Pango.Style.ITALIC)
self.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')
self.table.set_property('wrap-mode', Gtk.WrapMode.NONE)
self.table.set_property('pixels-above-lines', 0)
self.table.set_property('pixels-below-lines', 0)
self.mathtext = buffer.create_tag('mathtext')
self.graytext = buffer.create_tag('graytext',
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.margins_indents = {}
self.update_margins_indents()
# Style
self.on_style_updated()
self.version = 0
def on_style_updated(self, *_):
(found, color) = self.text_view.get_style_context().lookup_color('math_text_color')
if not found:
(_, color) = self.text_view.get_style_context().lookup_color('foreground_color')
self.mathtext.set_property("foreground", color.to_string())
def apply(self):
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)
def do_apply(self, version=None):
if version is not None and version != self.version:
return
buffer = self.text_buffer
start = buffer.get_start_iter()
end = buffer.get_end_iter()
offset = 0
text = buffer.get_slice(start, end, False)
# Remove tags
buffer.remove_tag(self.italic, start, end)
buffer.remove_tag(self.bold, start, end)
buffer.remove_tag(self.bolditalic, start, end)
buffer.remove_tag(self.strikethrough, start, end)
buffer.remove_tag(self.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)
# 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)
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)
# 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
# of multi-line text. Indent is applied to every line.
# * Margin level can be negative, as a baseline margin exists from which it can be subtracted.
# 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:
tag = self.text_buffer.create_tag(
"margin_indent_" + str(margin_level) + "_" + str(indent_level))
margin, indent = self.get_margin_indent(margin_level, indent_level)
tag.set_property("left-margin", margin)
tag.set_property("indent", indent)
self.margins_indents[level] = tag
return tag
else:
return self.margins_indents[level]
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
if baseline_margin is None:
baseline_margin = self.text_view.get_left_margin()
if char_width is None:
char_width = helpers.get_char_width(self.text_view)
margin = max(baseline_margin + char_width * margin_level, 0)
indent = char_width * indent_level
return margin, indent
def update_margins_indents(self):
baseline_margin = self.text_view.get_left_margin()
char_width = helpers.get_char_width(self.text_view)
# Adjust tab size, as character width can change
tab_array = Pango.TabArray.new(1, True)
tab_array.set_tab(0, Pango.TabAlign.LEFT, 4 * char_width)
self.text_view.set_tabs(tab_array)
# Adjust margins and indents, as character width can change
for level, tag in self.margins_indents.items():
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
tag.set_property("left-margin", margin)
tag.set_property("indent", indent)

View File

@ -0,0 +1,204 @@
class UndoableInsert:
"""Something has been inserted into text_buffer"""
def __init__(self, text_iter, text, length):
self.offset = text_iter.get_offset()
self.text = text
self.length = length
self.mergeable = not bool(self.length > 1 or self.text in ("\r", "\n", " "))
class UndoableDelete:
"""Something has been deleted from text_buffer"""
def __init__(self, text_buffer, start_iter, end_iter):
self.text = text_buffer.get_text(start_iter, end_iter, False)
self.start = start_iter.get_offset()
self.end = end_iter.get_offset()
# Find out if backspace or delete were used to not mess up redo
insert_iter = text_buffer.get_iter_at_mark(text_buffer.get_insert())
self.delete_key_used = bool(insert_iter.get_offset() <= self.start)
self.mergeable = not bool(self.end - self.start > 1 or self.text in ("\r", "\n", " "))
class UndoRedoHandler:
"""Manages undo/redo for a given text_buffer.
Methods can be called directly, as well as be used as signal callbacks."""
def __init__(self):
self.undo_stack = []
self.redo_stack = []
self.not_undoable_action = False
self.undo_in_progress = False
def undo(self, text_view, _data=None):
"""Undo insertions or deletions. Undone actions are moved to redo stack.
This method can be registered to a custom undo signal, or used independently."""
if not self.undo_stack:
return
self.__begin_not_undoable_action()
self.undo_in_progress = True
undo_action = self.undo_stack.pop()
self.redo_stack.append(undo_action)
text_buffer = text_view.get_buffer()
if isinstance(undo_action, UndoableInsert):
offset = undo_action.offset
start = text_buffer.get_iter_at_offset(offset)
stop = text_buffer.get_iter_at_offset(
offset + undo_action.length
)
text_buffer.place_cursor(start)
text_buffer.delete(start, stop)
else:
start = text_buffer.get_iter_at_offset(undo_action.start)
text_buffer.insert(start, undo_action.text)
if undo_action.delete_key_used:
text_buffer.place_cursor(start)
else:
stop = text_buffer.get_iter_at_offset(undo_action.end)
text_buffer.place_cursor(stop)
self.__end_not_undoable_action()
self.undo_in_progress = False
def redo(self, text_view, _data=None):
"""Redo insertions or deletions. Redone actions are moved to undo stack
This method can be registered to a custom redo signal, or used independently."""
if not self.redo_stack:
return
self.__begin_not_undoable_action()
self.undo_in_progress = True
redo_action = self.redo_stack.pop()
self.undo_stack.append(redo_action)
text_buffer = text_view.get_buffer()
if isinstance(redo_action, UndoableInsert):
start = text_buffer.get_iter_at_offset(redo_action.offset)
text_buffer.insert(start, redo_action.text)
new_cursor_pos = text_buffer.get_iter_at_offset(
redo_action.offset + redo_action.length)
text_buffer.place_cursor(new_cursor_pos)
else:
start = text_buffer.get_iter_at_offset(redo_action.start)
stop = text_buffer.get_iter_at_offset(redo_action.end)
text_buffer.delete(start, stop)
text_buffer.place_cursor(start)
self.__end_not_undoable_action()
self.undo_in_progress = False
def clear(self):
self.undo_stack = []
self.redo_stack = []
def on_insert_text(self, _text_buffer, text_iter, text, _length):
"""Registers a text insert. Refer to TextBuffer's "insert-text" signal.
This method must be registered to TextBuffer's "insert-text" signal, or called manually."""
def can_be_merged(prev, cur):
"""Check if multiple insertions can be merged
can't merge if prev and cur are not mergeable in the first place
can't merge when user set the input bar somewhere else
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if cur.offset != (prev.offset + prev.length):
return False
if cur.text in whitespace and prev.text not in whitespace:
return False
if prev.text in whitespace and cur.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableInsert(text_iter, text, len(text))
try:
prev_insert = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_insert, UndoableInsert):
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_insert, undo_action):
prev_insert.length += undo_action.length
prev_insert.text += undo_action.text
self.undo_stack.append(prev_insert)
else:
self.undo_stack.append(prev_insert)
self.undo_stack.append(undo_action)
def on_delete_range(self, text_buffer, start_iter, end_iter):
"""Registers a range deletion. Refer to TextBuffer's "delete-range" signal.
This method must be registered to TextBuffer's "delete-range" signal, or called manually."""
def can_be_merged(prev, cur):
"""Check if multiple deletions can be merged
can't merge if prev and cur are not mergeable in the first place
can't merge if delete and backspace key were both used
can't merge across word boundaries"""
whitespace = (' ', '\t')
if not cur.mergeable or not prev.mergeable:
return False
if prev.delete_key_used != cur.delete_key_used:
return False
if prev.start != cur.start and prev.start != cur.end:
return False
if cur.text not in whitespace and \
prev.text in whitespace:
return False
if cur.text in whitespace and \
prev.text not in whitespace:
return False
return True
if not self.undo_in_progress:
self.redo_stack = []
if self.not_undoable_action:
return
undo_action = UndoableDelete(text_buffer, start_iter, end_iter)
try:
prev_delete = self.undo_stack.pop()
except IndexError:
self.undo_stack.append(undo_action)
return
if not isinstance(prev_delete, UndoableDelete):
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
return
if can_be_merged(prev_delete, undo_action):
if prev_delete.start == undo_action.start: # delete key used
prev_delete.text += undo_action.text
prev_delete.end += (undo_action.end - undo_action.start)
else: # Backspace used
prev_delete.text = "%s%s" % (undo_action.text,
prev_delete.text)
prev_delete.start = undo_action.start
self.undo_stack.append(prev_delete)
else:
self.undo_stack.append(prev_delete)
self.undo_stack.append(undo_action)
def __begin_not_undoable_action(self):
"""Toggle to stop recording actions"""
self.not_undoable_action = True
def __end_not_undoable_action(self):
"""Toggle to start recording actions"""
self.not_undoable_action = False

View File

@ -11,11 +11,11 @@ class Theme:
The light variant is listed first, followed by the dark variant, if any.
"""
previous = None
settings = Settings.new()
def __init__(self, name, gtk_css_path, web_css_path, is_dark, inverse_name):
def __init__(self, name, web_css_path, is_dark, inverse_name):
self.name = name
self.gtk_css_path = gtk_css_path
self.web_css_path = web_css_path
self.is_dark = is_dark
self.inverse_name = inverse_name
@ -29,33 +29,39 @@ class Theme:
return current_theme
@classmethod
def get_current(cls):
def get_current_changed(cls):
theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name')
dark_mode_auto = cls.settings.get_value('dark-mode-auto').get_boolean()
dark_mode = cls.settings.get_value('dark-mode').get_boolean()
current_theme = cls.get_for_name(theme_name)
# Technically, we could very easily allow the user to force the light ui on a dark theme.
# However, as there is no inverse of "gtk-application-prefer-dark-theme", we shouldn't do that.
if dark_mode and not current_theme.is_dark and current_theme.inverse_name:
if not dark_mode_auto and dark_mode != current_theme.is_dark and current_theme.inverse_name:
current_theme = cls.get_for_name(current_theme.inverse_name, current_theme.name)
changed = current_theme != cls.previous
cls.previous = current_theme
return current_theme, changed
@classmethod
def get_current(cls):
current_theme, _ = cls.get_current_changed()
return current_theme
def __eq__(self, other):
return isinstance(other, self.__class__) and \
self.name == other.name and \
self.web_css_path == other.web_css_path and \
self.is_dark == other.is_dark and \
self.inverse_name == other.inverse_name
defaultThemes = [
# https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita
Theme('Adwaita', get_css_path('gtk_adwaita.css'),
get_css_path('web_adwaita.css'), False, 'Adwaita-dark'),
Theme('Adwaita-dark', get_css_path('gtk_adwaita_dark.css'),
get_css_path('web_adwaita_dark.css'), True, 'Adwaita'),
Theme('Adwaita', get_css_path('web/adwaita.css'), False, 'Adwaita-dark'),
Theme('Adwaita-dark', get_css_path('web/adwaita_dark.css'), True, 'Adwaita'),
# https://github.com/NicoHood/arc-theme/tree/master/common/gtk-3.0/3.20/sass
Theme('Arc', get_css_path('gtk_arc.css'),
get_css_path('web_arc.css'), False, 'Arc-Dark'),
Theme('Arc-Darker', get_css_path('gtk_arc_darker.css'),
get_css_path('web_arc_darker.css'), False, 'Arc-Dark'),
Theme('Arc-Dark', get_css_path('gtk_arc_dark.css'),
get_css_path('web_arc_dark.css'), True, 'Arc'),
Theme('Arc', get_css_path('web/arc.css'), False, 'Arc-Dark'),
Theme('Arc-Darker', get_css_path('web/arc_darker.css'), False, 'Arc-Dark'),
Theme('Arc-Dark', get_css_path('web/arc_dark.css'), True, 'Arc'),
# https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/HighContrast
Theme('HighContrast', get_css_path('gtk_high_contrast.css'),
get_css_path('web_high_contrast.css'), False, 'HighContrastInverse'),
Theme('HighContrastInverse', get_css_path('gtk_high_contrast_inverse.css'),
get_css_path('web_high_contrast_inverse.css'), True, 'HighContrast'),
Theme('HighContrast', get_css_path('web/highcontrast.css'), False, 'HighContrastInverse'),
Theme('HighContrastInverse', get_css_path('web/highcontrast_inverse.css'), True, 'HighContrast')
]

View File

@ -101,7 +101,6 @@ class PyGTKBrowser:
if options.delay:
print("--delay is only supported on Mac OS X (for now). Sorry!")
gobject.threads_init()
window = gtk.Window()
window.resize(int(options.initWidth),int(options.initHeight))
self.view = webkit.WebView()

File diff suppressed because it is too large Load Diff