Compare commits
305 Commits
ft.font-si
...
master
Author | SHA1 | Date |
---|---|---|
Manuel Genovés | 9bc8dcc19d | |
Manuel Genovés | dc028353fe | |
Manuel Genovés | 7a11f189c0 | |
Manuel Genovés | 1843bc9533 | |
Manuel Genovés | 8042e7d8b0 | |
Manuel Genovés | 57ac571160 | |
Manuel Genovés | 833e75b0ab | |
Manuel Genovés | 4ad1ec67d5 | |
Manuel Genovés | c1a134496f | |
Manuel Genovés | 8feca99cc2 | |
Manuel Genovés | bd741f2f64 | |
Manuel Genovés | 5877682ee8 | |
Manuel Genovés | 557faf66f2 | |
somas95 | dd2bc4a7dc | |
somas95 | db2d266f90 | |
somas95 | 74891c9192 | |
somas95 | 6f182dfaa0 | |
somas95 | 33eac1c16a | |
somas95 | 274c59f884 | |
somas95 | 490601728b | |
somas95 | 3f7c01c659 | |
somas95 | 9f020841c3 | |
somas95 | 564b609ee9 | |
somas95 | 4a62a1e03e | |
somas95 | f6d924769c | |
somas95 | cd51a53b2e | |
somas95 | 2a5e08b3e2 | |
somas95 | 7d0fe8d596 | |
somas95 | fb64e442dc | |
somas95 | 8c9c27919c | |
somas95 | ac0ebcebf2 | |
Manuel Genovés | 0123fc8630 | |
Manuel Genovés | 9bf34e5151 | |
somas95 | 775c0006e1 | |
Manuel Genovés | e70b6d52bc | |
Manuel Genovés | 29d2af97fa | |
Manuel Genovés | af2975d48a | |
Manuel Genovés | 30af9b2b20 | |
somas95 | e38520c35d | |
somas95 | 3cbd386cf4 | |
somas95 | c6dce3f2cb | |
somas95 | e6e8655c2a | |
Manuel Genovés | 5f3bbb9fd9 | |
Manuel Genovés | c9b4bdd110 | |
Manuel Genovés | 4a85f21d35 | |
Manuel Genoves | 7a8d3a8459 | |
Manuel Genovés | ef04a90fd7 | |
Manuel Genovés | b70416709c | |
Manuel Genovés | 5b1571293c | |
Manuel Genovés | a4d0d3038c | |
Manuel Genovés | 8358571c8e | |
Manuel Genovés | 48f32afa1b | |
Manuel Genoves | bdc2e6cda7 | |
Manuel Genovés | bcd08397f1 | |
Manuel Genovés | cab5306efa | |
Manuel Genovés | 45f4205939 | |
Manuel Genovés | 004c7544a0 | |
Manuel Genovés | 6ca2410432 | |
Manuel Genovés | a02c61ec7e | |
Manuel Genovés | bf1fe7f59c | |
Manuel Genovés | a6f7e85255 | |
Manuel Genovés | 77eb75c36b | |
Manuel Genovés | 81d5aab9e4 | |
Manuel Genovés | 28fc4e0a72 | |
Manuel Genovés | 2fabd9a500 | |
Manuel Genovés | d1640619f9 | |
Manuel Genovés | 8e45b8d3da | |
Manuel Genovés | c1cd347c49 | |
Manuel Genovés | 07378d0ca8 | |
Manuel Genovés | 9ece36b5ac | |
Manuel Genovés | a00298e0db | |
somas95 | b9f86d5c44 | |
Manuel Genovés | 68bcc1206b | |
Manuel Genovés | d7df0c68f7 | |
Manuel Genovés | 0647b41340 | |
Manuel Genovés | 41378d55e1 | |
somas95 | 66af5f8217 | |
Thomas Lavend'Homme | 01d124aea5 | |
Wolf Vollprecht | 0d1da19ce8 | |
Gurjus Bhasin | 33b2d70dfd | |
Manuel Genovés | 9d59118afd | |
Manuel Genovés | d59994f3c9 | |
somas95 | d246877a17 | |
Manuel Genovés | bde7c0ecb8 | |
somas95 | daa72b4779 | |
somas95 | 0af10a1b0f | |
Thomas Lavend'Homme | 5cae9eb68c | |
Thomas Lavend'Homme | e4b5952ec2 | |
Thomas Lavend'Homme | 7734e9410e | |
Thomas Lavend'Homme | 2abb2af472 | |
Manuel Genovés | 234eca06a7 | |
somas95 | cabbd8f7a3 | |
Manuel Genovés | 5615a4c3d7 | |
Manuel Genovés | a3a948e434 | |
Manuel Genovés | 2fec09999b | |
Manuel Genovés | 109e9efbcc | |
Manuel Genovés | d05d0c3bdc | |
Wolf Vollprecht | 0bcb2f3984 | |
Thomas Lavend'Homme | 6edf041169 | |
Thomas Lavend'Homme | 5f75998973 | |
Thomas Lavend'Homme | 0fce1bae77 | |
Manuel Genoves | 9216db1b80 | |
Manuel Genoves | d56623bfbd | |
Manuel Genoves | 5a78d75668 | |
somas95 | 2912baaa41 | |
Manuel Genovés | 97e809a576 | |
Manuel Genovés | 151809ae9b | |
Manuel Genovés | 48e48d95de | |
Thomas Lavend'Homme | bf73910483 | |
Thomas Lavend'Homme | 2caff3b389 | |
Thomas Lavend'Homme | 3062037eeb | |
Thomas Lavend'Homme | 11616c1621 | |
Manuel Genovés | 7606a55389 | |
Manuel Genovés | 17c20199f5 | |
Manuel Genovés | ff579b956f | |
Manuel Genovés | 87e3cc127b | |
Manuel Genovés | 567ae6bc42 | |
Manuel Genovés | 8ac8728e3b | |
Manuel Genovés | f766c3703d | |
Manuel Genovés | b5260e3906 | |
somas95 | 1fc36c6fdc | |
Manuel Genoves | f091e5ac1a | |
Manuel Genovés | 0e2f731ff4 | |
Manuel Genovés | 10f98f35b8 | |
Manuel Genovés | f4809faf19 | |
Manuel Genovés | 8f5dd56863 | |
somas95 | a9650d86bf | |
Manuel Genovés | 88f216161b | |
Manuel Genovés | 680ef98a75 | |
somas95 | 6a6b5f4c69 | |
Manuel Genovés | 13296024c8 | |
Manuel Genovés | 3af59e2c1d | |
somas95 | b7c9eafbdb | |
Manuel Genoves | 207f2e8f6c | |
Manuel Genoves | 3be5f1c7ea | |
Manuel Genoves | ca0b458ca1 | |
Manuel Genoves | 731f9cb470 | |
Manuel Genoves | 11bc9fc086 | |
Manuel Genoves | 4b2be6bf20 | |
Manuel Genoves | e39e515e6d | |
Manuel Genovés | 3d52aa8042 | |
Manuel Genovés | 36eb349b96 | |
Manuel Genovés | ded8effa21 | |
Manuel Genovés | 355fecef43 | |
Manuel Genovés | 379bb91619 | |
Manuel Genovés | 83299d2bd4 | |
Manuel Genovés | 8bbe3c9044 | |
somas95 | d75a14d0c5 | |
Gonçalo Silva | 28446c42d1 | |
Gonçalo Silva | ec2f33e248 | |
Gonçalo Silva | 3bb813895e | |
Gonçalo Silva | 128ce54761 | |
Gonçalo Silva | b4696cda30 | |
Gonçalo Silva | 05cdfe0599 | |
Manuel Genovés | 63ff2659fc | |
Manuel Genovés | 53a9f4ebbd | |
Manuel Genovés | 3cae19c0cc | |
Manuel Genovés | 3e661b8d9d | |
Manuel Genovés | 300c386631 | |
Manuel Genovés | 26077831fa | |
Gonçalo Silva | 7c3d4d9364 | |
Gonçalo Silva | 23cddba0d0 | |
Gonçalo Silva | 859ad84524 | |
Gonçalo Silva | 7ea8f67216 | |
Gonçalo Silva | bd2d78b86a | |
Gonçalo Silva | 0b6e84bf8c | |
Gonçalo Silva | aa3f5c3430 | |
Gonçalo Silva | e3b99e823b | |
Gonçalo Silva | d9014b12e7 | |
Gonçalo Silva | efb1a02f30 | |
Gonçalo Silva | ef4009fcd6 | |
Gonçalo Silva | 7c6d2c12a3 | |
Gonçalo Silva | 1cc2fc5a4c | |
Manuel Genovés | adcb73b129 | |
Manuel Genovés | 0bdb9e54ec | |
Manuel Genovés | c8ea808623 | |
Gonçalo Silva | cb3da0331e | |
Gonçalo Silva | ab383db98a | |
Gonçalo Silva | 55e5cd3856 | |
Gonçalo Silva | 3fa56afaef | |
Gonçalo Silva | 55d82856c2 | |
Gonçalo Silva | 931d92bdfd | |
Gonçalo Silva | bb279d0379 | |
Gonçalo Silva | 0d87299040 | |
Gonçalo Silva | 3f4f8292ca | |
Gonçalo Silva | aae38ddb5f | |
Gonçalo Silva | d78602c4db | |
Gonçalo Silva | df79f9329e | |
Gonçalo Silva | 0b13fdddc5 | |
Gonçalo Silva | eec633437b | |
Gonçalo Silva | 8e97b7ae2c | |
somas95 | ada47cc89a | |
somas95 | edc4cad961 | |
somas95 | 6340c15c85 | |
somas95 | 47116eaf8c | |
Manuel Genovés | 213ff104e5 | |
Bilal Elmoussaoui | 5f0d9570cf | |
Bilal Elmoussaoui | 16b5e8821f | |
somas95 | 68af5b3161 | |
somas95 | 09e4b91b42 | |
Gonçalo Silva | 828f4b0bf1 | |
Manuel Genovés | e63f9b4b72 | |
Manuel Genovés | 335cdf8e09 | |
Manuel Genovés | c4b00f1014 | |
Manuel Genovés | 7e0de3d4d1 | |
somas95 | 82a95492ee | |
somas95 | 76aef04369 | |
somas95 | 748f473ac0 | |
somas95 | dd23cbb426 | |
somas95 | 266279e883 | |
somas95 | 9075fad5e4 | |
somas95 | abe3813d34 | |
somas95 | 6c41f8a5fd | |
somas95 | 661e311059 | |
somas95 | 41ed2c5319 | |
somas95 | 7c3f92b82b | |
somas95 | ff8bebcd8b | |
somas95 | e2544ceebf | |
somas95 | bced0277e0 | |
somas95 | de6d25cbe8 | |
somas95 | 53a5593f72 | |
somas95 | da6550cbee | |
somas95 | 26ad6c087f | |
Manuel Genovés | f0f2f05670 | |
somas95 | 3d2fd337d6 | |
Gonçalo Silva | 832b3e3d38 | |
Gonçalo Silva | 21387ea6a7 | |
Manuel Genovés | b55f4dbecd | |
Manuel Genovés | 7e34e9cc62 | |
Manuel Genovés | 6b4fcabb57 | |
somas95 | a0638f6912 | |
somas95 | 6206e58c4f | |
Gonçalo Silva | c5abc531e0 | |
Gonçalo Silva | e39694571d | |
Manuel Genovés | bc24251461 | |
Gonçalo Silva | 16382d9574 | |
Gonçalo Silva | 250519f319 | |
Gonçalo Silva | c2d0bde9f8 | |
Gonçalo Silva | 939edcc762 | |
Gonçalo Silva | db652ef84f | |
Gonçalo Silva | a0a19ffbe7 | |
Gonçalo Silva | c2a43c374a | |
Gonçalo Silva | e80b61cf9d | |
Gonçalo Silva | 7a2e6d5d8f | |
Gonçalo Silva | 7ff1df4371 | |
Gonçalo Silva | e533bf190d | |
Gonçalo Silva | ebbbd73056 | |
Gonçalo Silva | 8ae2dfcb0b | |
Gonçalo Silva | 86cffc40ec | |
Gonçalo Silva | dc0652e3ed | |
Gonçalo Silva | 65e7028843 | |
Gonçalo Silva | f72f61ae7d | |
Gonçalo Silva | 2cb161307c | |
Gonçalo Silva | 5e770510ee | |
Gonçalo Silva | bc23fa9b0b | |
Gonçalo Silva | 562cc7e200 | |
Gonçalo Silva | 63b20d0f3c | |
somas95 | b9e2720eed | |
Manuel Genovés | 6c50e53c9d | |
Manuel Genovés | 249e78c746 | |
Gonçalo Silva | 241ba567e4 | |
Gonçalo Silva | 9238a82d4d | |
Gonçalo Silva | 3bbbdf95e1 | |
Manuel Genovés | cd6e5a86aa | |
somas95 | 5deae402b7 | |
somas95 | dc80c6763a | |
Christopher Davis | e0cea3654a | |
Christopher Davis | 15c69190d8 | |
somas95 | 181af445e6 | |
Gonçalo Silva | 878bbdb67c | |
Gonçalo Silva | ddcf76df47 | |
Gonçalo Silva | 81f9104d9f | |
Gonçalo Silva | e87de1424e | |
Manuel Genovés | 9bf30143d1 | |
Manuel Genovés | 3d26dc2b25 | |
somas95 | c2b5116a46 | |
somas95 | f2d00f2f0d | |
Ryan Gonzalez | 32bb70a261 | |
Manuel Genovés | f466171eda | |
Manuel Genovés | dc4c4d9c2c | |
Manuel Genovés | 8e5ccfc01d | |
somas95 | 2b5140dada | |
Gonçalo Silva | dccc645430 | |
Gonçalo Silva | f6c62fb459 | |
Gonçalo Silva | c19f57f64b | |
Gonçalo Silva | e76b85e837 | |
Gonçalo Silva | 1a7443fd3c | |
Gonçalo Silva | dfe7cc420e | |
Manuel Genovés | ec3dc6d632 | |
Manuel Genovés | 21e27eaaf0 | |
Manuel Genovés | d3c9b81fe8 | |
somas95 | d99e4c65f9 | |
Manuel Genovés | f67eaedcb6 | |
Manuel Genovés | 6baa398995 | |
Manuel Genovés | 83c15361ea | |
Manuel Genovés | 96865fbefc | |
Manuel Genovés | 9ae1eaedda | |
Manuel Genovés | 844890507c | |
Manuel Genovés | c629137ed1 | |
Manuel Genovés | 97b4963fa7 | |
somas95 | e2cb547648 | |
somas95 | 37a8eb1cd1 | |
Manuel Genovés | 3bb9e0b575 | |
Manuel Genovés | 5bf0d877d3 | |
Manuel Genovés | aa4a878dbc |
|
@ -1,23 +1,11 @@
|
|||
build/lib.linux-x86_64-2.7
|
||||
*.pyc
|
||||
__pycache__/
|
||||
_build/*
|
||||
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
|
||||
*.py~
|
||||
data/ui/shortcut_handlers
|
||||
*.ui~
|
||||
*.*~
|
||||
.vscode/
|
||||
*.glade~
|
||||
dist/uberwriter-2.0b0-py3.7.egg
|
||||
.idea/
|
||||
builddir/*
|
||||
build-aux/flatpak/_build/*
|
||||
build-aux/flatpak/.flatpak-builder/*
|
||||
flatpak/*
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment(please complete the following information):**
|
||||
- Linux distribution:
|
||||
- Desktop Enviroment:
|
||||
- DE version:
|
||||
- GTK version:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
/label ~bug ~triage
|
||||
/assign @somas
|
|
@ -0,0 +1,14 @@
|
|||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
/label ~"feature request"
|
||||
/assign @somas
|
7
AUTHORS
|
@ -1,2 +1,5 @@
|
|||
Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
Copyright (C) 2012, Vova Kolobok <vovkkk@ya.ru>
|
||||
Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
Vova Kolobok <vovkkk@ya.ru>
|
||||
Manuel Genovés <manuel.genoves@gmail.com>
|
||||
Gonçalo Silva <goncalossilva@gmail.com>
|
||||
Thomas Lavend <lavendthomas@outlook.be>
|
|
@ -0,0 +1,76 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at manuel.genoves@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
|
@ -1,7 +0,0 @@
|
|||
### ENVIROMENT
|
||||
- Linux distribution:
|
||||
- Desktop Enviroment:
|
||||
- DE version:
|
||||
- GTK version:
|
||||
|
||||
### BUG
|
6
Makefile
|
@ -1,6 +0,0 @@
|
|||
all:
|
||||
python3 ./setup.py build
|
||||
|
||||
install:
|
||||
python3 ./setup.py install --prefix=/app --skip-build --optimize=1
|
||||
|
10
PKGBUILD
|
@ -1,17 +1,17 @@
|
|||
pkgname=uberwriter
|
||||
_pkgname=uberwriter
|
||||
pkgname=apostrophe
|
||||
_pkgname=apostrophe
|
||||
pkgver=2.1.3
|
||||
pkgrel=1
|
||||
pkgdesc='A distraction free Markdown editor for GNU/Linux made with GTK+'
|
||||
arch=('any')
|
||||
url='http://uberwriter.github.io/uberwriter/'
|
||||
url='http://apostrophe.github.io/apostrophe/'
|
||||
license=('GPL3')
|
||||
depends=('gtk3' 'pandoc' 'python-gtkspellcheck')
|
||||
depends=('gtk3' 'pandoc' 'gspell')
|
||||
makedepends=('python-setuptools')
|
||||
optdepends=('texlive-core' 'otf-fira-mono: Recommended font')
|
||||
provides=("$_pkgname")
|
||||
conflicts=("$_pkgname")
|
||||
source=('git+https://github.com/UberWriter/uberwriter.git#branch=refactoring')
|
||||
source=('git+https://github.com/Apostrophe/apostrophe.git#branch=refactoring')
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
|
|
64
README.md
|
@ -1,25 +1,33 @@
|
|||
Uberwriter
|
||||
==========
|
||||
[![Please do not theme this app](https://stopthemingmy.app/badge.svg)](https://stopthemingmy.app)
|
||||
|
||||
# Apostrophe
|
||||
|
||||
![](screenshots/main.png)
|
||||
|
||||
# About
|
||||
## About
|
||||
|
||||
Uberwriter is a GTK+ based distraction free Markdown editor, mainly developed by Wolf Vollprecht. It uses pandoc as backend for markdown parsing and offers a very clean and sleek user interface.
|
||||
Apostrophe is a GTK+ based distraction free Markdown editor, mainly developed by Wolf Vollprecht and Manuel Genovés. It uses pandoc as backend for markdown parsing and offers a very clean and sleek user interface.
|
||||
|
||||
# Install
|
||||
## Install
|
||||
|
||||
You can get now UberWriter on Flathub!
|
||||
You can get Apostrophe on Flathub!
|
||||
[Get it now](https://flathub.org/apps/details/de.wolfvollprecht.UberWriter)
|
||||
|
||||
# Contributions and localization
|
||||
## Contributions and localization
|
||||
|
||||
If you want to help to localize the project, just join us at [Poeditor](https://poeditor.com/join/project/gxVzFyXb2x)
|
||||
Any help is appreciated!
|
||||
|
||||
# Running and building it
|
||||
## Building from Git
|
||||
|
||||
To use uberwriter, please make sure you have some dependencies installed:
|
||||
```bash
|
||||
$ git clone https://github.com/Apostrophe/apostrophe.git
|
||||
$ cd apostrophe
|
||||
$ meson builddir --prefix=/usr
|
||||
# sudo ninja -C builddir install
|
||||
```
|
||||
|
||||
To use apostrophe, please make sure you have some dependencies installed:
|
||||
|
||||
- Pandoc, the program used to convert Markdown to basically anything else (the package name should be pandoc in most distributions)
|
||||
- Of course, gtk3 etc. needs to be installed as well since this is a gtk application
|
||||
|
@ -27,31 +35,21 @@ To use uberwriter, please make sure you have some dependencies installed:
|
|||
- Please find these packages on your distribution: `python3 python3-regex python3-setuptools python3-levenshtein python3-enchant python3-gi python3-cairo`
|
||||
- Optional dependencies are `texlive` for the pdftex module.
|
||||
|
||||
You can run UberWriter with `./bin/uberwriter` without installing it in the system,
|
||||
### Running it without installing it
|
||||
|
||||
You can run Apostrophe with `./apostrophe.in` without installing it in the system,
|
||||
but you'll need to install and compile the schemas before:
|
||||
`sudo cp data/de.wolfvollprecht.UberWriter.gschema.xml /usr/share/glib-2.0/schemas/de.wolfvollprecht.UberWriter.gschema.xml`
|
||||
`sudo glib-compile-schemas /usr/share/glib-2.0/schemas`
|
||||
|
||||
```bash
|
||||
# sudo cp data/de.wolfvollprecht.UberWriter.gschema.xml /usr/share/glib-2.0/schemas/de.wolfvollprecht.UberWriter.gschema.xml
|
||||
# sudo glib-compile-schemas /usr/share/glib-2.0/schemas
|
||||
```
|
||||
|
||||
### Building a flatpak package
|
||||
|
||||
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)
|
||||
- `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:
|
||||
|
||||
- `cd flatpak`
|
||||
- `flatpak-builder --repo=org.foo.Uberwriter --force-clean build uberwriter.json`
|
||||
- `flatpak remote-add --no-gpg-verify user org.foo.Uberwriter`
|
||||
- `flatpak install foo de.wolfvollprecht.UberWriter`
|
||||
|
||||
Where `org.foo.repo` is the name of your repo, you can change 'foo' with the name you want
|
||||
Then you can run it as before or from your system launcher.
|
||||
|
||||
If you want to update an existing installation, just run
|
||||
|
||||
- `flatpak update de.wolfvollprecht.UberWriter`
|
||||
|
||||
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`
|
||||
```bash
|
||||
$ cd build-aux/flatpak
|
||||
$ flatpak-builder --force-clean --install --user _build de.wolfvollprecht.UberWriter.json
|
||||
```
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -25,35 +25,39 @@ import pkg_resources
|
|||
import gettext
|
||||
import locale
|
||||
|
||||
from gi.repository import Gio
|
||||
|
||||
# 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 :)
|
||||
# Set the path if needed. This allows apostrophe 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'))
|
||||
|
||||
if (os.path.exists(os.path.join(PROJECT_ROOT_DIRECTORY, 'apostrophe'))
|
||||
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 apostrophe
|
||||
|
||||
import uberwriter
|
||||
|
||||
locale_dir = os.path.abspath(os.path.join(os.path.dirname(uberwriter.__file__),'../po/'))
|
||||
localedir = '@LOCALE_DIR@'
|
||||
pkgdatadir = '@DATA_DIR@'
|
||||
|
||||
|
||||
#locale_dir = os.path.abspath(os.path.join(os.path.dirname(apostrophe.__file__),'../po/'))
|
||||
|
||||
# L10n
|
||||
locale.textdomain('uberwriter')
|
||||
locale.bindtextdomain('uberwriter', locale_dir)
|
||||
gettext.textdomain('uberwriter')
|
||||
gettext.bindtextdomain('uberwriter', locale_dir)
|
||||
locale.textdomain('apostrophe')
|
||||
locale.bindtextdomain('apostrophe', localedir)
|
||||
gettext.textdomain('apostrophe')
|
||||
gettext.bindtextdomain('apostrophe', localedir)
|
||||
|
||||
uberwriter.main()
|
||||
resource = Gio.resource_load(os.path.join(pkgdatadir, 'apostrophe/apostrophe.gresource'))
|
||||
Gio.Resource._register(resource)
|
||||
|
||||
|
||||
apostrophe.main()
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -19,10 +19,10 @@ import gi
|
|||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from uberwriter import window
|
||||
from uberwriter import application
|
||||
from uberwriter.helpers import set_up_logging
|
||||
from uberwriter.config import get_version
|
||||
from apostrophe import main_window
|
||||
from apostrophe import application
|
||||
from apostrophe.helpers import set_up_logging
|
||||
from apostrophe.config import get_version
|
||||
|
||||
|
||||
def main():
|
|
@ -15,14 +15,16 @@ from gettext import gettext as _
|
|||
|
||||
import gi
|
||||
|
||||
from apostrophe.main_window import MainWindow
|
||||
|
||||
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import GLib, Gio, Gtk, GdkPixbuf
|
||||
|
||||
from uberwriter import window
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.helpers import set_up_logging
|
||||
from uberwriter.preferences_dialog import PreferencesDialog
|
||||
from uberwriter.helpers import get_builder, get_media_path
|
||||
from apostrophe import main_window
|
||||
from apostrophe.settings import Settings
|
||||
from apostrophe.helpers import set_up_logging
|
||||
from apostrophe.preferences_dialog import PreferencesDialog
|
||||
from apostrophe.helpers import get_media_path
|
||||
|
||||
|
||||
class Application(Gtk.Application):
|
||||
|
@ -39,6 +41,9 @@ class Application(Gtk.Application):
|
|||
Gtk.Application.do_startup(self)
|
||||
|
||||
self.settings.connect("changed", self.on_settings_changed)
|
||||
self._set_dark_mode ()
|
||||
|
||||
# Header bar
|
||||
|
||||
action = Gio.SimpleAction.new("new", None)
|
||||
action.connect("activate", self.on_new)
|
||||
|
@ -60,6 +65,7 @@ class Application(Gtk.Application):
|
|||
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)
|
||||
|
@ -84,7 +90,7 @@ class Application(Gtk.Application):
|
|||
action.connect("activate", self.on_save_as)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("export", None)
|
||||
action = Gio.SimpleAction.new("export", GLib.VariantType("s"))
|
||||
action.connect("activate", self.on_export)
|
||||
self.add_action(action)
|
||||
|
||||
|
@ -92,6 +98,10 @@ class Application(Gtk.Application):
|
|||
action.connect("activate", self.on_copy_html)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("search_replace", None)
|
||||
action.connect("activate", self.on_search_replace)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("preferences", None)
|
||||
action.connect("activate", self.on_preferences)
|
||||
self.add_action(action)
|
||||
|
@ -112,15 +122,32 @@ class Application(Gtk.Application):
|
|||
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)
|
||||
|
||||
# Preview Menu
|
||||
|
||||
preview_mode = self.settings.get_string("preview-mode")
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"preview_mode", GLib.VariantType.new("s"), GLib.Variant.new_string(preview_mode))
|
||||
action.connect("activate", self.on_preview_mode)
|
||||
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"])
|
||||
self.set_accels_for_action("app.fullscreen", ["F11"])
|
||||
self.set_accels_for_action("app.preview", ["<Ctl>p"])
|
||||
self.set_accels_for_action("app.search", ["<Ctl>f"])
|
||||
self.set_accels_for_action("app.search_replace", ["<Ctl>h"])
|
||||
self.set_accels_for_action("app.spellcheck", ["F7"])
|
||||
|
||||
self.set_accels_for_action("app.new", ["<Ctl>n"])
|
||||
|
@ -134,8 +161,8 @@ class Application(Gtk.Application):
|
|||
if not self.window:
|
||||
# Windows are associated with the application
|
||||
# when the last one is closed the application shuts down
|
||||
# self.window = Window(application=self, title="UberWriter")
|
||||
self.window = window.Window(self)
|
||||
# self.window = Window(application=self, title="Apostrophe")
|
||||
self.window = MainWindow(self)
|
||||
if self.args:
|
||||
self.window.load_file(self.args[0])
|
||||
|
||||
|
@ -146,7 +173,7 @@ class Application(Gtk.Application):
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", dest="verbose",
|
||||
help=_("Show debug messages (-vv debugs uberwriter also)"))
|
||||
help=_("Show debug messages (-vv debugs apostrophe also)"))
|
||||
parser.add_argument(
|
||||
"-e", "--experimental-features", help=_("Use experimental features"),
|
||||
action='store_true')
|
||||
|
@ -157,15 +184,30 @@ class Application(Gtk.Application):
|
|||
self.activate()
|
||||
return 0
|
||||
|
||||
def _set_dark_mode (self):
|
||||
dark = self.settings.get_value("dark-mode")
|
||||
settings = Gtk.Settings.get_default()
|
||||
|
||||
settings.props.gtk_application_prefer_dark_theme = dark
|
||||
|
||||
if settings.props.gtk_theme_name == "HighContrast" and dark:
|
||||
settings.props.gtk_theme_name = "HighContrastInverse"
|
||||
elif settings.props.gtk_theme_name == "HighContrastInverse" and not dark:
|
||||
settings.props.gtk_theme_name = "HighContrast"
|
||||
|
||||
def on_settings_changed(self, settings, key):
|
||||
if key == "dark-mode-auto" or key == "dark-mode":
|
||||
self.window.apply_current_theme()
|
||||
if key == "dark-mode":
|
||||
self._set_dark_mode ()
|
||||
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 == "sync-scroll":
|
||||
self.window.reload_preview(reshow=True)
|
||||
elif key == "stat-default":
|
||||
self.window.update_default_stat()
|
||||
elif key == "preview-mode":
|
||||
self.window.update_preview_mode()
|
||||
|
||||
def on_new(self, _action, _value):
|
||||
self.window.new_document()
|
||||
|
@ -180,7 +222,10 @@ class Application(Gtk.Application):
|
|||
self.window.save_document()
|
||||
|
||||
def on_search(self, _action, _value):
|
||||
self.window.open_search_and_replace()
|
||||
self.window.open_search()
|
||||
|
||||
def on_search_replace(self, _action, _value):
|
||||
self.window.open_search(replace=True)
|
||||
|
||||
def on_focus_mode(self, action, value):
|
||||
action.set_state(value)
|
||||
|
@ -201,8 +246,8 @@ class Application(Gtk.Application):
|
|||
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_export(self, _action, value):
|
||||
self.window.open_advanced_export(value.get_string())
|
||||
|
||||
def on_copy_html(self, _action, _value):
|
||||
self.window.copy_html_to_clipboard()
|
||||
|
@ -211,27 +256,34 @@ class Application(Gtk.Application):
|
|||
PreferencesDialog(self.settings).show(self.window)
|
||||
|
||||
def on_shortcuts(self, _action, _param):
|
||||
builder = get_builder('Shortcuts')
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Shortcuts.ui")
|
||||
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()
|
||||
self.window.open_apostrophe_markdown()
|
||||
|
||||
def on_about(self, _action, _param):
|
||||
builder = get_builder('About')
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_resource("/de/wolfvollprecht/UberWriter/About.ui")
|
||||
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())
|
||||
|
||||
def on_preview_mode(self, action, value):
|
||||
action.set_state(value)
|
||||
self.settings.set_string("preview-mode", value.get_string())
|
||||
|
||||
# ~ if __name__ == "__main__":
|
||||
# ~ app = Application()
|
||||
# ~ app.run(sys.argv)
|
|
@ -1,7 +1,7 @@
|
|||
# UberwriterAutoCorrect
|
||||
# The Uberwriter Auto Correct is a auto correction
|
||||
# ApostropheAutoCorrect
|
||||
# The Apostrophe Auto Correct is a auto correction
|
||||
# mechanism to prevent stupid typos
|
||||
# import presage
|
||||
# CURRENTLY DISABLED
|
||||
|
||||
import os
|
||||
import pickle
|
||||
|
@ -13,16 +13,16 @@ from gi.repository import Gtk, Gdk
|
|||
|
||||
import enchant
|
||||
|
||||
from uberwriter import pressagio
|
||||
# import uberwriter.pressagio.predictor
|
||||
# import uberwriter.pressagio.tokenizer
|
||||
# import uberwriter.pressagio.dbconnector
|
||||
# import uberwriter.pressagio.context_tracker
|
||||
# import uberwriter.pressagio.callback
|
||||
from apostrophe import pressagio
|
||||
# import apostrophe.pressagio.predictor
|
||||
# import apostrophe.pressagio.tokenizer
|
||||
# import apostrophe.pressagio.dbconnector
|
||||
# import apostrophe.pressagio.context_tracker
|
||||
# import apostrophe.pressagio.callback
|
||||
|
||||
# from Levenshtein import distance
|
||||
|
||||
from uberwriter.helpers import get_media_path
|
||||
from apostrophe.helpers import get_media_path
|
||||
|
||||
# Define and create PresageCallback object
|
||||
class PressagioCallback(pressagio.callback.Callback):
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -28,7 +28,7 @@ import gi
|
|||
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import GObject, Gtk # pylint: disable=E0611
|
||||
|
||||
LOGGER = logging.getLogger('uberwriter')
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
# this module is big so uses some conventional prefixes and postfixes
|
||||
# *s list, except self.widgets is a dictionary
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -24,7 +24,7 @@ __all__ = [
|
|||
|
||||
# Where your project will look for your data (for instance, images and ui
|
||||
# files). By default, this is ../data, relative your trunk layout
|
||||
__uberwriter_data_directory__ = '../data/'
|
||||
__apostrophe_data_directory__ = '../data/'
|
||||
__license__ = 'GPL-3'
|
||||
__version__ = 'VERSION'
|
||||
|
||||
|
@ -45,26 +45,25 @@ def get_data_file(*path_segments):
|
|||
|
||||
|
||||
def get_data_path():
|
||||
"""Retrieve uberwriter data path
|
||||
"""Retrieve apostrophe data path
|
||||
|
||||
This path is by default <uberwriter_path>/../data/ in trunk
|
||||
and /opt/uberwriter/data in an installed version but this path
|
||||
This path is by default <apostrophe_path>/../data/ in trunk
|
||||
and /opt/apostrophe/data in an installed version but this path
|
||||
is specified at installation time.
|
||||
"""
|
||||
|
||||
# Get pathname absolute or relative.
|
||||
# TODO: Abstract this (the old env IN_FLATPAK)
|
||||
if os.path.isfile("/.flatpak-info"):
|
||||
return '/app/share/uberwriter/data/'
|
||||
return '/app/share/apostrophe/'
|
||||
|
||||
path = os.path.join(
|
||||
os.path.dirname(__file__), __uberwriter_data_directory__)
|
||||
os.path.dirname(__file__), __apostrophe_data_directory__)
|
||||
|
||||
# We try first if the data exists in the local folder and then
|
||||
# in the system installation path
|
||||
abs_data_path = os.path.abspath(path)
|
||||
if not os.path.exists(abs_data_path):
|
||||
abs_data_path = '/usr/share/uberwriter/data/'
|
||||
abs_data_path = '/usr/share/apostrophe/'
|
||||
elif not os.path.exists(abs_data_path):
|
||||
raise ProjectPathNotFound
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -24,13 +24,12 @@ from gettext import gettext as _
|
|||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
from uberwriter import helpers
|
||||
from uberwriter.theme import Theme
|
||||
from uberwriter.helpers import get_builder
|
||||
from apostrophe import helpers
|
||||
from apostrophe.theme import Theme
|
||||
|
||||
LOGGER = logging.getLogger('uberwriter')
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
|
||||
class Export:
|
||||
|
@ -122,7 +121,7 @@ class Export:
|
|||
"to": "texinfo"
|
||||
},
|
||||
{
|
||||
"name": "OpenOffice Text Document",
|
||||
"name": "LibreOffice Text Document",
|
||||
"ext": "odt",
|
||||
"to": "odt"
|
||||
},
|
||||
|
@ -148,39 +147,102 @@ class Export:
|
|||
}
|
||||
]
|
||||
|
||||
def __init__(self, filename):
|
||||
"""Set up the about dialog"""
|
||||
self.builder = get_builder('Export')
|
||||
self.dialog = self.builder.get_object("Export")
|
||||
self.stack = self.builder.get_object("export_stack")
|
||||
self.stack_switcher = self.builder.get_object("format_switcher")
|
||||
def __init__(self, filename, export_format, text):
|
||||
"""Set up the export dialog"""
|
||||
|
||||
stack_pdf_disabled = self.builder.get_object("pdf_disabled")
|
||||
filename = filename or _("Untitled document.md")
|
||||
self.export_menu = {
|
||||
"pdf":
|
||||
{
|
||||
"extension": "pdf",
|
||||
"name": "PDF",
|
||||
"mimetype": "application/pdf",
|
||||
"dialog": self.regular_export_dialog
|
||||
},
|
||||
"html":
|
||||
{
|
||||
"extension": "html",
|
||||
"name": "HTML",
|
||||
"mimetype": "text/html",
|
||||
"dialog": self.regular_export_dialog
|
||||
},
|
||||
"odt":
|
||||
{
|
||||
"extension": "odt",
|
||||
"name": "ODT",
|
||||
"mimetype": "application/vnd.oasis.opendocument.text",
|
||||
"dialog": self.regular_export_dialog
|
||||
},
|
||||
"advanced":
|
||||
{
|
||||
"extension": "",
|
||||
"name": "",
|
||||
"mimetype": "",
|
||||
"dialog": self.advanced_export_dialog
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if export_format == "advanced":
|
||||
self.adv_export_name = self.builder.get_object("advanced_export_name")
|
||||
self.adv_export_name.set_text(os.path.basename(filename)[:-3])
|
||||
else:
|
||||
filechooser.set_current_name(os.path.basename(filename)[:-2] + export_format)
|
||||
self.filename = filename or _("Untitled document.md")
|
||||
self.export_format = export_format
|
||||
|
||||
# Disable pdf if Texlive not installed
|
||||
self.dialog = self.export_menu[export_format]["dialog"]()
|
||||
|
||||
response = self.dialog.run()
|
||||
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
try:
|
||||
self.export(export_format, text)
|
||||
except (NotADirectoryError, RuntimeError) as e:
|
||||
dialog = Gtk.MessageDialog(None,
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.ERROR,
|
||||
Gtk.ButtonsType.CLOSE,
|
||||
_("An error happened while trying to export:\n\n{err_msg}")
|
||||
.format(err_msg= str(e).encode().decode("unicode-escape"))
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
self.dialog.destroy()
|
||||
|
||||
def regular_export_dialog(self):
|
||||
texlive_installed = helpers.exist_executable("pdftex")
|
||||
|
||||
if not texlive_installed:
|
||||
self.filechoosers["pdf"].set_visible(False)
|
||||
stack_pdf_disabled.set_visible(True)
|
||||
stack_pdf_disabled.set_text(disabled_text())
|
||||
stack_pdf_disabled.set_justify(Gtk.Justification.CENTER)
|
||||
self.stack.connect('notify', self.allow_export, 'visible_child_name')
|
||||
if (self.export_menu[self.export_format]["extension"] == "pdf" and
|
||||
not texlive_installed):
|
||||
dialog = Gtk.MessageDialog(None,
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.INFO,
|
||||
Gtk.ButtonsType.CLOSE,
|
||||
_("Oh, no!")
|
||||
)
|
||||
|
||||
dialog.props.secondary_text = _("Seems that you don't have TexLive installed.\n" +
|
||||
disabled_text())
|
||||
else:
|
||||
dialog = Gtk.FileChooserNative.new(_("Export"),
|
||||
None,
|
||||
Gtk.FileChooserAction.SAVE,
|
||||
_("Export to %s") %
|
||||
self.export_menu[self.export_format]["extension"],
|
||||
_("Cancel"))
|
||||
dialog_filter = Gtk.FileFilter.new()
|
||||
dialog_filter.set_name(self.export_menu[self.export_format]["name"])
|
||||
dialog_filter.add_mime_type(self.export_menu[self.export_format]["mimetype"])
|
||||
dialog.add_filter(dialog_filter)
|
||||
dialog.set_do_overwrite_confirmation(True)
|
||||
dialog.set_current_folder(os.path.dirname(self.filename))
|
||||
dialog.set_current_name(os.path.basename(self.filename)[:-2] +
|
||||
self.export_menu[self.export_format]["extension"])
|
||||
|
||||
return dialog
|
||||
|
||||
def advanced_export_dialog(self):
|
||||
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Export.ui")
|
||||
|
||||
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))
|
||||
|
||||
|
@ -195,7 +257,15 @@ class Export:
|
|||
self.format_field.add_attribute(format_renderer, "text", 1)
|
||||
self.format_field.set_active(0)
|
||||
|
||||
def export(self, text=""):
|
||||
self.adv_export_folder = self.builder.get_object("advanced")
|
||||
|
||||
self.adv_export_name = self.builder.get_object("advanced_export_name")
|
||||
self.adv_export_name.set_text(os.path.basename(self.filename)[:-3])
|
||||
self.paper_size = self.builder.get_object("combobox_paper_size")
|
||||
|
||||
return self.builder.get_object("Export")
|
||||
|
||||
def export(self, export_type, text=""):
|
||||
"""Export the given text using the specified format.
|
||||
For advanced export, this includes special flags for the enabled options.
|
||||
|
||||
|
@ -203,11 +273,16 @@ class Export:
|
|||
text {str} -- Text to export (default: {""})
|
||||
"""
|
||||
|
||||
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())
|
||||
|
||||
# TODO: use walrust operator
|
||||
output_uri = self.adv_export_folder.get_uri()
|
||||
if output_uri:
|
||||
output_dir = GLib.filename_from_uri(output_uri)[0]
|
||||
else:
|
||||
raise NotADirectoryError(_("A folder must be selected before proceeding"))
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
fmt = self.formats[self.format_field.get_active()]
|
||||
|
@ -219,10 +294,13 @@ class Export:
|
|||
if self.builder.get_object("smart").get_active():
|
||||
to += "+smart"
|
||||
|
||||
args.extend(self.get_advanced_arguments())
|
||||
args.extend(self.get_advanced_arguments(to, ext))
|
||||
|
||||
else:
|
||||
filename = self.filechoosers[export_type].get_filename()
|
||||
args = [
|
||||
"--variable=papersize:a4"
|
||||
]
|
||||
filename = self.dialog.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))
|
||||
|
@ -233,17 +311,18 @@ class Export:
|
|||
|
||||
if export_type == "html":
|
||||
to = "html5"
|
||||
args.append("--standalone")
|
||||
args.append("--self-contained")
|
||||
args.append("--css=%s" % Theme.get_current().web_css_path)
|
||||
args.append("--mathjax")
|
||||
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'))
|
||||
|
||||
|
||||
helpers.pandoc_convert(
|
||||
text, to=to, args=args,
|
||||
outputfile="%s/%s.%s" % (output_dir, basename, ext))
|
||||
|
||||
def get_advanced_arguments(self):
|
||||
def get_advanced_arguments(self, to_fmt, ext_fmt):
|
||||
"""Retrieve a list of the selected advanced arguments
|
||||
|
||||
For most of the advanced option checkboxes, returns a list
|
||||
|
@ -251,6 +330,8 @@ class Export:
|
|||
|
||||
Arguments:
|
||||
basename {str} -- the name of the file
|
||||
to_fmt {str} -- the format of the export
|
||||
ext_fmt {str} -- the extension of the export
|
||||
|
||||
Returns:
|
||||
list of {str} -- related pandoc flags
|
||||
|
@ -259,6 +340,16 @@ class Export:
|
|||
highlight_style = self.builder.get_object("highlight_style").get_active_text()
|
||||
|
||||
conditions = [
|
||||
{
|
||||
"condition": to_fmt == "pdf",
|
||||
"yes": "--variable=papersize:" + self.get_paper_size(),
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": (self.get_paper_size() == "a4" and (to_fmt in ("odt", "docx"))),
|
||||
"yes": "--reference-doc=" + helpers.get_reference_files_path('reference-a4.' + to_fmt),
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("toc").get_active(),
|
||||
"yes": "--toc",
|
||||
|
@ -316,18 +407,15 @@ class Export:
|
|||
|
||||
return args
|
||||
|
||||
def allow_export(self, widget, data, signal):
|
||||
"""Disable export button if the visible child is "pdf_disabled"
|
||||
"""
|
||||
def get_paper_size(self):
|
||||
paper_size = self.paper_size.get_active_text()
|
||||
|
||||
del widget, data, signal
|
||||
paper_formats = {
|
||||
"A4": "a4",
|
||||
"US Letter": "letter"
|
||||
}
|
||||
|
||||
export_btn = self.builder.get_object("export_btn")
|
||||
|
||||
if self.stack.get_visible_child_name() == "pdf_disabled":
|
||||
export_btn.set_sensitive(False)
|
||||
else:
|
||||
export_btn.set_sensitive(True)
|
||||
return paper_formats[paper_size]
|
||||
|
||||
def disabled_text():
|
||||
"""Return the TexLive installation instructions
|
|
@ -4,7 +4,7 @@ gi.require_version('Gtk', '3.0')
|
|||
from gi.repository import Gtk
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('uberwriter')
|
||||
logger = logging.getLogger('apostrophe')
|
||||
|
||||
class FixTable():
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# END LICENSE
|
||||
"""Manage all the headerbars related stuff
|
||||
"""
|
||||
|
||||
import gi
|
||||
|
||||
from gettext import gettext as _
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
from apostrophe.helpers import get_descendant
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
|
||||
class BaseHeaderbar:
|
||||
"""Base class for all headerbars
|
||||
"""
|
||||
# preview modes
|
||||
FULL_WIDTH = 0
|
||||
HALF_WIDTH = 1
|
||||
HALF_HEIGHT = 2
|
||||
WINDOWED = 3
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
self.settings = Settings.new()
|
||||
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Headerbar.ui")
|
||||
self.builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/ExportPopover.ui")
|
||||
|
||||
self.hb = self.builder.get_object(
|
||||
"Headerbar")
|
||||
self.hb_revealer = self.builder.get_object(
|
||||
"titlebar_revealer")
|
||||
|
||||
self.preview_toggle_revealer = self.builder.get_object(
|
||||
"preview_switch_revealer")
|
||||
self.preview_switcher_icon = self.builder.get_object(
|
||||
"preview_switch_toggle_icon")
|
||||
|
||||
self.__populate_layout_switcher_menu()
|
||||
self.update_preview_layout_icon()
|
||||
|
||||
self.sync_scroll_switch = self.builder.get_object("sync_scroll_switch")
|
||||
self.sync_scroll_switch.set_active(self.settings.get_value("sync-scroll"))
|
||||
self.sync_scroll_switch.connect("state-set", self.__on_sync_scroll)
|
||||
|
||||
self.menu_button = self.builder.get_object("menu_button")
|
||||
self.recents_button = self.builder.get_object("recents_button")
|
||||
self.export_button = self.builder.get_object("export_button")
|
||||
export_popover = self.builder.get_object("export_menu")
|
||||
self.preview_switch_button = self.builder.get_object("preview_switch_button")
|
||||
|
||||
self.export_button.set_popover(export_popover)
|
||||
|
||||
add_menus(self, app)
|
||||
|
||||
settings = Gtk.Settings.get_default()
|
||||
# TODO: use walrust operator whenever Python3.8 lands on SDK
|
||||
# if global_dark:= settings.props.gtk_theme_name.endswith("-dark"):
|
||||
global_dark = settings.props.gtk_theme_name.endswith("-dark")
|
||||
if global_dark:
|
||||
self.light_button.set_sensitive(False)
|
||||
self.light_button.set_tooltip_text(_(
|
||||
"Light mode isn't available while using a dark global theme"))
|
||||
|
||||
self.dark_button.set_active(self.settings.get_boolean("dark-mode") or global_dark)
|
||||
|
||||
self.light_button.connect("toggled", self.__on_dark_mode)
|
||||
|
||||
self.select_preview_layout_row()
|
||||
|
||||
def update_preview_layout_icon(self):
|
||||
mode = self.settings.get_enum("preview-mode")
|
||||
self.preview_switcher_icon.set_from_icon_name(
|
||||
self.__get_icon_for_preview_mode(mode), 4)
|
||||
|
||||
def select_preview_layout_row(self):
|
||||
mode = self.settings.get_enum("preview-mode")
|
||||
row = self.preview_menu.get_row_at_index(mode)
|
||||
self.preview_menu.select_row(row)
|
||||
|
||||
def __populate_layout_switcher_menu(self):
|
||||
self.preview_menu = self.builder.get_object("preview_switch_options")
|
||||
modes = self.settings.props.settings_schema.get_key("preview-mode").get_range()[1]
|
||||
|
||||
for i, mode in enumerate(modes):
|
||||
item_builder = Gtk.Builder()
|
||||
item_builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/PreviewLayoutSwitcherItem.ui")
|
||||
menu_item = item_builder.get_object("switcherItem")
|
||||
|
||||
menu_item.label = item_builder.get_object("label")
|
||||
menu_item.label.set_text(self.__get_text_for_preview_mode(i))
|
||||
|
||||
menu_item.icon = item_builder.get_object("icon")
|
||||
menu_item.icon.set_from_icon_name(self.__get_icon_for_preview_mode(i), 16)
|
||||
|
||||
menu_item.set_action_name("app.preview_mode")
|
||||
menu_item.set_action_target_value(GLib.Variant.new_string(mode))
|
||||
|
||||
menu_item.show_all()
|
||||
self.preview_menu.insert(menu_item, -1)
|
||||
|
||||
def __get_text_for_preview_mode(self, mode):
|
||||
if mode == self.FULL_WIDTH:
|
||||
return _("Full-Width")
|
||||
elif mode == self.HALF_WIDTH:
|
||||
return _("Half-Width")
|
||||
elif mode == self.HALF_HEIGHT:
|
||||
return _("Half-Height")
|
||||
elif mode == self.WINDOWED:
|
||||
return _("Windowed")
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(mode))
|
||||
|
||||
def __get_icon_for_preview_mode(self, mode):
|
||||
if mode == self.FULL_WIDTH:
|
||||
return "preview-layout-full-width-symbolic"
|
||||
elif mode == self.HALF_WIDTH:
|
||||
return "preview-layout-half-width-symbolic"
|
||||
elif mode == self.HALF_HEIGHT:
|
||||
return "preview-layout-half-height-symbolic"
|
||||
elif mode == self.WINDOWED:
|
||||
return "preview-layout-windowed-symbolic"
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(mode))
|
||||
|
||||
def __on_sync_scroll(self, _, state):
|
||||
self.settings.set_boolean("sync-scroll", state)
|
||||
return False
|
||||
|
||||
def __on_dark_mode(self, _):
|
||||
self.settings.set_boolean("dark-mode", self.dark_button.get_active())
|
||||
|
||||
class MainHeaderbar(BaseHeaderbar): # pylint: disable=too-few-public-methods
|
||||
"""Sets up the main application headerbar
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
BaseHeaderbar.__init__(self, app)
|
||||
|
||||
self.hb.set_show_close_button(True)
|
||||
|
||||
self.hb_revealer.props.transition_duration = 0
|
||||
|
||||
|
||||
class FullscreenHeaderbar(BaseHeaderbar):
|
||||
"""Sets up and manages the fullscreen headerbar and his events
|
||||
"""
|
||||
|
||||
def __init__(self, fs_builder, app):
|
||||
|
||||
BaseHeaderbar.__init__(self, app)
|
||||
|
||||
self.hb.set_show_close_button(False)
|
||||
|
||||
self.exit_fs_button = self.builder.get_object("exit_fs_button")
|
||||
self.exit_fs_button.set_visible(True)
|
||||
|
||||
self.events = fs_builder.get_object("FullscreenEventbox")
|
||||
self.events.add(self.hb_revealer)
|
||||
|
||||
# this is a little tricky
|
||||
# we show hb when the cursor enters an area of 1px at the top
|
||||
# as the hb is shown the height of the eventbox grows to accomodate it
|
||||
self.events.connect('enter_notify_event', self.show_fs_hb)
|
||||
self.events.connect('leave_notify_event', self.hide_fs_hb)
|
||||
self.menu_button.get_popover().connect('closed', self.hide_fs_hb)
|
||||
self.recents_button.get_popover().connect('closed', self.hide_fs_hb)
|
||||
self.export_button.get_popover().connect('closed', self.hide_fs_hb)
|
||||
self.preview_switch_button.get_popover().connect('closed', self.hide_fs_hb)
|
||||
|
||||
def show_fs_hb(self, _widget=None, _data=None):
|
||||
"""show headerbar of the fullscreen mode
|
||||
"""
|
||||
self.hb_revealer.set_reveal_child(True)
|
||||
|
||||
def hide_fs_hb(self, _widget=None, _data=None):
|
||||
"""hide headerbar of the fullscreen mode
|
||||
"""
|
||||
if (self.menu_button.get_active() or
|
||||
self.recents_button.get_active() or
|
||||
self.export_button.get_active() or
|
||||
self.preview_switch_button.get_active()):
|
||||
pass
|
||||
else:
|
||||
self.hb_revealer.set_reveal_child(False)
|
||||
|
||||
|
||||
class DummyHeaderbar(BaseHeaderbar):
|
||||
"""Sets up and manages the dummy headerbar wich fades away when entering
|
||||
the free-distracting mode
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
|
||||
BaseHeaderbar.__init__(self, app)
|
||||
|
||||
self.hb.set_show_close_button(True)
|
||||
self.hb_revealer.set_transition_type(
|
||||
Gtk.RevealerTransitionType.CROSSFADE)
|
||||
self.hb_revealer.set_reveal_child(False)
|
||||
self.hb_revealer.hide()
|
||||
|
||||
self.menu_button.set_sensitive(True)
|
||||
self.recents_button.set_sensitive(True)
|
||||
|
||||
def show_dm_hb(self):
|
||||
"""show dummy headerbar:
|
||||
It appears instantly to inmediatly fade away
|
||||
"""
|
||||
self.hb_revealer.show()
|
||||
self.hb_revealer.set_transition_duration(0)
|
||||
self.hb_revealer.set_reveal_child(True)
|
||||
self.hb_revealer.set_transition_duration(600)
|
||||
self.hb_revealer.set_reveal_child(False)
|
||||
|
||||
def hide_dm_hb(self):
|
||||
"""hide dummy headerbar
|
||||
It appears slowly to inmediatly dissapear
|
||||
"""
|
||||
self.hb_revealer.set_transition_duration(400)
|
||||
self.hb_revealer.set_reveal_child(True)
|
||||
GLib.timeout_add(400, self.hide_dm_hb_with_wait)
|
||||
|
||||
def hide_dm_hb_with_wait(self):
|
||||
self.hb_revealer.set_transition_duration(0)
|
||||
self.hb_revealer.set_reveal_child(False)
|
||||
self.hb_revealer.hide()
|
||||
return False
|
||||
|
||||
|
||||
class PreviewHeaderbar:
|
||||
"""Sets up the preview headerbar
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.hb = Gtk.HeaderBar().new()
|
||||
self.hb.props.show_close_button = True
|
||||
self.hb.get_style_context().add_class("titlebar")
|
||||
|
||||
self.hb_revealer = Gtk.Revealer(name="titlebar-revealer-pv")
|
||||
self.hb_revealer.add(self.hb)
|
||||
self.hb_revealer.props.transition_duration = 750
|
||||
self.hb_revealer.set_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.set_shadow_type(Gtk.ShadowType.NONE)
|
||||
self.hb_container.add(self.hb_revealer)
|
||||
self.hb_container.show()
|
||||
|
||||
self.hb.show_all()
|
||||
|
||||
|
||||
def add_menus(headerbar, app):
|
||||
""" Add menu models to hb buttons
|
||||
"""
|
||||
|
||||
# Add menu model to the menu button
|
||||
|
||||
builder_window_menu = Gtk.Builder()
|
||||
builder_window_menu.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Menu.ui")
|
||||
model = builder_window_menu.get_object("Menu")
|
||||
headerbar.light_button = builder_window_menu.get_object("light_mode_button")
|
||||
headerbar.dark_button = builder_window_menu.get_object("dark_mode_button")
|
||||
|
||||
headerbar.menu_button.set_popover(model)
|
||||
|
||||
# Add recents menu to the open recents button
|
||||
|
||||
recents_builder = Gtk.Builder()
|
||||
recents_builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Recents.ui")
|
||||
recents = recents_builder.get_object("recent_md_popover")
|
||||
|
||||
recents_treeview = get_descendant(recents, "recent_view", level=0)
|
||||
recents_treeview.set_activate_on_single_click(True)
|
||||
|
||||
recents_wd = recents_builder.get_object("recent_md_widget")
|
||||
recents_wd.connect('item-activated', app.on_open_recent)
|
||||
|
||||
headerbar.recents_button.set_popover(recents)
|
||||
headerbar.recents_button.set_sensitive(True)
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -20,42 +20,34 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
|
||||
import gi
|
||||
import pypandoc
|
||||
from gi.overrides.Pango import Pango
|
||||
|
||||
from uberwriter.settings import Settings
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk # pylint: disable=E0611
|
||||
|
||||
from uberwriter.config import get_data_file
|
||||
from uberwriter.builder import Builder
|
||||
from apostrophe.config import get_data_file
|
||||
from apostrophe.builder import Builder
|
||||
|
||||
|
||||
def get_builder(builder_file_name):
|
||||
"""Return a fully-instantiated Gtk.Builder instance from specified ui
|
||||
file
|
||||
|
||||
:param builder_file_name: The name of the builder file, without extension.
|
||||
Assumed to be in the 'ui' directory under the data path.
|
||||
"""
|
||||
# Look for the ui file that describes the user interface.
|
||||
ui_filename = get_data_file('ui', '%s.ui' % (builder_file_name,))
|
||||
if not os.path.exists(ui_filename):
|
||||
ui_filename = None
|
||||
|
||||
builder = Builder()
|
||||
builder.set_translation_domain()
|
||||
builder.add_from_file(ui_filename)
|
||||
return builder
|
||||
@contextmanager
|
||||
def user_action(text_buffer):
|
||||
text_buffer.begin_user_action()
|
||||
yield text_buffer
|
||||
text_buffer.end_user_action()
|
||||
|
||||
|
||||
def path_to_file(path):
|
||||
"""Return a file path (file:///) for the given path"""
|
||||
|
||||
return "file:///" + path
|
||||
return "file://" + path
|
||||
|
||||
|
||||
def get_media_file(media_file_path):
|
||||
|
@ -93,6 +85,15 @@ def get_script_path(script_file_name):
|
|||
return script_path
|
||||
|
||||
|
||||
def get_reference_files_path(reference_file_name):
|
||||
"""Return the full path of a given filename under the reference_files dir
|
||||
"""
|
||||
refs_path = get_data_file('reference_files', '%s' % (reference_file_name,))
|
||||
if not os.path.exists(refs_path):
|
||||
refs_path = ""
|
||||
return refs_path
|
||||
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
@ -107,12 +108,12 @@ def set_up_logging(opts):
|
|||
formatter = logging.Formatter(
|
||||
"%(levelname)s:%(name)s: %(funcName)s() '%(message)s'")
|
||||
|
||||
logger = logging.getLogger('uberwriter')
|
||||
logger = logging.getLogger('apostrophe')
|
||||
logger_sh = logging.StreamHandler()
|
||||
logger_sh.setFormatter(formatter)
|
||||
logger.addHandler(logger_sh)
|
||||
|
||||
lib_logger = logging.getLogger('uberwriter')
|
||||
lib_logger = logging.getLogger('apostrophe')
|
||||
lib_logger_sh = logging.StreamHandler()
|
||||
lib_logger_sh.setFormatter(formatter)
|
||||
lib_logger.addHandler(lib_logger_sh)
|
||||
|
@ -132,7 +133,7 @@ def get_help_uri(page=None):
|
|||
|
||||
if not os.path.exists(help_uri):
|
||||
# installed so use gnome help tree - user's language
|
||||
help_uri = 'uberwriter'
|
||||
help_uri = 'apostrophe'
|
||||
|
||||
# unspecified page is the index.page
|
||||
if page is not None:
|
||||
|
@ -148,12 +149,14 @@ def show_uri(parent, link):
|
|||
|
||||
def alias(alternative_function_name):
|
||||
'''see http://www.drdobbs.com/web-development/184406073#l9'''
|
||||
|
||||
def decorator(function):
|
||||
'''attach alternative_function_name(s) to function'''
|
||||
if not hasattr(function, 'aliases'):
|
||||
function.aliases = []
|
||||
function.aliases.append(alternative_function_name)
|
||||
return function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
@ -172,21 +175,21 @@ def exist_executable(command):
|
|||
|
||||
def get_descendant(widget, child_name, level, doPrint=False):
|
||||
if widget is not None:
|
||||
if doPrint: print("-"*level + str(Gtk.Buildable.get_name(widget)) +
|
||||
if doPrint: print("-" * level + str(Gtk.Buildable.get_name(widget)) +
|
||||
" :: " + widget.get_name())
|
||||
else:
|
||||
if doPrint: print("-"*level + "None")
|
||||
if doPrint: print("-" * level + "None")
|
||||
return None
|
||||
#/*** If it is what we are looking for ***/
|
||||
if Gtk.Buildable.get_name(widget) == child_name: # not widget.get_name() !
|
||||
# /*** If it is what we are looking for ***/
|
||||
if Gtk.Buildable.get_name(widget) == child_name: # not widget.get_name() !
|
||||
return widget
|
||||
#/*** If this widget has one child only search its child ***/
|
||||
# /*** If this widget has one child only search its child ***/
|
||||
if (hasattr(widget, 'get_child') and
|
||||
callable(getattr(widget, 'get_child')) and
|
||||
child_name != ""):
|
||||
child = widget.get_child()
|
||||
if child is not None:
|
||||
return get_descendant(child, child_name, level+1,doPrint)
|
||||
return get_descendant(child, child_name, level + 1, doPrint)
|
||||
# /*** Ity might have many children, so search them ***/
|
||||
elif (hasattr(widget, 'get_children') and
|
||||
callable(getattr(widget, 'get_children')) and
|
||||
|
@ -196,7 +199,7 @@ def get_descendant(widget, child_name, level, doPrint=False):
|
|||
found = None
|
||||
for child in children:
|
||||
if child is not None:
|
||||
found = get_descendant(child, child_name, level+1, doPrint) # //search the child
|
||||
found = get_descendant(child, child_name, level + 1, doPrint) # //search the child
|
||||
if found: return found
|
||||
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# END LICENSE
|
||||
|
||||
import re
|
||||
import os
|
||||
import telnetlib
|
||||
from gettext import gettext as _
|
||||
from urllib.parse import unquote
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("WebKit2", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
|
||||
from gi.repository import WebKit2
|
||||
from apostrophe import latex_to_PNG, markup_regex
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
|
||||
class DictAccessor:
|
||||
reEndResponse = re.compile(br"^[2-5][0-58][0-9] .*\r\n$", re.DOTALL + re.MULTILINE)
|
||||
reDefinition = re.compile(br"^151(.*?)^\.", re.DOTALL + re.MULTILINE)
|
||||
|
||||
def __init__(self, host="pan.alephnull.com", port=2628, timeout=60):
|
||||
self.telnet = telnetlib.Telnet(host, port)
|
||||
self.timeout = timeout
|
||||
self.login_response = self.telnet.expect([self.reEndResponse], self.timeout)[2]
|
||||
|
||||
def run_command(self, cmd):
|
||||
self.telnet.write(cmd.encode("utf-8") + b"\r\n")
|
||||
return self.telnet.expect([self.reEndResponse], self.timeout)[2]
|
||||
|
||||
def get_matches(self, database, strategy, word):
|
||||
if database in ["", "all"]:
|
||||
d = "*"
|
||||
else:
|
||||
d = database
|
||||
if strategy in ["", "default"]:
|
||||
s = "."
|
||||
else:
|
||||
s = strategy
|
||||
w = word.replace("\"", r"\\\"")
|
||||
tsplit = self.run_command("MATCH {} {} \"{}\"".format(d, s, w)).splitlines()
|
||||
mlist = list()
|
||||
if tsplit[-1].startswith(b"250 ok") and tsplit[0].startswith(b"1"):
|
||||
mlines = tsplit[1:-2]
|
||||
for line in mlines:
|
||||
lsplit = line.strip().split()
|
||||
db = lsplit[0]
|
||||
word = unquote(" ".join(lsplit[1:]))
|
||||
mlist.append((db, word))
|
||||
return mlist
|
||||
|
||||
def get_definition(self, database, word):
|
||||
if database in ["", "all"]:
|
||||
d = "*"
|
||||
else:
|
||||
d = database
|
||||
w = word.replace("\"", r"\\\"")
|
||||
dsplit = self.run_command("DEFINE {} \"{}\"".format(d, w)).splitlines(True)
|
||||
|
||||
dlist = list()
|
||||
if dsplit[-1].startswith(b"250 ok") and dsplit[0].startswith(b"1"):
|
||||
dlines = dsplit[1:-1]
|
||||
dtext = b"".join(dlines)
|
||||
dlist = [dtext]
|
||||
return dlist
|
||||
|
||||
def close(self):
|
||||
t = self.run_command("QUIT")
|
||||
self.telnet.close()
|
||||
return t
|
||||
|
||||
def parse_wordnet(self, response):
|
||||
# consisting of group (n,v,adj,adv)
|
||||
# number, description, examples, synonyms, antonyms
|
||||
|
||||
lines = response.splitlines()
|
||||
lines = lines[2:]
|
||||
lines = " ".join(lines)
|
||||
lines = re.sub(r"\s+", " ", lines).strip()
|
||||
lines = re.split(r"( adv | adj | n | v |^adv |^adj |^n |^v )", lines)
|
||||
res = []
|
||||
act_res = {"defs": [], "class": "none", "num": "None"}
|
||||
for l in lines:
|
||||
l = l.strip()
|
||||
if not l:
|
||||
continue
|
||||
if l in ["adv", "adj", "n", "v"]:
|
||||
if act_res:
|
||||
res.append(act_res.copy())
|
||||
act_res = {"defs": [], "class": l}
|
||||
else:
|
||||
ll = re.split(r"(?: |^)(\d): ", l)
|
||||
act_def = {}
|
||||
for lll in ll:
|
||||
if lll.strip().isdigit() or not lll.strip():
|
||||
if "description" in act_def and act_def["description"]:
|
||||
act_res["defs"].append(act_def.copy())
|
||||
act_def = {"num": lll}
|
||||
continue
|
||||
a = re.findall(r"(\[(syn|ant): (.+?)\] ??)+", lll)
|
||||
for n in a:
|
||||
if n[1] == "syn":
|
||||
act_def["syn"] = re.findall(r"{(.*?)}.*?", n[2])
|
||||
else:
|
||||
act_def["ant"] = re.findall(r"{(.*?)}.*?", n[2])
|
||||
tbr = re.search(r"\[.+\]", lll)
|
||||
if tbr:
|
||||
lll = lll[:tbr.start()]
|
||||
lll = lll.split(";")
|
||||
act_def["examples"] = []
|
||||
act_def["description"] = []
|
||||
for llll in lll:
|
||||
llll = llll.strip()
|
||||
if llll.strip().startswith("\""):
|
||||
act_def["examples"].append(llll)
|
||||
else:
|
||||
act_def["description"].append(llll)
|
||||
if act_def and "description" in act_def:
|
||||
act_res["defs"].append(act_def.copy())
|
||||
|
||||
res.append(act_res.copy())
|
||||
return res
|
||||
|
||||
|
||||
def get_dictionary(term):
|
||||
da = DictAccessor()
|
||||
output = da.get_definition("wn", term)
|
||||
if output:
|
||||
output = output[0]
|
||||
else:
|
||||
return None
|
||||
return da.parse_wordnet(output.decode(encoding="UTF-8"))
|
||||
|
||||
|
||||
class InlinePreview:
|
||||
WIDTH = 400
|
||||
HEIGHT = 300
|
||||
|
||||
def __init__(self, text_view):
|
||||
self.settings = Settings.new()
|
||||
|
||||
self.text_view = text_view
|
||||
self.text_view.connect("button-press-event", self.on_button_press_event)
|
||||
self.text_buffer = text_view.get_buffer()
|
||||
self.cursor_mark = self.text_buffer.create_mark(
|
||||
"click", self.text_buffer.get_iter_at_mark(self.text_buffer.get_insert()))
|
||||
|
||||
self.latex_converter = latex_to_PNG.LatexToPNG()
|
||||
self.characters_per_line = self.settings.get_int("characters-per-line")
|
||||
|
||||
self.popover = Gtk.Popover.new(self.text_view)
|
||||
self.popover.get_style_context().add_class("quick-preview-popup")
|
||||
self.popover.set_modal(True)
|
||||
|
||||
self.preview_fns = {
|
||||
markup_regex.MATH: self.get_view_for_math,
|
||||
markup_regex.IMAGE: self.get_view_for_image,
|
||||
markup_regex.LINK: self.get_view_for_link,
|
||||
markup_regex.LINK_ALT: self.get_view_for_link,
|
||||
markup_regex.FOOTNOTE_ID: self.get_view_for_footnote,
|
||||
re.compile(r"(?P<text>\w+)"): self.get_view_for_lexikon
|
||||
}
|
||||
|
||||
def on_button_press_event(self, _text_view, event):
|
||||
if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK:
|
||||
x, y = self.text_view.window_to_buffer_coords(2, int(event.x), int(event.y))
|
||||
self.text_buffer.move_mark(
|
||||
self.cursor_mark, self.text_view.get_iter_at_location(x, y).iter)
|
||||
self.open_popover(self.text_view)
|
||||
|
||||
def get_view_for_math(self, match):
|
||||
success, result = self.latex_converter.generatepng(match.group("text"))
|
||||
if success:
|
||||
return Gtk.Image.new_from_file(result)
|
||||
else:
|
||||
error = _("Formula looks incorrect:")
|
||||
error += "\n\n“{}”".format(result)
|
||||
return Gtk.Label(label=error)
|
||||
|
||||
def get_view_for_image(self, match):
|
||||
path = match.group("url")
|
||||
|
||||
if path.startswith(("https://", "http://", "www.")):
|
||||
return self.get_view_for_link(match)
|
||||
if path.startswith(("file://")):
|
||||
path = path[7:]
|
||||
if not path.startswith(("/", "file://")):
|
||||
path = os.path.join(self.settings.get_string("open-file-path"), path)
|
||||
path = unquote(path)
|
||||
|
||||
return Gtk.Image.new_from_pixbuf(
|
||||
GdkPixbuf.Pixbuf.new_from_file_at_size(path, self.WIDTH, self.HEIGHT))
|
||||
|
||||
def get_view_for_link(self, match):
|
||||
url = match.group("url")
|
||||
web_view = WebKit2.WebView(zoom_level=0.3125) # ~1280x960
|
||||
web_view.set_size_request(self.WIDTH, self.HEIGHT)
|
||||
if GLib.uri_parse_scheme(url) is None:
|
||||
url = "http://{}".format(url)
|
||||
web_view.load_uri(url)
|
||||
return web_view
|
||||
|
||||
def get_view_for_footnote(self, match):
|
||||
footnote_id = match.group("id")
|
||||
fn_matches = re.finditer(markup_regex.FOOTNOTE, self.text_buffer.props.text)
|
||||
for fn_match in fn_matches:
|
||||
if fn_match.group("id") == footnote_id:
|
||||
if fn_match:
|
||||
footnote = re.sub("\n[\t ]+", "\n", fn_match.group("text"))
|
||||
else:
|
||||
footnote = _("No matching footnote found")
|
||||
label = Gtk.Label(label=footnote)
|
||||
label.set_max_width_chars(self.characters_per_line)
|
||||
label.set_line_wrap(True)
|
||||
return label
|
||||
return None
|
||||
|
||||
def get_view_for_lexikon(self, match):
|
||||
term = match.group("text")
|
||||
lexikon_dict = get_dictionary(term)
|
||||
if lexikon_dict:
|
||||
grid = Gtk.Grid.new()
|
||||
grid.get_style_context().add_class("lexikon")
|
||||
grid.set_row_spacing(2)
|
||||
grid.set_column_spacing(4)
|
||||
i = 0
|
||||
for entry in lexikon_dict:
|
||||
if not entry["defs"]:
|
||||
continue
|
||||
elif entry["class"].startswith("n"):
|
||||
word_type = _("noun")
|
||||
elif entry["class"].startswith("v"):
|
||||
word_type = _("verb")
|
||||
elif entry["class"].startswith("adj"):
|
||||
word_type = _("adjective")
|
||||
elif entry["class"].startswith("adv"):
|
||||
word_type = _("adverb")
|
||||
else:
|
||||
continue
|
||||
|
||||
vocab_label = Gtk.Label.new(term + " ~ " + word_type)
|
||||
vocab_label.get_style_context().add_class("header")
|
||||
if i == 0:
|
||||
vocab_label.get_style_context().add_class("first")
|
||||
vocab_label.set_halign(Gtk.Align.START)
|
||||
vocab_label.set_justify(Gtk.Justification.LEFT)
|
||||
grid.attach(vocab_label, 0, i, 3, 1)
|
||||
|
||||
for definition in entry["defs"]:
|
||||
i = i + 1
|
||||
num_label = Gtk.Label.new(definition["num"] + ".")
|
||||
num_label.get_style_context().add_class("number")
|
||||
num_label.set_valign(Gtk.Align.START)
|
||||
grid.attach(num_label, 0, i, 1, 1)
|
||||
|
||||
def_label = Gtk.Label(label=" ".join(definition["description"]))
|
||||
def_label.get_style_context().add_class("description")
|
||||
def_label.set_halign(Gtk.Align.START)
|
||||
def_label.set_max_width_chars(self.characters_per_line)
|
||||
def_label.set_line_wrap(True)
|
||||
def_label.set_justify(Gtk.Justification.FILL)
|
||||
grid.attach(def_label, 1, i, 1, 1)
|
||||
i = i + 1
|
||||
if i > 0:
|
||||
return grid
|
||||
return None
|
||||
|
||||
def open_popover(self, _editor, _data=None):
|
||||
start_iter = self.text_buffer.get_iter_at_mark(self.cursor_mark)
|
||||
line_offset = start_iter.get_line_offset()
|
||||
end_iter = start_iter.copy()
|
||||
start_iter.set_line_offset(0)
|
||||
end_iter.forward_to_line_end()
|
||||
text = self.text_buffer.get_text(start_iter, end_iter, False)
|
||||
|
||||
for regex, get_view_fn in self.preview_fns.items():
|
||||
matches = re.finditer(regex, text)
|
||||
for match in matches:
|
||||
if match.start() <= line_offset <= match.end():
|
||||
prev_view = self.popover.get_child()
|
||||
if prev_view:
|
||||
prev_view.destroy()
|
||||
view = get_view_fn(match)
|
||||
view.show_all()
|
||||
self.popover.add(view)
|
||||
rect = self.text_view.get_iter_location(
|
||||
self.text_buffer.get_iter_at_mark(self.cursor_mark))
|
||||
rect.x, rect.y = self.text_view.buffer_to_window_coords(
|
||||
Gtk.TextWindowType.TEXT, rect.x, rect.y)
|
||||
self.popover.set_pointing_to(rect)
|
||||
GLib.idle_add(self.popover.popup) # TODO: It doesn't popup without idle_add.
|
||||
return
|
|
@ -0,0 +1,128 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
"""
|
||||
Based on latex2png.py from Stuart Rackham
|
||||
|
||||
AUTHOR
|
||||
Written by Stuart Rackham, <srackham@gmail.com>
|
||||
The code was inspired by Kjell Magne Fauske"s code:
|
||||
http://fauskes.net/nb/htmleqII/
|
||||
|
||||
See also:
|
||||
http://www.amk.ca/python/code/mt-math
|
||||
http://code.google.com/p/latexmath2png/
|
||||
|
||||
COPYING
|
||||
Copyright (C) 2010 Stuart Rackham. Free use of this software is
|
||||
granted under the terms of the MIT License.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
class LatexToPNG:
|
||||
TEX_HEADER = r"""\documentclass{article}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{bm}
|
||||
\newcommand{\mx}[1]{\mathbf{\bm{#1}}} % Matrix command
|
||||
\newcommand{\vc}[1]{\mathbf{\bm{#1}}} % Vector command
|
||||
\newcommand{\T}{\text{T}} % Transpose
|
||||
\pagestyle{empty}
|
||||
\begin{document}"""
|
||||
|
||||
TEX_FOOTER = r"""\end{document}"""
|
||||
|
||||
def __init__(self):
|
||||
self.temp_result = tempfile.NamedTemporaryFile(suffix=".png")
|
||||
|
||||
def latex2png(self, tex, outfile, dpi, modified):
|
||||
"""Convert LaTeX input file infile to PNG file named outfile."""
|
||||
outfile = os.path.abspath(outfile)
|
||||
outdir = os.path.dirname(outfile)
|
||||
texfile = tempfile.mktemp(suffix=".tex", dir=os.path.dirname(outfile))
|
||||
basefile = os.path.splitext(texfile)[0]
|
||||
dvifile = basefile + ".dvi"
|
||||
temps = [basefile + ext for ext in (".tex", ".dvi", ".aux", ".log")]
|
||||
skip = False
|
||||
|
||||
tex = "{}\n{}\n{}\n".format(self.TEX_HEADER, tex.strip(), self.TEX_FOOTER)
|
||||
|
||||
open(texfile, "w").write(tex)
|
||||
saved_pwd = os.getcwd()
|
||||
|
||||
os.chdir(outdir)
|
||||
|
||||
args = ["latex", "-halt-on-error", texfile]
|
||||
p = subprocess.Popen(args,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdout=subprocess.PIPE)
|
||||
|
||||
output = p.stdout
|
||||
output_lines = output.readlines()
|
||||
if os.path.isfile(dvifile): # DVI File exists
|
||||
# Convert DVI file to PNG.
|
||||
args = ["dvipng",
|
||||
"-D", str(dpi),
|
||||
"-T", "tight",
|
||||
"-x", "1000",
|
||||
"-z", "9",
|
||||
"-bg", "Transparent",
|
||||
"-o", outfile,
|
||||
dvifile]
|
||||
|
||||
p = subprocess.Popen(args)
|
||||
p.communicate()
|
||||
|
||||
else:
|
||||
self.clean_up(temps)
|
||||
"""
|
||||
Errors in Latex output start with "! "
|
||||
Stripping exclamation marks and superflous newlines
|
||||
and telling the user what he"s done wrong.
|
||||
"""
|
||||
i = []
|
||||
error = ""
|
||||
for line in output_lines:
|
||||
line = line.decode("utf-8")
|
||||
if line.startswith("!"):
|
||||
error += line[2:] # removing "! "
|
||||
if error.endswith("\n"):
|
||||
error = error[:-1]
|
||||
raise Exception(error)
|
||||
|
||||
def generatepng(self, formula):
|
||||
try:
|
||||
self.temp_result = tempfile.NamedTemporaryFile(suffix=".png")
|
||||
formula = "$" + formula + "$"
|
||||
self.latex2png(formula, self.temp_result.name, 300, False)
|
||||
return True, self.temp_result.name
|
||||
|
||||
except Exception as e:
|
||||
self.temp_result.close()
|
||||
return False, e.args[0]
|
||||
|
||||
def clean_up(self, files):
|
||||
for f in files:
|
||||
if os.path.isfile(f):
|
||||
os.remove(f)
|
|
@ -0,0 +1,652 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# END LICENSE
|
||||
|
||||
import io
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import urllib
|
||||
from gettext import gettext as _
|
||||
|
||||
import gi
|
||||
|
||||
from apostrophe.export_dialog import Export
|
||||
from apostrophe.preview_handler import PreviewHandler
|
||||
from apostrophe.stats_handler import StatsHandler
|
||||
from apostrophe.styled_window import StyledWindow
|
||||
from apostrophe.text_view import TextView
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GObject, GLib, Gio
|
||||
|
||||
import cairo
|
||||
|
||||
from apostrophe import helpers
|
||||
|
||||
from apostrophe.sidebar import Sidebar
|
||||
from apostrophe.search_and_replace import SearchAndReplace
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
from . import headerbars
|
||||
|
||||
# Some Globals
|
||||
# TODO move them somewhere for better
|
||||
# accesibility from other files
|
||||
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
CONFIG_PATH = os.path.expanduser("~/.config/apostrophe/")
|
||||
|
||||
|
||||
class MainWindow(StyledWindow):
|
||||
__gsignals__ = {
|
||||
'save-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'open-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'save-file-as': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'new-file': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'toggle-bibtex': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'toggle-preview': (GObject.SIGNAL_ACTION, None, ()),
|
||||
'close-window': (GObject.SIGNAL_ACTION, None, ())
|
||||
}
|
||||
|
||||
def __init__(self, app):
|
||||
"""Set up the main window"""
|
||||
|
||||
super().__init__(application=Gio.Application.get_default(), title="Apostrophe")
|
||||
|
||||
self.get_style_context().add_class('apostrophe-window')
|
||||
|
||||
# Set UI
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Window.ui")
|
||||
root = builder.get_object("AppOverlay")
|
||||
self.connect("delete-event", self.on_delete_called)
|
||||
self.add(root)
|
||||
|
||||
self.set_default_size(1000, 600)
|
||||
|
||||
# Preferences
|
||||
self.settings = Settings.new()
|
||||
|
||||
# Headerbars
|
||||
self.last_height = 0
|
||||
self.headerbar = headerbars.MainHeaderbar(app)
|
||||
self.headerbar.hb_revealer.connect(
|
||||
"size_allocate", self.header_size_allocate)
|
||||
self.set_titlebar(self.headerbar.hb_revealer)
|
||||
|
||||
self.fs_headerbar = headerbars.FullscreenHeaderbar(builder, app)
|
||||
|
||||
# Bind properties between normal and fs headerbar
|
||||
self.headerbar.light_button.bind_property(
|
||||
"active", self.fs_headerbar.light_button, "active",
|
||||
GObject.BindingFlags.BIDIRECTIONAL
|
||||
| GObject.BindingFlags.SYNC_CREATE)
|
||||
|
||||
self.headerbar.dark_button.bind_property(
|
||||
"active", self.fs_headerbar.dark_button, "active",
|
||||
GObject.BindingFlags.BIDIRECTIONAL
|
||||
| GObject.BindingFlags.SYNC_CREATE)
|
||||
|
||||
# The dummy headerbar is a cosmetic hack to be able to
|
||||
# crossfade the hb on top of the window
|
||||
self.dm_headerbar = headerbars.DummyHeaderbar(app)
|
||||
root.add_overlay(self.dm_headerbar.hb_revealer)
|
||||
root.reorder_overlay(self.dm_headerbar.hb_revealer, 0)
|
||||
root.set_overlay_pass_through(self.dm_headerbar.hb_revealer, True)
|
||||
|
||||
self.title_end = " – Apostrophe"
|
||||
self.set_headerbar_title("New File" + self.title_end)
|
||||
|
||||
self.accel_group = Gtk.AccelGroup()
|
||||
self.add_accel_group(self.accel_group)
|
||||
|
||||
self.scrolled_window = builder.get_object('editor_scrolledwindow')
|
||||
|
||||
# Setup text editor
|
||||
self.text_view = TextView(self.settings.get_int("characters-per-line"))
|
||||
self.text_view.set_top_margin(80)
|
||||
self.text_view.connect('focus-out-event', self.focus_out)
|
||||
self.text_view.get_buffer().connect('changed', self.on_text_changed)
|
||||
self.text_view.show()
|
||||
self.text_view.grab_focus()
|
||||
self.scrolled_window.add(self.text_view)
|
||||
|
||||
# Setup stats counter
|
||||
self.stats_revealer = builder.get_object('editor_stats_revealer')
|
||||
self.stats_button = builder.get_object('editor_stats_button')
|
||||
self.stats_handler = StatsHandler(self.stats_button, self.text_view)
|
||||
|
||||
# Setup preview
|
||||
content = builder.get_object('content')
|
||||
editor = builder.get_object('editor')
|
||||
self.preview_handler = PreviewHandler(self, content, editor, self.text_view)
|
||||
|
||||
# Setup header/stats bar
|
||||
self.headerbar_visible = True
|
||||
self.bottombar_visible = True
|
||||
self.buffer_modified_for_status_bar = False
|
||||
|
||||
# Init file name with None
|
||||
self.set_filename()
|
||||
|
||||
# Setting up spellcheck
|
||||
self.auto_correct = None
|
||||
self.toggle_spellcheck(self.settings.get_value("spellcheck"))
|
||||
self.did_change = False
|
||||
|
||||
###
|
||||
# Sidebar initialization test
|
||||
###
|
||||
self.paned_window = builder.get_object("main_paned")
|
||||
self.sidebar_box = builder.get_object("sidebar_box")
|
||||
self.sidebar = Sidebar(self)
|
||||
self.sidebar_box.hide()
|
||||
|
||||
###
|
||||
# Search and replace initialization
|
||||
# Same interface as Sidebar ;)
|
||||
###
|
||||
self.searchreplace = SearchAndReplace(self, self.text_view, builder)
|
||||
|
||||
# EventBoxes
|
||||
|
||||
self.headerbar_eventbox = builder.get_object("HeaderbarEventbox")
|
||||
self.headerbar_eventbox.connect('enter_notify_event',
|
||||
self.reveal_headerbar_bottombar)
|
||||
|
||||
self.stats_revealer.connect('enter_notify_event', self.reveal_bottombar)
|
||||
|
||||
def header_size_allocate(self, widget, allocation):
|
||||
""" When the main hb starts to shrink its size, add that size
|
||||
to the textview margin, so it stays in place
|
||||
"""
|
||||
|
||||
# prevent 1px jumps
|
||||
if allocation.height == 1 and not widget.get_child_revealed():
|
||||
allocation.height = 0
|
||||
|
||||
height = self.headerbar.hb.get_allocated_height() - allocation.height
|
||||
if height == self.last_height:
|
||||
return
|
||||
|
||||
self.last_height = height
|
||||
|
||||
self.text_view.update_vertical_margin(height)
|
||||
self.text_view.queue_draw()
|
||||
|
||||
def on_text_changed(self, *_args):
|
||||
"""called when the text changes, sets the self.did_change to true and
|
||||
updates the title and the counters to reflect that
|
||||
"""
|
||||
|
||||
if self.did_change is False:
|
||||
self.did_change = True
|
||||
title = self.get_title()
|
||||
self.set_headerbar_title("* " + title)
|
||||
|
||||
self.buffer_modified_for_status_bar = True
|
||||
if self.settings.get_value("autohide-headerbar"):
|
||||
self.hide_headerbar_bottombar()
|
||||
|
||||
def set_fullscreen(self, state):
|
||||
"""Puts the application in fullscreen mode and show/hides
|
||||
the poller for motion in the top border
|
||||
|
||||
Arguments:
|
||||
state {almost bool} -- The desired fullscreen state of the window
|
||||
"""
|
||||
|
||||
if state.get_boolean():
|
||||
self.fullscreen()
|
||||
self.fs_headerbar.events.show()
|
||||
self.fs_headerbar.hide_fs_hb()
|
||||
self.headerbar_eventbox.hide()
|
||||
else:
|
||||
self.unfullscreen()
|
||||
self.fs_headerbar.events.hide()
|
||||
self.headerbar_eventbox.show()
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def set_focus_mode(self, state):
|
||||
"""toggle focusmode
|
||||
"""
|
||||
|
||||
self.text_view.set_focus_mode(state.get_boolean(), self.headerbar.hb.get_allocated_height())
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def set_hemingway_mode(self, state):
|
||||
"""toggle hemingwaymode
|
||||
"""
|
||||
|
||||
self.text_view.set_hemingway_mode(state.get_boolean())
|
||||
self.text_view.grab_focus()
|
||||
|
||||
def toggle_preview(self, state):
|
||||
"""Toggle the preview mode
|
||||
|
||||
Arguments:
|
||||
state {gtk bool} -- Desired state of the preview mode (enabled/disabled)
|
||||
"""
|
||||
|
||||
if state.get_boolean():
|
||||
self.text_view.grab_focus()
|
||||
self.preview_handler.show()
|
||||
self.headerbar.preview_toggle_revealer.set_reveal_child(True)
|
||||
self.fs_headerbar.preview_toggle_revealer.set_reveal_child(True)
|
||||
self.dm_headerbar.preview_toggle_revealer.set_reveal_child(True)
|
||||
else:
|
||||
self.preview_handler.hide()
|
||||
self.text_view.grab_focus()
|
||||
self.headerbar.preview_toggle_revealer.set_reveal_child(False)
|
||||
self.fs_headerbar.preview_toggle_revealer.set_reveal_child(False)
|
||||
self.dm_headerbar.preview_toggle_revealer.set_reveal_child(False)
|
||||
|
||||
return True
|
||||
|
||||
# TODO: refactorizable
|
||||
def save_document(self, _widget=None, _data=None):
|
||||
"""provide to the user a filechooser and save the document
|
||||
where he wants. Call set_headbar_title after that
|
||||
"""
|
||||
|
||||
if self.filename:
|
||||
LOGGER.info("saving")
|
||||
filename = self.filename
|
||||
file_to_save = io.open(filename, encoding="utf-8", mode='w')
|
||||
file_to_save.write(self.text_view.get_text())
|
||||
file_to_save.close()
|
||||
if self.did_change:
|
||||
self.did_change = False
|
||||
title = self.get_title()
|
||||
self.set_headerbar_title(title[2:])
|
||||
return Gtk.ResponseType.OK
|
||||
|
||||
filefilter = Gtk.FileFilter.new()
|
||||
filefilter.add_mime_type('text/x-markdown')
|
||||
filefilter.add_mime_type('text/plain')
|
||||
filefilter.set_name('Markdown (.md)')
|
||||
filechooser = Gtk.FileChooserDialog(
|
||||
_("Save your File"),
|
||||
self,
|
||||
Gtk.FileChooserAction.SAVE,
|
||||
("_Cancel", Gtk.ResponseType.CANCEL,
|
||||
"_Save", Gtk.ResponseType.OK)
|
||||
)
|
||||
|
||||
filechooser.set_do_overwrite_confirmation(True)
|
||||
filechooser.add_filter(filefilter)
|
||||
response = filechooser.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
filename = filechooser.get_filename()
|
||||
|
||||
if filename[-3:] != ".md":
|
||||
filename = filename + ".md"
|
||||
try:
|
||||
self.recent_manager.add_item("file:/ " + filename)
|
||||
except:
|
||||
pass
|
||||
|
||||
file_to_save = io.open(filename, encoding="utf-8", mode='w')
|
||||
file_to_save.write(self.text_view.get_text())
|
||||
file_to_save.close()
|
||||
|
||||
self.set_filename(filename)
|
||||
self.set_headerbar_title(
|
||||
os.path.basename(filename) + self.title_end, filename)
|
||||
|
||||
self.did_change = False
|
||||
filechooser.destroy()
|
||||
return response
|
||||
|
||||
filechooser.destroy()
|
||||
return Gtk.ResponseType.CANCEL
|
||||
|
||||
def save_document_as(self, _widget=None, _data=None):
|
||||
"""provide to the user a filechooser and save the document
|
||||
where he wants. Call set_headbar_title after that
|
||||
"""
|
||||
filechooser = Gtk.FileChooserDialog(
|
||||
"Save your File",
|
||||
self,
|
||||
Gtk.FileChooserAction.SAVE,
|
||||
("_Cancel", Gtk.ResponseType.CANCEL,
|
||||
"_Save", Gtk.ResponseType.OK)
|
||||
)
|
||||
filechooser.set_do_overwrite_confirmation(True)
|
||||
if self.filename:
|
||||
filechooser.set_filename(self.filename)
|
||||
response = filechooser.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
|
||||
filename = filechooser.get_filename()
|
||||
if filename[-3:] != ".md":
|
||||
filename = filename + ".md"
|
||||
try:
|
||||
self.recent_manager.remove_item("file:/" + filename)
|
||||
self.recent_manager.add_item("file:/ " + filename)
|
||||
except:
|
||||
pass
|
||||
|
||||
file_to_save = io.open(filename, encoding="utf-8", mode='w')
|
||||
file_to_save.write(self.text_view.get_text())
|
||||
file_to_save.close()
|
||||
|
||||
self.set_filename(filename)
|
||||
self.set_headerbar_title(
|
||||
os.path.basename(filename) + self.title_end, filename)
|
||||
|
||||
try:
|
||||
self.recent_manager.add_item(filename)
|
||||
except:
|
||||
pass
|
||||
|
||||
filechooser.destroy()
|
||||
self.did_change = False
|
||||
|
||||
else:
|
||||
filechooser.destroy()
|
||||
return Gtk.ResponseType.CANCEL
|
||||
|
||||
def copy_html_to_clipboard(self, _widget=None, _date=None):
|
||||
"""Copies only html without headers etc. to Clipboard
|
||||
"""
|
||||
|
||||
output = helpers.pandoc_convert(self.text_view.get_text())
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(output, -1)
|
||||
clipboard.store()
|
||||
|
||||
def open_document(self, _widget=None):
|
||||
"""open the desired file
|
||||
"""
|
||||
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
markdown_filter = Gtk.FileFilter.new()
|
||||
markdown_filter.add_mime_type('text/markdown')
|
||||
markdown_filter.add_mime_type('text/x-markdown')
|
||||
markdown_filter.set_name(_('Markdown Files'))
|
||||
|
||||
plaintext_filter = Gtk.FileFilter.new()
|
||||
plaintext_filter.add_mime_type('text/plain')
|
||||
plaintext_filter.set_name(_('Plain Text Files'))
|
||||
|
||||
filechooser = Gtk.FileChooserDialog(
|
||||
_("Open a .md file"),
|
||||
self,
|
||||
Gtk.FileChooserAction.OPEN,
|
||||
("_Cancel", Gtk.ResponseType.CANCEL,
|
||||
"_Open", Gtk.ResponseType.OK)
|
||||
)
|
||||
filechooser.add_filter(markdown_filter)
|
||||
filechooser.add_filter(plaintext_filter)
|
||||
response = filechooser.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
filename = filechooser.get_filename()
|
||||
self.load_file(filename)
|
||||
filechooser.destroy()
|
||||
|
||||
elif response == Gtk.ResponseType.CANCEL:
|
||||
filechooser.destroy()
|
||||
|
||||
def check_change(self):
|
||||
"""Show dialog to prevent loss of unsaved changes
|
||||
"""
|
||||
|
||||
if self.filename:
|
||||
title = os.path.basename(self.filename)
|
||||
else:
|
||||
title = _("New File")
|
||||
|
||||
if self.did_change and self.text_view.get_text():
|
||||
dialog = Gtk.MessageDialog(self,
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.WARNING,
|
||||
Gtk.ButtonsType.NONE,
|
||||
_("Save changes to document “%s” before closing?") %
|
||||
title
|
||||
)
|
||||
|
||||
dialog.props.secondary_text = _("If you don’t save, " +
|
||||
"all your changes will be permanently lost.")
|
||||
close_button = dialog.add_button(_("Close without saving"), Gtk.ResponseType.NO)
|
||||
dialog.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
|
||||
dialog.add_button(_("Save now"), Gtk.ResponseType.YES)
|
||||
|
||||
close_button.get_style_context().add_class("destructive-action")
|
||||
# dialog.set_default_size(200, 60)
|
||||
dialog.set_default_response(Gtk.ResponseType.YES)
|
||||
response = dialog.run()
|
||||
|
||||
if response == Gtk.ResponseType.YES:
|
||||
if self.save_document() == Gtk.ResponseType.CANCEL:
|
||||
dialog.destroy()
|
||||
return self.check_change()
|
||||
|
||||
dialog.destroy()
|
||||
return response
|
||||
if response == Gtk.ResponseType.NO:
|
||||
dialog.destroy()
|
||||
return response
|
||||
|
||||
dialog.destroy()
|
||||
return Gtk.ResponseType.CANCEL
|
||||
|
||||
def new_document(self, _widget=None):
|
||||
"""create new document
|
||||
"""
|
||||
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
self.text_view.clear()
|
||||
|
||||
self.did_change = False
|
||||
self.set_filename()
|
||||
self.set_headerbar_title(_("New File") + self.title_end)
|
||||
|
||||
def update_default_stat(self):
|
||||
self.stats_handler.update_default_stat()
|
||||
|
||||
def update_preview_mode(self):
|
||||
self.preview_handler.update_preview_mode()
|
||||
self.headerbar.update_preview_layout_icon()
|
||||
self.headerbar.select_preview_layout_row()
|
||||
self.fs_headerbar.update_preview_layout_icon()
|
||||
self.fs_headerbar.select_preview_layout_row()
|
||||
|
||||
def menu_toggle_sidebar(self, _widget=None):
|
||||
"""WIP
|
||||
"""
|
||||
self.sidebar.toggle_sidebar()
|
||||
|
||||
def toggle_spellcheck(self, state):
|
||||
"""Enable/disable the autospellchecking
|
||||
|
||||
Arguments:
|
||||
status {gtk bool} -- Desired status of the spellchecking
|
||||
"""
|
||||
|
||||
self.text_view.set_spellcheck(state.get_boolean())
|
||||
|
||||
def reload_preview(self, reshow=False):
|
||||
self.preview_handler.reload(reshow=reshow)
|
||||
|
||||
def load_file(self, filename=None):
|
||||
"""Open File from command line or open / open recent etc."""
|
||||
LOGGER.info("trying to open " + filename)
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if filename:
|
||||
if filename.startswith('file://'):
|
||||
filename = filename[7:]
|
||||
filename = urllib.parse.unquote_plus(filename)
|
||||
self.text_view.clear()
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
with io.open(filename, encoding="utf-8", mode='r') as current_file:
|
||||
self.text_view.set_text(current_file.read())
|
||||
else:
|
||||
dialog = Gtk.MessageDialog(self,
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.WARNING,
|
||||
Gtk.ButtonsType.CLOSE,
|
||||
_("The file you tried to open doesn't exist.\
|
||||
\nA new file will be created in its place when you save the current one.")
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
self.set_headerbar_title(os.path.basename(filename) + self.title_end, filename)
|
||||
self.set_filename(filename)
|
||||
|
||||
except Exception as e:
|
||||
LOGGER.warning(_("Error Reading File: %r") % e)
|
||||
dialog = Gtk.MessageDialog(self,
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.WARNING,
|
||||
Gtk.ButtonsType.CLOSE,
|
||||
_("Error reading file:\
|
||||
\n%r" %e)
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
self.did_change = False
|
||||
else:
|
||||
LOGGER.warning("No File arg")
|
||||
|
||||
def open_apostrophe_markdown(self, _widget=None, _data=None):
|
||||
"""open a markdown mini tutorial
|
||||
"""
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.load_file(helpers.get_media_file('apostrophe_markdown.md'))
|
||||
|
||||
def open_search(self, replace=False):
|
||||
"""toggle the search box
|
||||
"""
|
||||
|
||||
self.searchreplace.toggle_search(replace=replace)
|
||||
|
||||
def open_advanced_export(self, export_format):
|
||||
"""open the export and advanced export dialog
|
||||
"""
|
||||
text = bytes(self.text_view.get_text(), "utf-8")
|
||||
|
||||
self.export = Export(self.filename, export_format, text)
|
||||
|
||||
def open_recent(self, _widget, data=None):
|
||||
"""open the given recent document
|
||||
"""
|
||||
|
||||
if data:
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
self.load_file(data)
|
||||
|
||||
def focus_out(self, _widget, _data=None):
|
||||
"""events called when the window losses focus
|
||||
"""
|
||||
self.reveal_headerbar_bottombar()
|
||||
|
||||
def reveal_headerbar_bottombar(self, _widget=None, _data=None):
|
||||
|
||||
def __reveal_hb():
|
||||
self.headerbar_eventbox.hide()
|
||||
self.headerbar.hb_revealer.set_reveal_child(True)
|
||||
self.get_style_context().remove_class("focus")
|
||||
return False
|
||||
|
||||
self.reveal_bottombar()
|
||||
|
||||
if not self.headerbar_visible:
|
||||
self.dm_headerbar.hide_dm_hb()
|
||||
GLib.timeout_add(400, __reveal_hb)
|
||||
|
||||
self.headerbar_visible = True
|
||||
|
||||
def reveal_bottombar(self, _widget=None, _data=None):
|
||||
|
||||
if not self.bottombar_visible:
|
||||
self.stats_revealer.set_reveal_child(True)
|
||||
|
||||
self.bottombar_visible = True
|
||||
|
||||
self.buffer_modified_for_status_bar = True
|
||||
|
||||
def hide_headerbar_bottombar(self):
|
||||
|
||||
if self.headerbar_visible:
|
||||
self.headerbar.hb_revealer.set_reveal_child(False)
|
||||
self.dm_headerbar.show_dm_hb()
|
||||
self.get_style_context().add_class("focus")
|
||||
|
||||
self.headerbar_visible = False
|
||||
|
||||
if self.bottombar_visible:
|
||||
self.stats_revealer.set_reveal_child(False)
|
||||
|
||||
self.bottombar_visible = False
|
||||
|
||||
self.headerbar_eventbox.show()
|
||||
self.buffer_modified_for_status_bar = False
|
||||
|
||||
def on_delete_called(self, _widget, _data=None):
|
||||
"""Called when the TexteditorWindow is closed.
|
||||
"""
|
||||
LOGGER.info('delete called')
|
||||
if self.check_change() == Gtk.ResponseType.CANCEL:
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_mnu_close_activate(self, _widget, _data=None):
|
||||
"""Signal handler for closing the Window.
|
||||
Overriden from parent Window Class
|
||||
"""
|
||||
if self.on_delete_called(self): # Really destroy?
|
||||
return
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
def set_headerbar_title(self, title, subtitle=None):
|
||||
"""set the desired headerbar title
|
||||
"""
|
||||
self.headerbar.hb.props.title = title
|
||||
self.dm_headerbar.hb.props.title = title
|
||||
self.fs_headerbar.hb.props.title = title
|
||||
if subtitle:
|
||||
self.headerbar.hb.props.subtitle = subtitle
|
||||
self.dm_headerbar.hb.props.subtitle = subtitle
|
||||
self.fs_headerbar.hb.props.subtitle = subtitle
|
||||
self.headerbar.hb.set_tooltip_text(subtitle)
|
||||
self.fs_headerbar.hb.set_tooltip_text(subtitle)
|
||||
self.set_title(title)
|
||||
|
||||
def set_filename(self, filename=None):
|
||||
"""set filename
|
||||
"""
|
||||
if filename:
|
||||
self.filename = filename
|
||||
base_path = os.path.dirname(self.filename)
|
||||
os.chdir(base_path)
|
||||
else:
|
||||
self.filename = None
|
||||
base_path = "/"
|
||||
self.settings.set_string("open-file-path", base_path)
|
|
@ -0,0 +1,42 @@
|
|||
import re
|
||||
|
||||
ITALIC_ASTERISK = re.compile(
|
||||
r"(?<!\\)\*[^\s\*](?P<text>.*?\S?.*?)(?<!\\)\*")
|
||||
ITALIC_UNDERSCORE = re.compile(
|
||||
r"(?<!(\\|\S))_[^\s_](?P<text>.*?\S?.*?)(?<!\\)_(?=\s)")
|
||||
BOLD = re.compile(
|
||||
r"(\*\*|__)[^\s*](?P<text>.*?\S.*?)\1")
|
||||
BOLD_ITALIC = re.compile(
|
||||
r"((\*\*|__)([*_])|([*_])(\*\*|__))[^\s*](?P<text>.*?\S.*?)(?:\5\4|\3\2)")
|
||||
STRIKETHROUGH = re.compile(
|
||||
r"~~(?P<text>.*?\S.*?)~~")
|
||||
CODE = re.compile(
|
||||
r"`(?P<text>[^`].+?)`")
|
||||
LINK = re.compile(
|
||||
r"\[(?P<text>.*)\]\((?P<url>.+?)(?: \"(?P<title>.+)\")?\)")
|
||||
LINK_ALT = re.compile(
|
||||
r"<(?P<text>(?P<url>[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*|(?:[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)))>")
|
||||
IMAGE = re.compile(
|
||||
r"!\[(?P<text>.*)\]\((?P<url>.+?)(?: \"(?P<title>.+)\")?\)")
|
||||
HORIZONTAL_RULE = re.compile(
|
||||
r"(?:^|\n{2,})(?P<symbols> {0,3}[*\-_]{3,} *)(?:\n{2,}|$)")
|
||||
LIST = re.compile(
|
||||
r"(?:^|\n)(?P<content>(?P<indent>(?:\t| {4})*)[\-*+]( +)(?P<text>.+(?:\n+ \2.+)*))")
|
||||
ORDERED_LIST = re.compile(
|
||||
r"(?:^|\n)(?P<content>(?P<indent>(?:\t| {4})*)(?P<prefix>(?:\d|[a-z])+[.)]) (?P<text>.+(?:\n+ {2}\2.+)*))")
|
||||
BLOCK_QUOTE = re.compile(
|
||||
r"^ {0,3}(?:> ?)+(?P<text>.+)", re.M)
|
||||
HEADER = re.compile(
|
||||
r"^ {0,3}(?P<level>#{1,6}) (?P<text>[^\n]+)", re.M)
|
||||
HEADER_UNDER = re.compile(
|
||||
r"(?:^\n*|\n\n)(?P<text>[^\s].+)\n {0,3}[=\-]+(?: +?\n|$)")
|
||||
CODE_BLOCK = re.compile(
|
||||
r"(?:^|\n) {0,3}(?P<block>([`~]{3})(?P<text>.+?) {0,3}\2)(?:\s+?\n|$)", re.S)
|
||||
TABLE = re.compile(
|
||||
r"^[\-+]{5,}\n(?P<text>.+?)\n[\-+]{5,}\n", re.S)
|
||||
MATH = re.compile(
|
||||
r"([$]{1,2})(?P<text>[^` ].+?[^`\\ ])\1")
|
||||
FOOTNOTE_ID = re.compile(
|
||||
r"[^\s]+\[\^(?P<id>(?P<text>[^\s]+))\]")
|
||||
FOOTNOTE = re.compile(
|
||||
r"(?:^\n*|\n\n)\[\^(?P<id>[^\s]+)\]: (?P<text>(?:[^\n]+|\n+(?=(?:\t| {4})))+)(?:\n+|$)", re.M)
|
|
@ -0,0 +1,503 @@
|
|||
### GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
### Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom
|
||||
to share and change it. By contrast, the GNU General Public Licenses
|
||||
are intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations
|
||||
below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that there
|
||||
is no warranty for the free library. Also, if the library is modified
|
||||
by someone else and passed on, the recipients should know that what
|
||||
they have is not the original version, so that the original author's
|
||||
reputation will not be affected by problems that might be introduced
|
||||
by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using a
|
||||
shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it
|
||||
becomes a de-facto standard. To achieve this, non-free programs must
|
||||
be allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
### TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
**0.** This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License"). Each
|
||||
licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control
|
||||
compilation and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does and
|
||||
what the program that uses the Library does.
|
||||
|
||||
**1.** You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
**2.** You may modify your copy or copies of the Library or any
|
||||
portion of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
- **a)** The modified work must itself be a software library.
|
||||
- **b)** You must cause the files modified to carry prominent
|
||||
notices stating that you changed the files and the date of
|
||||
any change.
|
||||
- **c)** You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
- **d)** If a facility in the modified Library refers to a function
|
||||
or a table of data to be supplied by an application program that
|
||||
uses the facility, other than as an argument passed when the
|
||||
facility is invoked, then you must make a good faith effort to
|
||||
ensure that, in the event an application does not supply such
|
||||
function or table, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of
|
||||
the application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
**3.** You may opt to apply the terms of the ordinary GNU General
|
||||
Public License instead of this License to a given copy of the Library.
|
||||
To do this, you must alter all the notices that refer to this License,
|
||||
so that they refer to the ordinary GNU General Public License, version
|
||||
2, instead of to this License. (If a newer version than version 2 of
|
||||
the ordinary GNU General Public License has appeared, then you can
|
||||
specify that version instead if you wish.) Do not make any other
|
||||
change in these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for that
|
||||
copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of the
|
||||
Library into a program that is not a library.
|
||||
|
||||
**4.** You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy from
|
||||
a designated place, then offering equivalent access to copy the source
|
||||
code from the same place satisfies the requirement to distribute the
|
||||
source code, even though third parties are not compelled to copy the
|
||||
source along with the object code.
|
||||
|
||||
**5.** A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a work,
|
||||
in isolation, is not a derivative work of the Library, and therefore
|
||||
falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License. Section
|
||||
6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data structure
|
||||
layouts and accessors, and small macros and small inline functions
|
||||
(ten lines or less in length), then the use of the object file is
|
||||
unrestricted, regardless of whether it is legally a derivative work.
|
||||
(Executables containing this object code plus portions of the Library
|
||||
will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
**6.** As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a work
|
||||
containing portions of the Library, and distribute that work under
|
||||
terms of your choice, provided that the terms permit modification of
|
||||
the work for the customer's own use and reverse engineering for
|
||||
debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
- **a)** Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood that
|
||||
the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
- **b)** Use a suitable shared library mechanism for linking with
|
||||
the Library. A suitable mechanism is one that (1) uses at run time
|
||||
a copy of the library already present on the user's computer
|
||||
system, rather than copying library functions into the executable,
|
||||
and (2) will operate properly with a modified version of the
|
||||
library, if the user installs one, as long as the modified version
|
||||
is interface-compatible with the version that the work was
|
||||
made with.
|
||||
- **c)** Accompany the work with a written offer, valid for at least
|
||||
three years, to give the same user the materials specified in
|
||||
Subsection 6a, above, for a charge no more than the cost of
|
||||
performing this distribution.
|
||||
- **d)** If distribution of the work is made by offering access to
|
||||
copy from a designated place, offer equivalent access to copy the
|
||||
above specified materials from the same place.
|
||||
- **e)** Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
**7.** You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
- **a)** Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other
|
||||
library facilities. This must be distributed under the terms of
|
||||
the Sections above.
|
||||
- **b)** Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
**8.** You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
**9.** You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
**10.** Each time you redistribute the Library (or any work based on
|
||||
the Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
**11.** If, as a consequence of a court judgment or allegation of
|
||||
patent infringement or for any other reason (not limited to patent
|
||||
issues), conditions are imposed on you (whether by court order,
|
||||
agreement or otherwise) that contradict the conditions of this
|
||||
License, they do not excuse you from the conditions of this License.
|
||||
If you cannot distribute so as to satisfy simultaneously your
|
||||
obligations under this License and any other pertinent obligations,
|
||||
then as a consequence you may not distribute the Library at all. For
|
||||
example, if a patent license would not permit royalty-free
|
||||
redistribution of the Library by all those who receive copies directly
|
||||
or indirectly through you, then the only way you could satisfy both it
|
||||
and this License would be to refrain entirely from distribution of the
|
||||
Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply, and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
**12.** If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
**13.** The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time. Such
|
||||
new versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
**14.** If you wish to incorporate parts of the Library into other
|
||||
free programs whose distribution conditions are incompatible with
|
||||
these, write to the author to ask for permission. For software which
|
||||
is copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
**NO WARRANTY**
|
||||
|
||||
**15.** BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
**16.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
### END OF TERMS AND CONDITIONS
|
||||
|
||||
### How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms
|
||||
of the ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It
|
||||
is safest to attach them to the start of each source file to most
|
||||
effectively convey the exclusion of warranty; and each file should
|
||||
have at least the "copyright" line and a pointer to where the full
|
||||
notice is found.
|
||||
|
||||
one line to give the library's name and an idea of what it does.
|
||||
Copyright (C) year name of author
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper
|
||||
mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or
|
||||
your school, if any, to sign a "copyright disclaimer" for the library,
|
||||
if necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in
|
||||
the library `Frob' (a library for tweaking knobs) written
|
||||
by James Random Hacker.
|
||||
|
||||
signature of Ty Coon, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
|
@ -6,7 +6,7 @@ from . import fuzzywuzzy
|
|||
from .gi_composites import GtkTemplate
|
||||
|
||||
|
||||
@GtkTemplate(ui='/home/wolfv/Programs/uberwriter/uberwriter/plugins/bibtex/bibtex_item.glade')
|
||||
@GtkTemplate(ui='/home/wolfv/Programs/apostrophe/apostrophe/plugins/bibtex/bibtex_item.glade')
|
||||
class BibTexItem(Gtk.Box):
|
||||
|
||||
__gtype_name__ = 'BibTexItem'
|
||||
|
@ -68,7 +68,7 @@ class BibTex(object):
|
|||
self.bib_db = bibtexparser.load(f)
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file('/home/wolfv/Programs/uberwriter/uberwriter/plugins/bibtex/bibtex.glade')
|
||||
builder.add_from_file('/home/wolfv/Programs/apostrophe/apostrophe/plugins/bibtex/bibtex.glade')
|
||||
self.window = builder.get_object('bibtex_window')
|
||||
self.window.set_transient_for(self.app)
|
||||
self.window.set_modal(True)
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 223 B |
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -25,9 +25,8 @@ import gi
|
|||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Pango, GLib # pylint: disable=E0611
|
||||
import logging
|
||||
logger = logging.getLogger('uberwriter')
|
||||
logger = logging.getLogger('apostrophe')
|
||||
|
||||
from uberwriter.helpers import get_builder
|
||||
|
||||
|
||||
class PreferencesDialog:
|
||||
|
@ -64,26 +63,20 @@ class PreferencesDialog:
|
|||
|
||||
def __init__(self, settings):
|
||||
self.settings = settings
|
||||
self.builder = get_builder("Preferences")
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Preferences.ui")
|
||||
|
||||
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.autohide_headerbar_switch = self.builder.get_object("autohide_headerbar_switch")
|
||||
self.autohide_headerbar_switch.set_active(self.settings.get_value("autohide-headerbar"))
|
||||
self.autohide_headerbar_switch.connect("state-set", self.on_autohide_headerbar)
|
||||
|
||||
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 = self.settings.get_string("input-format")
|
||||
input_format_active = 0
|
||||
for i, fmt in enumerate(self.formats):
|
||||
input_format_store.append([i, fmt["name"]])
|
||||
|
@ -106,29 +99,17 @@ class PreferencesDialog:
|
|||
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))
|
||||
def on_autohide_headerbar(self, _, state):
|
||||
self.settings.set_boolean("autohide-headerbar", state)
|
||||
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))
|
||||
self.settings.set_boolean("spellcheck", 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"]))
|
||||
self.settings.set_string("input-format", fmt["format"])
|
||||
|
||||
def on_input_format_help(self, _):
|
||||
fmt = self.formats[self.input_format_combobox.get_active()]
|
|
@ -0,0 +1,50 @@
|
|||
from queue import Queue
|
||||
from threading import Thread
|
||||
import os
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from apostrophe import helpers
|
||||
from apostrophe.theme import Theme
|
||||
|
||||
|
||||
class PreviewConverter:
|
||||
"""Converts markdown to html using a background thread."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.queue = Queue()
|
||||
worker = Thread(target=self.__do_convert, name="preview-converter")
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
|
||||
def convert(self, text, callback, *user_data):
|
||||
"""Converts text to html, calling callback when done.
|
||||
|
||||
The callback argument contains the result."""
|
||||
|
||||
self.queue.put((text, callback, user_data))
|
||||
|
||||
def stop(self):
|
||||
"""Stops the background worker. PreviewConverter shouldn't be used after this."""
|
||||
|
||||
self.queue.put((None, None))
|
||||
|
||||
def __do_convert(self):
|
||||
while True:
|
||||
while True:
|
||||
(text, callback, user_data) = self.queue.get()
|
||||
if text is None and callback is None:
|
||||
return
|
||||
if self.queue.empty():
|
||||
break
|
||||
|
||||
args = ['--standalone',
|
||||
'--mathjax',
|
||||
'--css=' + Theme.get_current().web_css_path,
|
||||
'--lua-filter=' + helpers.get_script_path('relative_to_absolute.lua'),
|
||||
'--lua-filter=' + helpers.get_script_path('task-list.lua')]
|
||||
text = helpers.pandoc_convert(text, to="html5", args=args)
|
||||
|
||||
GLib.idle_add(callback, text, *user_data)
|
|
@ -0,0 +1,154 @@
|
|||
import math
|
||||
import webbrowser
|
||||
from enum import auto, IntEnum
|
||||
|
||||
import gi
|
||||
|
||||
from apostrophe.preview_renderer import PreviewRenderer
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2, GLib, Gtk
|
||||
|
||||
from apostrophe.preview_converter import PreviewConverter
|
||||
from apostrophe.preview_web_view import PreviewWebView
|
||||
|
||||
|
||||
class Step(IntEnum):
|
||||
CONVERT_HTML = auto()
|
||||
LOAD_WEBVIEW = auto()
|
||||
RENDER = auto()
|
||||
|
||||
|
||||
class PreviewHandler:
|
||||
"""Handles showing/hiding the preview, and allows the user to toggle between modes.
|
||||
|
||||
The rendering itself is handled by `PreviewRendered`. This class handles conversion/loading and
|
||||
connects it all together (including synchronization, ie. text changes, scroll)."""
|
||||
|
||||
def __init__(self, window, content, editor, text_view):
|
||||
self.text_view = text_view
|
||||
|
||||
self.web_view = None
|
||||
self.web_view_pending_html = None
|
||||
|
||||
self.preview_converter = PreviewConverter()
|
||||
self.preview_renderer = PreviewRenderer(
|
||||
window, content, editor, text_view)
|
||||
|
||||
window.connect("style-updated", self.reload)
|
||||
|
||||
self.text_changed_handler_id = None
|
||||
|
||||
self.settings = Settings.new()
|
||||
self.web_scroll_handler_id = None
|
||||
self.text_scroll_handler_id = None
|
||||
|
||||
self.loading = False
|
||||
self.shown = False
|
||||
|
||||
def show(self):
|
||||
self.__show()
|
||||
|
||||
def __show(self, html=None, step=Step.CONVERT_HTML):
|
||||
if step == Step.CONVERT_HTML:
|
||||
# First step: convert text to HTML.
|
||||
buf = self.text_view.get_buffer()
|
||||
self.preview_converter.convert(
|
||||
buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False),
|
||||
self.__show, Step.LOAD_WEBVIEW)
|
||||
|
||||
elif step == Step.LOAD_WEBVIEW:
|
||||
# Second step: load HTML.
|
||||
self.loading = True
|
||||
|
||||
if not self.web_view:
|
||||
self.web_view = PreviewWebView()
|
||||
self.web_view.get_settings().set_allow_universal_access_from_file_urls(True)
|
||||
|
||||
# Show preview once the load is finished
|
||||
self.web_view.connect("load-changed", self.on_load_changed)
|
||||
|
||||
# All links will be opened in default browser, but local files are opened in apps.
|
||||
self.web_view.connect("decide-policy", self.on_click_link)
|
||||
|
||||
if self.web_view.is_loading():
|
||||
self.web_view_pending_html = html
|
||||
else:
|
||||
self.web_view.load_html(html, "file://localhost/")
|
||||
|
||||
elif step == Step.RENDER:
|
||||
# Last step: show the preview. This is a one-time step.
|
||||
if self.shown:
|
||||
return
|
||||
self.shown = True
|
||||
|
||||
self.text_changed_handler_id = \
|
||||
self.text_view.get_buffer().connect("changed", self.__show)
|
||||
|
||||
GLib.idle_add(self.web_view.set_scroll_scale, self.text_view.get_scroll_scale())
|
||||
|
||||
self.preview_renderer.show(self.web_view)
|
||||
|
||||
if self.settings.get_boolean("sync-scroll"):
|
||||
self.web_scroll_handler_id = \
|
||||
self.web_view.connect("scroll-scale-changed", self.on_web_view_scrolled)
|
||||
self.text_scroll_handler_id = \
|
||||
self.text_view.connect("scroll-scale-changed", self.on_text_view_scrolled)
|
||||
|
||||
def reload(self, *_widget, reshow=False):
|
||||
if self.shown:
|
||||
if reshow:
|
||||
self.hide()
|
||||
self.show()
|
||||
|
||||
def hide(self):
|
||||
if self.shown:
|
||||
self.shown = False
|
||||
|
||||
self.text_view.get_buffer().disconnect(self.text_changed_handler_id)
|
||||
|
||||
GLib.idle_add(self.text_view.set_scroll_scale, self.web_view.get_scroll_scale())
|
||||
|
||||
self.preview_renderer.hide(self.web_view)
|
||||
|
||||
if self.text_scroll_handler_id:
|
||||
self.text_view.disconnect(self.text_scroll_handler_id)
|
||||
self.text_scroll_handler_id = None
|
||||
if self.web_scroll_handler_id:
|
||||
self.web_view.disconnect(self.web_scroll_handler_id)
|
||||
self.web_scroll_handler_id = None
|
||||
|
||||
if self.loading:
|
||||
self.loading = False
|
||||
|
||||
self.web_view.destroy()
|
||||
self.web_view = None
|
||||
|
||||
def update_preview_mode(self):
|
||||
self.preview_renderer.update_mode(self.web_view)
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
if event == WebKit2.LoadEvent.FINISHED:
|
||||
self.loading = False
|
||||
if self.web_view_pending_html:
|
||||
self.__show(html=self.web_view_pending_html, step=Step.LOAD_WEBVIEW)
|
||||
self.web_view_pending_html = None
|
||||
else:
|
||||
self.__show(step=Step.RENDER)
|
||||
|
||||
def on_text_view_scrolled(self, _text_view, scale):
|
||||
if self.shown and not math.isclose(scale, self.web_view.get_scroll_scale(), rel_tol=1e-4):
|
||||
self.web_view.set_scroll_scale(scale)
|
||||
|
||||
def on_web_view_scrolled(self, _web_view, scale):
|
||||
if self.shown and self.text_view.get_mapped() and \
|
||||
not math.isclose(scale, self.text_view.get_scroll_scale(), rel_tol=1e-4):
|
||||
self.text_view.set_scroll_scale(scale)
|
||||
|
||||
@staticmethod
|
||||
def on_click_link(web_view, decision, _decision_type):
|
||||
if web_view.get_uri().startswith(("http://", "https://", "www.")):
|
||||
webbrowser.open(web_view.get_uri())
|
||||
decision.ignore()
|
||||
return True
|
|
@ -0,0 +1,136 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from gi.repository import Gtk, Gio, GLib
|
||||
|
||||
from apostrophe import headerbars
|
||||
from apostrophe.settings import Settings
|
||||
from apostrophe.styled_window import StyledWindow
|
||||
|
||||
|
||||
class PreviewRenderer:
|
||||
"""Renders the preview according to the user selected mode."""
|
||||
|
||||
# Must match the order/index defined in gschema.xml
|
||||
FULL_WIDTH = 0
|
||||
HALF_WIDTH = 1
|
||||
HALF_HEIGHT = 2
|
||||
WINDOWED = 3
|
||||
|
||||
def __init__(
|
||||
self, main_window, content, editor, text_view):
|
||||
self.main_window = main_window
|
||||
self.main_window.connect("delete-event", self.on_window_closed)
|
||||
self.content = content
|
||||
self.editor = editor
|
||||
self.text_view = text_view
|
||||
|
||||
self.settings = Settings.new()
|
||||
self.popover = None
|
||||
self.window = None
|
||||
self.headerbar = None
|
||||
|
||||
self.mode = self.settings.get_enum("preview-mode")
|
||||
self.update_mode()
|
||||
|
||||
def show(self, web_view):
|
||||
"""Show the preview, depending on the currently selected mode."""
|
||||
|
||||
# Windowed preview: create a window and show the preview in it.
|
||||
if self.mode == self.WINDOWED:
|
||||
# Create transient window of the main window.
|
||||
self.window = StyledWindow(application=self.main_window.get_application())
|
||||
self.window.connect("delete-event", self.on_window_closed)
|
||||
|
||||
# Create a custom header bar and move the mode button there.
|
||||
headerbar = headerbars.PreviewHeaderbar()
|
||||
self.headerbar = headerbar.hb
|
||||
self.headerbar.set_title(_("Preview"))
|
||||
self.window.set_titlebar(headerbar.hb_container)
|
||||
|
||||
# Position it next to the main window.
|
||||
width, height = self.main_window.get_size()
|
||||
self.window.resize(width, height)
|
||||
x, y = self.main_window.get_position()
|
||||
if x is not None and y is not None:
|
||||
self.main_window.move(x, y)
|
||||
self.window.move(x + width + 16, y)
|
||||
|
||||
# Add webview and show.
|
||||
self.window.add(web_view)
|
||||
self.window.show()
|
||||
|
||||
else:
|
||||
self.content.pack_start(web_view, True, True, 0)
|
||||
|
||||
# Full-width preview: swap editor with preview.
|
||||
if self.mode == self.FULL_WIDTH:
|
||||
self.content.remove(self.editor)
|
||||
|
||||
# Half-width preview: set horizontal orientation and add the preview.
|
||||
# Ask for a minimum width that respects the editor's minimum requirements.
|
||||
elif self.mode == self.HALF_WIDTH:
|
||||
self.content.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.content.set_size_request(self.text_view.get_min_width() * 2, -1)
|
||||
|
||||
# Half-height preview: set vertical orientation and add the preview.
|
||||
# Ask for a minimum height that provides a comfortable experience.
|
||||
elif self.mode == self.HALF_HEIGHT:
|
||||
self.content.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.content.set_size_request(-1, 768)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(self.mode))
|
||||
|
||||
web_view.show()
|
||||
|
||||
def hide(self, web_view):
|
||||
"""Hide the preview, depending on the currently selected mode."""
|
||||
|
||||
# Windowed preview: remove preview and destroy window.
|
||||
if self.mode == self.WINDOWED:
|
||||
self.main_window.present()
|
||||
self.headerbar = None
|
||||
self.window.remove(web_view)
|
||||
self.window.destroy()
|
||||
self.window = None
|
||||
|
||||
else:
|
||||
self.content.remove(web_view)
|
||||
|
||||
# Full-width preview: swap preview with editor.
|
||||
if self.mode == self.FULL_WIDTH:
|
||||
self.content.add(self.editor)
|
||||
|
||||
# Half-width/height previews: remove preview and reset size requirements.
|
||||
elif self.mode == self.HALF_WIDTH or self.mode == self.HALF_HEIGHT:
|
||||
self.content.set_size_request(-1, -1)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(self.mode))
|
||||
|
||||
def update_mode(self, web_view=None):
|
||||
"""Update preview mode, adjusting the mode button and the preview itself."""
|
||||
|
||||
mode = self.settings.get_enum("preview-mode")
|
||||
if web_view and mode != self.mode:
|
||||
self.hide(web_view)
|
||||
self.mode = mode
|
||||
self.show(web_view)
|
||||
else:
|
||||
self.mode = mode
|
||||
|
||||
def on_window_closed(self, window, _event):
|
||||
preview_action = window.get_application().lookup_action("preview")
|
||||
preview_action.change_state(GLib.Variant.new_boolean(False))
|
||||
|
||||
def get_text_for_preview_mode(self, mode):
|
||||
if mode == self.FULL_WIDTH:
|
||||
return _("Full-Width")
|
||||
elif mode == self.HALF_WIDTH:
|
||||
return _("Half-Width")
|
||||
elif mode == self.HALF_HEIGHT:
|
||||
return _("Half-Height")
|
||||
elif mode == self.WINDOWED:
|
||||
return _("Windowed")
|
||||
else:
|
||||
raise ValueError("Unknown preview mode {}".format(mode))
|
|
@ -0,0 +1,146 @@
|
|||
import webbrowser
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import WebKit2, GLib, GObject
|
||||
|
||||
|
||||
class PreviewWebView(WebKit2.WebView):
|
||||
"""A WebView that provides read/write access to scroll.
|
||||
|
||||
It does so using JavaScript, by continuously monitoring it while loaded.
|
||||
The alternative is using a WebExtension and C-bindings (see reference), but that is more
|
||||
complicated implementation-wise, as well as build-wise until we start building with Meson.
|
||||
|
||||
Reference: https://github.com/aperezdc/webkit2gtk-python-webextension-example
|
||||
"""
|
||||
|
||||
SYNC_SCROLL_SCALE_JS = """
|
||||
scale = {:.16f};
|
||||
write = {};
|
||||
|
||||
// Configure MathJax.
|
||||
if (typeof hasMathJax === "undefined") {{
|
||||
hasMathJax = typeof MathJax !== "undefined";
|
||||
if (hasMathJax) {{
|
||||
MathJax.Hub.Config({{ messageStyle: "none" }});
|
||||
}}
|
||||
}}
|
||||
|
||||
// Figure out if scrollable and rendered.
|
||||
e = document.documentElement;
|
||||
canScroll = e.scrollHeight > e.clientHeight;
|
||||
wasRendered = typeof isRendered !== "undefined" && isRendered;
|
||||
isRendered = wasRendered ||
|
||||
!hasMathJax ||
|
||||
MathJax.Hub.queue.running == 0 && MathJax.Hub.queue.pending == 0;
|
||||
|
||||
// Write the current scroll if instructed or if it was just rendered.
|
||||
if (canScroll && (write || isRendered && !wasRendered)) {{
|
||||
e.scrollTop = (e.scrollHeight - e.clientHeight) * scale;
|
||||
}}
|
||||
|
||||
// Return the current scroll if scrollable and rendered, or -1.
|
||||
if (canScroll && isRendered) {{
|
||||
e.scrollTop / (e.scrollHeight - e.clientHeight);
|
||||
}} else {{
|
||||
-1;
|
||||
}}
|
||||
""".strip()
|
||||
|
||||
__gsignals__ = {
|
||||
"scroll-scale-changed": (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.connect("size-allocate", self.on_size_allocate)
|
||||
self.connect("decide-policy", self.on_decide_policy)
|
||||
self.connect("load-changed", self.on_load_changed)
|
||||
self.connect("load-failed", self.on_load_failed)
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
self.props.expand = True
|
||||
|
||||
self.scroll_scale = -1
|
||||
|
||||
self.state_loaded = False
|
||||
self.state_load_failed = False
|
||||
self.state_discard_read = False
|
||||
self.state_dirty = False
|
||||
self.state_waiting = False
|
||||
|
||||
self.timeout_id = None
|
||||
|
||||
def can_scroll(self):
|
||||
return self.scroll_scale != -1
|
||||
|
||||
def get_scroll_scale(self):
|
||||
return self.scroll_scale
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
self.state_dirty = scale != self.scroll_scale
|
||||
self.scroll_scale = scale
|
||||
self.state_loop()
|
||||
|
||||
def on_size_allocate(self, *_):
|
||||
self.set_scroll_scale(self.scroll_scale)
|
||||
|
||||
def on_decide_policy(self, _web_view, decision, decision_type):
|
||||
if decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION and \
|
||||
decision.get_navigation_action().is_user_gesture():
|
||||
webbrowser.open(decision.get_request().get_uri())
|
||||
decision.ignore() # Do not follow the link in the WebView
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_load_changed(self, _web_view, event):
|
||||
self.state_loaded = event >= WebKit2.LoadEvent.COMMITTED and not self.state_load_failed
|
||||
self.state_load_failed = False
|
||||
self.state_discard_read = event == WebKit2.LoadEvent.STARTED and self.state_waiting
|
||||
self.state_dirty = True
|
||||
self.state_loop()
|
||||
|
||||
def on_load_failed(self, _web_view, _event):
|
||||
self.state_loaded = False
|
||||
self.state_load_failed = True
|
||||
self.state_loop()
|
||||
|
||||
def on_destroy(self, _widget):
|
||||
self.state_loaded = False
|
||||
self.state_loop()
|
||||
|
||||
def sync_scroll_scale(self, scroll_scale, write):
|
||||
self.state_waiting = True
|
||||
self.run_javascript(
|
||||
self.SYNC_SCROLL_SCALE_JS.format(scroll_scale, "true" if write else "false"),
|
||||
None, self.finish_sync_scroll_scale)
|
||||
|
||||
def finish_sync_scroll_scale(self, _web_view, result):
|
||||
self.state_waiting = False
|
||||
result = self.run_javascript_finish(result)
|
||||
self.state_loop(result.get_js_value().to_double())
|
||||
|
||||
def state_loop(self, scroll_scale=None, delay=16): # 16ms ~ 60hz
|
||||
# Remove any pending callbacks
|
||||
if self.timeout_id:
|
||||
GLib.source_remove(self.timeout_id)
|
||||
self.timeout_id = None
|
||||
|
||||
# Set scroll scale if specified, and the state is not dirty
|
||||
if not self.state_discard_read and scroll_scale not in (None, self.scroll_scale):
|
||||
self.scroll_scale = scroll_scale
|
||||
if self.scroll_scale != -1:
|
||||
self.emit("scroll-scale-changed", self.scroll_scale)
|
||||
self.state_discard_read = False
|
||||
|
||||
# Handle the current state
|
||||
if not self.state_loaded or self.state_load_failed or self.state_waiting:
|
||||
return
|
||||
elif self.state_dirty or delay == 0:
|
||||
self.sync_scroll_scale(self.scroll_scale, self.state_dirty)
|
||||
self.state_dirty = False
|
||||
else:
|
||||
self.timeout_id = GLib.timeout_add(delay, self.state_loop, None, 0)
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
# Copyright (C) 2019, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
#
|
||||
# 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
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2012, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
# Copyright (C) 2012, Carlos Jenkins <carlos@jenkins.co.cr>
|
||||
# Copyright (C) 2019, Maximilian Köhl <linuxmaxi@googlemail.com>
|
||||
# Copyright (C) 2019, 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
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -19,46 +19,48 @@ import re
|
|||
|
||||
import gi
|
||||
|
||||
from apostrophe.helpers import user_action
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gdk
|
||||
|
||||
# from plugins import plugins
|
||||
|
||||
LOGGER = logging.getLogger('uberwriter')
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
|
||||
class SearchAndReplace:
|
||||
"""
|
||||
Adds (regex) search and replace functionality to
|
||||
uberwriter
|
||||
apostrophe
|
||||
"""
|
||||
|
||||
def __init__(self, parentwindow, textview):
|
||||
def __init__(self, parentwindow, textview, builder):
|
||||
self.parentwindow = parentwindow
|
||||
self.textview = textview
|
||||
self.textbuffer = textview.get_buffer()
|
||||
|
||||
self.box = parentwindow.builder.get_object("searchbar_placeholder")
|
||||
self.box = builder.get_object("searchbar_placeholder")
|
||||
self.box.set_reveal_child(False)
|
||||
self.searchbar = parentwindow.builder.get_object("searchbar")
|
||||
self.searchentry = parentwindow.builder.get_object("searchentrybox")
|
||||
self.searchbar = builder.get_object("searchbar")
|
||||
self.searchentry = builder.get_object("searchentrybox")
|
||||
self.searchentry.connect('changed', self.search)
|
||||
self.searchentry.connect('activate', self.scrolltonext)
|
||||
self.searchentry.connect('key-press-event', self.key_pressed)
|
||||
|
||||
self.open_replace_button = parentwindow.builder.get_object("replace")
|
||||
self.open_replace_button = builder.get_object("replace")
|
||||
self.open_replace_button.connect("toggled", self.toggle_replace)
|
||||
|
||||
self.nextbutton = parentwindow.builder.get_object("next_result")
|
||||
self.prevbutton = parentwindow.builder.get_object("previous_result")
|
||||
self.regexbutton = parentwindow.builder.get_object("regex")
|
||||
self.casesensitivebutton = parentwindow.builder.get_object("case_sensitive")
|
||||
self.nextbutton = builder.get_object("next_result")
|
||||
self.prevbutton = builder.get_object("previous_result")
|
||||
self.regexbutton = builder.get_object("regex")
|
||||
self.casesensitivebutton = builder.get_object("case_sensitive")
|
||||
|
||||
self.replacebox = parentwindow.builder.get_object("replace_placeholder")
|
||||
self.replacebox = builder.get_object("replace_placeholder")
|
||||
self.replacebox.set_reveal_child(False)
|
||||
self.replace_one_button = parentwindow.builder.get_object("replace_one")
|
||||
self.replace_all_button = parentwindow.builder.get_object("replace_all")
|
||||
self.replaceentry = parentwindow.builder.get_object("replaceentrybox")
|
||||
self.replace_one_button = builder.get_object("replace_one")
|
||||
self.replace_all_button = builder.get_object("replace_all")
|
||||
self.replaceentry = builder.get_object("replaceentrybox")
|
||||
|
||||
self.replace_all_button.connect('clicked', self.replace_all)
|
||||
self.replace_one_button.connect('clicked', self.replace_clicked)
|
||||
|
@ -80,11 +82,10 @@ class SearchAndReplace:
|
|||
"""
|
||||
self.replacebox.set_reveal_child(widget.get_active())
|
||||
|
||||
# TODO: refactorize!
|
||||
def key_pressed(self, _widget, event, _data=None):
|
||||
"""hide the search and replace content box when ESC is pressed
|
||||
"""
|
||||
if event.keyval in [Gdk.KEY_Escape]:
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.hide()
|
||||
|
||||
def focused_texteditor(self, _widget, _data=None):
|
||||
|
@ -92,14 +93,19 @@ class SearchAndReplace:
|
|||
"""
|
||||
self.hide()
|
||||
|
||||
def toggle_search(self, _widget=None, _data=None):
|
||||
def toggle_search(self, replace=False):
|
||||
"""
|
||||
show search box
|
||||
toggle search box
|
||||
"""
|
||||
if self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False:
|
||||
search_hidden = self.textview.get_mapped() and (
|
||||
self.box.get_reveal_child() is False or self.searchbar.get_search_mode() is False)
|
||||
replace_hidden = not self.open_replace_button.get_active()
|
||||
if search_hidden or (replace and replace_hidden):
|
||||
self.searchbar.set_search_mode(True)
|
||||
self.box.set_reveal_child(True)
|
||||
self.searchentry.grab_focus()
|
||||
if replace:
|
||||
self.open_replace_button.set_active(True)
|
||||
else:
|
||||
self.hide()
|
||||
self.open_replace_button.set_active(False)
|
||||
|
@ -146,8 +152,15 @@ class SearchAndReplace:
|
|||
self.active = index % len(self.matches)
|
||||
|
||||
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])
|
||||
|
||||
# create a mark at the start of the coincidence to scroll to it
|
||||
mark = self.textbuffer.create_mark(None, start_iter, False)
|
||||
self.textview.scroller.scroll_to_mark(mark, center=True)
|
||||
|
||||
# select coincidence
|
||||
self.textbuffer.select_range(start_iter, end_iter)
|
||||
|
||||
def hide(self):
|
||||
|
@ -161,18 +174,20 @@ class SearchAndReplace:
|
|||
self.replace(self.active)
|
||||
|
||||
def replace_all(self, _widget=None, _data=None):
|
||||
for match in reversed(self.matches):
|
||||
self.do_replace(match)
|
||||
with user_action(self.textbuffer):
|
||||
for match in reversed(self.matches):
|
||||
self.__do_replace(match)
|
||||
self.search(scroll=False)
|
||||
|
||||
def replace(self, searchindex, _inloop=False):
|
||||
self.do_replace(self.matches[searchindex])
|
||||
with user_action(self.textbuffer):
|
||||
self.__do_replace(self.matches[searchindex])
|
||||
active = self.active
|
||||
self.search(scroll=False)
|
||||
self.active = active
|
||||
self.scrollto(self.active)
|
||||
|
||||
def do_replace(self, match):
|
||||
def __do_replace(self, match):
|
||||
start_iter = self.textbuffer.get_iter_at_offset(match[0])
|
||||
end_iter = self.textbuffer.get_iter_at_offset(match[1])
|
||||
self.textbuffer.delete(start_iter, end_iter)
|
|
@ -1,5 +1,5 @@
|
|||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -18,7 +18,7 @@ from gi.repository import Gio
|
|||
class Settings(Gio.Settings):
|
||||
|
||||
"""
|
||||
UberWriter Settings
|
||||
Apostrophe Settings
|
||||
"""
|
||||
|
||||
def __init__(self):
|
|
@ -1,6 +1,6 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
|
@ -23,7 +23,7 @@ from gi.repository import Gtk, Gdk
|
|||
# from plugins import plugins
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('uberwriter')
|
||||
logger = logging.getLogger('apostrophe')
|
||||
|
||||
class Shelve():
|
||||
"""
|
|
@ -0,0 +1,115 @@
|
|||
import re
|
||||
from multiprocessing import Process, Pipe
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from apostrophe.markup_regex import ITALIC_ASTERISK, ITALIC_UNDERSCORE, BOLD_ITALIC, BOLD, STRIKETHROUGH, IMAGE, LINK, LINK_ALT,\
|
||||
HORIZONTAL_RULE, LIST, MATH, TABLE, CODE_BLOCK, HEADER_UNDER, HEADER, BLOCK_QUOTE, ORDERED_LIST, \
|
||||
FOOTNOTE_ID, FOOTNOTE
|
||||
|
||||
|
||||
class StatsCounter:
|
||||
"""Counts characters, words, sentences and read time using a worker process."""
|
||||
|
||||
# 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 at least 2 newlines.
|
||||
PARAGRAPHS = re.compile(r"[^\n]+(\n{2,}|$)")
|
||||
|
||||
# List of regexp whose matches should be replaced by their "text" group. Order is important.
|
||||
MARKUP_REGEXP_REPLACE = (
|
||||
BOLD_ITALIC, ITALIC_ASTERISK, ITALIC_UNDERSCORE, BOLD, STRIKETHROUGH, IMAGE, LINK, LINK_ALT, LIST, ORDERED_LIST,
|
||||
BLOCK_QUOTE, HEADER, HEADER_UNDER, CODE_BLOCK, TABLE, MATH, FOOTNOTE_ID, FOOTNOTE
|
||||
)
|
||||
|
||||
# List of regexp whose matches should be removed. Order is important.
|
||||
MARKUP_REGEXP_REMOVE = (
|
||||
HORIZONTAL_RULE,
|
||||
)
|
||||
|
||||
def __init__(self, callback):
|
||||
super().__init__()
|
||||
|
||||
# Worker process to handle counting.
|
||||
self.counting = False
|
||||
self.count_pending_text = None
|
||||
self.parent_conn, child_conn = Pipe()
|
||||
Process(target=self.do_count, args=(child_conn,), daemon=True).start()
|
||||
GLib.io_add_watch(
|
||||
self.parent_conn.fileno(), GLib.PRIORITY_LOW, GLib.IO_IN, self.on_counted, callback)
|
||||
|
||||
def count(self, text):
|
||||
"""Count stats for text.
|
||||
|
||||
In case counting is already running, it will re-count once it finishes. This ensure that
|
||||
the pipe doesn't fill (and block) if multiple requests are made in quick succession."""
|
||||
|
||||
if not self.counting:
|
||||
self.counting = True
|
||||
self.count_pending_text = None
|
||||
self.parent_conn.send(text)
|
||||
else:
|
||||
self.count_pending_text = text
|
||||
|
||||
def do_count(self, child_conn):
|
||||
"""Counts stats in a worker process.
|
||||
|
||||
The result is in the format: (characters, words, sentences, (hours, minutes, seconds))"""
|
||||
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
text = child_conn.recv()
|
||||
if not child_conn.poll():
|
||||
break
|
||||
except EOFError:
|
||||
child_conn.close()
|
||||
return
|
||||
|
||||
for regexp in self.MARKUP_REGEXP_REPLACE:
|
||||
text = re.sub(regexp, r"\g<text>", text)
|
||||
for regexp in self.MARKUP_REGEXP_REMOVE:
|
||||
text = re.sub(regexp, "", text)
|
||||
|
||||
character_count = len(re.findall(self.CHARACTERS, text))
|
||||
|
||||
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))
|
||||
|
||||
child_conn.send(
|
||||
(character_count, word_count, sentence_count, paragraph_count, read_time))
|
||||
|
||||
def on_counted(self, _source, _condition, callback):
|
||||
"""Reads the counting result from the pipe and triggers any pending count."""
|
||||
|
||||
self.counting = False
|
||||
if self.count_pending_text is not None:
|
||||
self.count(self.count_pending_text) # self.count clears the pending text.
|
||||
|
||||
try:
|
||||
if self.parent_conn.poll():
|
||||
callback(self.parent_conn.recv())
|
||||
return True
|
||||
except EOFError:
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""Stops the worker process. StatsCounter shouldn't be used after this."""
|
||||
|
||||
self.parent_conn.close()
|
|
@ -0,0 +1,96 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from gi.repository import GLib, Gio, Gtk
|
||||
|
||||
from apostrophe.settings import Settings
|
||||
from apostrophe.stats_counter import StatsCounter
|
||||
|
||||
|
||||
class StatsHandler:
|
||||
"""Shows a default statistic on the stats button, and allows the user to toggle which one."""
|
||||
|
||||
# Must match the order/index defined in gschema.xml
|
||||
CHARACTERS = 0
|
||||
WORDS = 1
|
||||
SENTENCES = 2
|
||||
PARAGRAPHS = 3
|
||||
READ_TIME = 4
|
||||
|
||||
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.stats_counter = StatsCounter(self.update_stats)
|
||||
|
||||
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))
|
||||
|
||||
def get_text_for_stat(self, stat):
|
||||
if stat == self.CHARACTERS:
|
||||
return _("{:n} Characters").format(self.characters)
|
||||
elif stat == self.WORDS:
|
||||
return _("{:n} Words").format(self.words)
|
||||
elif stat == self.SENTENCES:
|
||||
return _("{:n} Sentences").format(self.sentences)
|
||||
elif stat == self.PARAGRAPHS:
|
||||
return _("{:n} Paragraphs").format(self.paragraphs)
|
||||
elif stat == self.READ_TIME:
|
||||
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()
|
|
@ -0,0 +1,22 @@
|
|||
import gi
|
||||
|
||||
from apostrophe import helpers
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gio
|
||||
|
||||
|
||||
class StyledWindow(Gtk.ApplicationWindow):
|
||||
"""A window that will redraw itself upon theme changes."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set theme css
|
||||
css_provider_file = Gio.File.new_for_uri(
|
||||
"resource:///de/wolfvollprecht/UberWriter/media/css/gtk/base.css")
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_file(css_provider_file)
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
self.get_screen(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
@ -0,0 +1,304 @@
|
|||
import gi
|
||||
|
||||
from apostrophe.helpers import user_action
|
||||
from apostrophe.inline_preview import InlinePreview
|
||||
from apostrophe.text_view_drag_drop_handler import DragDropHandler, TARGET_URI, TARGET_TEXT
|
||||
from apostrophe.text_view_format_inserter import FormatInserter
|
||||
from apostrophe.text_view_markup_handler import MarkupHandler
|
||||
from apostrophe.text_view_scroller import TextViewScroller
|
||||
from apostrophe.text_view_undo_redo_handler import UndoRedoHandler
|
||||
|
||||
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('apostrophe')
|
||||
|
||||
|
||||
class TextView(Gtk.TextView):
|
||||
"""ApostropheTextView 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 Apostrophe (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, ()),
|
||||
'scroll-scale-changed': (GObject.SIGNAL_RUN_LAST, None, (float,)),
|
||||
}
|
||||
|
||||
font_sizes = [18, 17, 16, 15, 14] # Must match CSS selectors in gtk/base.css
|
||||
|
||||
def __init__(self, line_chars):
|
||||
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('apostrophe-editor')
|
||||
|
||||
self.set_margin_left(8)
|
||||
self.set_margin_right(8)
|
||||
|
||||
# Text sizing
|
||||
self.props.halign = Gtk.Align.FILL
|
||||
self.line_chars = line_chars
|
||||
self.font_size = 16
|
||||
self.get_style_context().add_class('size16')
|
||||
|
||||
# General behavior
|
||||
self.connect('size-allocate', self.on_size_allocate)
|
||||
self.get_buffer().connect('changed', self.on_text_changed)
|
||||
self.get_buffer().connect('paste-done', self.on_paste_done)
|
||||
|
||||
# Spell checking
|
||||
self.spellcheck = True
|
||||
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('begin-user-action', self.undo_redo.on_begin_user_action)
|
||||
self.get_buffer().connect('end-user-action', self.undo_redo.on_end_user_action)
|
||||
self.get_buffer().connect('insert-text', self.undo_redo.on_insert_text)
|
||||
self.get_buffer().connect('delete-range', self.undo_redo.on_delete_range)
|
||||
self.connect('undo', self.undo_redo.undo)
|
||||
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)
|
||||
self.connect('destroy', self.markup.stop)
|
||||
|
||||
# Preview popover
|
||||
self.preview_popover = InlinePreview(self)
|
||||
|
||||
# Drag and drop
|
||||
self.drag_drop = DragDropHandler(self, TARGET_URI, TARGET_TEXT)
|
||||
|
||||
# Scrolling
|
||||
self.scroller = None
|
||||
self.connect('parent-set', self.on_parent_set)
|
||||
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)
|
||||
|
||||
# While resizing the TextView, there is unwanted scroll upwards if a top margin is present.
|
||||
# When a size allocation is detected, this variable will hold the scroll to re-set until the
|
||||
# UI is idle again.
|
||||
# TODO: Find a better way to handle unwanted scroll.
|
||||
self.frozen_scroll_scale = None
|
||||
|
||||
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):
|
||||
"""Set text and clear undo history"""
|
||||
|
||||
text_buffer = self.get_buffer()
|
||||
with user_action(text_buffer):
|
||||
text_buffer.set_text(text)
|
||||
self.undo_redo.clear()
|
||||
|
||||
def can_scroll(self):
|
||||
return self.scroller.can_scroll()
|
||||
|
||||
def get_scroll_scale(self):
|
||||
return self.scroller.get_scroll_scale() if self.scroller else 0
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
if self.scroller:
|
||||
self.scroller.set_scroll_scale(scale)
|
||||
|
||||
def on_size_allocate(self, *_):
|
||||
self.update_horizontal_margin()
|
||||
self.markup.update_margins_indents()
|
||||
self.queue_draw()
|
||||
|
||||
# TODO: Find a better way to handle unwanted scroll on resize.
|
||||
self.frozen_scroll_scale = self.get_scroll_scale()
|
||||
GLib.idle_add(self.unfreeze_scroll_scale)
|
||||
|
||||
def on_text_changed(self, *_):
|
||||
self.markup.apply()
|
||||
|
||||
def on_paste_done(self, *_):
|
||||
self.smooth_scroll_to()
|
||||
|
||||
def on_parent_set(self, *_):
|
||||
parent = self.get_parent()
|
||||
if parent:
|
||||
parent.set_size_request(self.get_min_width(), 500)
|
||||
self.scroller = TextViewScroller(self, parent)
|
||||
parent.get_vadjustment().connect("changed", self.on_vadjustment_changed)
|
||||
parent.get_vadjustment().connect("value-changed", self.on_vadjustment_changed)
|
||||
else:
|
||||
self.scroller = None
|
||||
|
||||
def on_mark_set(self, _text_buffer, _location, mark, _data=None):
|
||||
if mark.get_name() == 'selection_bound':
|
||||
self.markup.apply()
|
||||
if not self.get_buffer().get_has_selection():
|
||||
self.smooth_scroll_to(mark)
|
||||
elif mark.get_name() == 'gtk_drag_target':
|
||||
self.smooth_scroll_to(mark)
|
||||
return True
|
||||
|
||||
def on_button_release_event(self, _widget, _event):
|
||||
if self.focus_mode:
|
||||
self.markup.apply()
|
||||
return False
|
||||
|
||||
def on_vadjustment_changed(self, *_):
|
||||
if self.frozen_scroll_scale is not None:
|
||||
self.set_scroll_scale(self.frozen_scroll_scale)
|
||||
elif self.can_scroll():
|
||||
self.emit("scroll-scale-changed", self.get_scroll_scale())
|
||||
|
||||
def unfreeze_scroll_scale(self):
|
||||
self.frozen_scroll_scale = None
|
||||
self.queue_draw()
|
||||
|
||||
def set_focus_mode(self, focus_mode, hb_height):
|
||||
"""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.update_vertical_margin(hb_size=hb_height)
|
||||
self.markup.apply()
|
||||
self.smooth_scroll_to()
|
||||
self.set_spellcheck(self.spellcheck)
|
||||
|
||||
def set_spellcheck(self, spellcheck):
|
||||
self.spellcheck = spellcheck
|
||||
self.gspell_view.set_inline_spell_checking(self.spellcheck and not self.focus_mode)
|
||||
|
||||
def update_horizontal_margin(self):
|
||||
width = self.get_allocation().width
|
||||
|
||||
# Ensure the appropriate font size is being used
|
||||
for font_size in self.font_sizes:
|
||||
if width >= self.get_min_width(font_size) or font_size == self.font_sizes[-1]:
|
||||
if font_size != self.font_size:
|
||||
self.font_size = font_size
|
||||
for fs in self.font_sizes:
|
||||
self.get_style_context().remove_class("size{}".format(fs))
|
||||
self.get_style_context().add_class("size{}".format(font_size))
|
||||
break
|
||||
|
||||
# Apply margin with the remaining space to allow for markup
|
||||
line_width = (self.line_chars + 1) * int(self.get_char_width(self.font_size)) - 1
|
||||
horizontal_margin = (width - line_width) / 2
|
||||
self.props.left_margin = horizontal_margin
|
||||
self.props.right_margin = horizontal_margin
|
||||
|
||||
def update_vertical_margin(self, top_margin=0, hb_size=0):
|
||||
if self.focus_mode:
|
||||
height = self.get_allocation().height + top_margin + hb_size
|
||||
|
||||
self.props.top_margin = height / 2 + top_margin
|
||||
self.props.bottom_margin = height / 2 - top_margin
|
||||
else:
|
||||
self.props.top_margin = 80 + top_margin
|
||||
self.props.bottom_margin = 64
|
||||
|
||||
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 clear(self):
|
||||
"""Clear text and undo history"""
|
||||
|
||||
self.set_text('')
|
||||
|
||||
def smooth_scroll_to(self, mark=None):
|
||||
"""Scrolls if needed to ensure mark is visible.
|
||||
|
||||
If mark is unspecified, the cursor is used."""
|
||||
|
||||
if self.scroller is None:
|
||||
return
|
||||
if mark is None:
|
||||
mark = self.get_buffer().get_insert()
|
||||
GLib.idle_add(self.scroller.smooth_scroll_to_mark, mark, self.focus_mode)
|
||||
|
||||
def get_min_width(self, font_size=None):
|
||||
"""Returns the minimum width of this text view."""
|
||||
|
||||
if font_size is None:
|
||||
font_size = self.font_sizes[-1]
|
||||
return (self.line_chars + self.get_pad_chars(font_size) + 1) \
|
||||
* self.get_char_width(font_size) - 1
|
||||
|
||||
def get_pad_chars(self, font_size):
|
||||
"""Returns the amount of character padding for font_size.
|
||||
|
||||
Markup can use up to 7 in normal conditions."""
|
||||
|
||||
return 8 * (1 + font_size - self.font_sizes[-1])
|
||||
|
||||
@staticmethod
|
||||
def get_char_width(font_size):
|
||||
"""Returns the font width for a given size. Note: specific to Fira Mono!"""
|
||||
|
||||
return font_size * 1 / 1.6
|
||||
|
||||
def _on_key_press_event(self, _widget, event):
|
||||
if self.hemingway_mode:
|
||||
return event.keyval == Gdk.KEY_BackSpace or event.keyval == Gdk.KEY_Delete
|
||||
|
||||
if event.state & Gdk.ModifierType.SHIFT_MASK == Gdk.ModifierType.SHIFT_MASK \
|
||||
and event.keyval == Gdk.KEY_ISO_Left_Tab: # Capure Shift-Tab
|
||||
self._on_shift_tab()
|
||||
return True
|
||||
|
||||
def _on_shift_tab(self):
|
||||
"""Delete last character if it is a tab"""
|
||||
text_buffer = self.get_buffer()
|
||||
pen_iter = text_buffer.get_end_iter()
|
||||
pen_iter.backward_char()
|
||||
end_iter = text_buffer.get_end_iter()
|
||||
|
||||
if pen_iter.get_char() == "\t":
|
||||
with user_action(text_buffer):
|
||||
text_buffer.delete(pen_iter, end_iter)
|
|
@ -0,0 +1,92 @@
|
|||
import mimetypes
|
||||
import urllib
|
||||
from gettext import gettext as _
|
||||
from os.path import basename
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
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.settings = Settings.new()
|
||||
|
||||
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:
|
||||
name = basename(urllib.parse.unquote_plus(uri))
|
||||
mime = mimetypes.guess_type(uri)
|
||||
|
||||
if mime[0] is not None and mime[0].startswith('image/'):
|
||||
basepath = self.settings.get_string("open-file-path")
|
||||
basepath = urllib.parse.quote(basepath)
|
||||
|
||||
if uri.startswith("file://"):
|
||||
uri = uri[7:]
|
||||
|
||||
# for handling local URIs we need to substract the basepath
|
||||
# except when it is "/" (document not saved)
|
||||
if uri.startswith(basepath) and basepath != "/":
|
||||
uri = uri[len(basepath)+1:]
|
||||
|
||||
text = "![{}]({})".format(name, uri)
|
||||
limit_left = 2
|
||||
limit_right = len(name)
|
||||
else:
|
||||
text = "[{}]({})".format(name, uri)
|
||||
limit_left = 1
|
||||
limit_right = len(name)
|
||||
|
||||
elif info == TARGET_TEXT:
|
||||
text = data.get_text()
|
||||
|
||||
# delete automatically added DnD text
|
||||
insert_mark = text_buffer.get_insert()
|
||||
cursor_iter_r = text_buffer.get_iter_at_mark(insert_mark)
|
||||
cursor_iter_l = cursor_iter_r.copy()
|
||||
cursor_iter_l.backward_chars(len(data.get_text()))
|
||||
|
||||
text_buffer.delete(cursor_iter_l, cursor_iter_r)
|
||||
|
||||
if text.startswith(("http://", "https://", "www.")):
|
||||
text = "[{}]({})".format(_("web page"), text)
|
||||
limit_left = 1
|
||||
limit_right = len(_("web page"))
|
||||
else:
|
||||
limit_left = 0
|
||||
limit_right = 0
|
||||
|
||||
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)
|
||||
|
||||
Gtk.drag_finish(drag_context, True, True, time)
|
||||
text_view.get_toplevel().present_with_time(time)
|
||||
return False
|
|
@ -1,5 +1,7 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from apostrophe.helpers import user_action
|
||||
|
||||
|
||||
class FormatInserter:
|
||||
"""Manages insertion of formatting.
|
||||
|
@ -25,7 +27,8 @@ class FormatInserter:
|
|||
"""Insert horizontal rule"""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
text_buffer.insert_at_cursor("\n\n---\n")
|
||||
with user_action(text_buffer):
|
||||
text_buffer.insert_at_cursor("\n\n---\n")
|
||||
text_view.scroll_mark_onscreen(text_buffer.get_insert())
|
||||
|
||||
def insert_list_item(self, text_view, _data=None):
|
||||
|
@ -35,12 +38,14 @@ class FormatInserter:
|
|||
if text_buffer.get_has_selection():
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
if start.starts_line():
|
||||
text = text_buffer.get_text(start, end, False)
|
||||
if text.startswith(("- ", "* ", "+ ")):
|
||||
delete_end = start.forward_chars(2)
|
||||
text_buffer.delete(start, delete_end)
|
||||
else:
|
||||
text_buffer.insert(start, "- ")
|
||||
with user_action(text_buffer):
|
||||
text = text_buffer.get_text(start, end, False)
|
||||
if text.startswith(("- ", "* ", "+ ")):
|
||||
delete_end = start.copy()
|
||||
delete_end.forward_chars(2)
|
||||
text_buffer.delete(start, delete_end)
|
||||
else:
|
||||
text_buffer.insert(start, "- ")
|
||||
else:
|
||||
helptext = _("Item")
|
||||
text_length = len(helptext)
|
||||
|
@ -53,25 +58,25 @@ class FormatInserter:
|
|||
text = text_buffer.get_text(cursor_iter, start_ext, False)
|
||||
lines = text.splitlines()
|
||||
|
||||
for line in reversed(lines):
|
||||
if line and line.startswith(("- ", "* ", "+ ")):
|
||||
if cursor_iter.starts_line():
|
||||
text_buffer.insert_at_cursor(line[:2] + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor(
|
||||
"\n" + line[:2] + helptext)
|
||||
break
|
||||
else:
|
||||
if not lines[-1] and not lines[-2]:
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
elif not lines[-1]:
|
||||
with user_action(text_buffer):
|
||||
for line in reversed(lines):
|
||||
if line and line.startswith(("- ", "* ", "+ ")):
|
||||
if cursor_iter.starts_line():
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
text_buffer.insert_at_cursor(line[:2] + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n- " + helptext)
|
||||
text_buffer.insert_at_cursor("\n" + line[:2] + helptext)
|
||||
break
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n\n- " + helptext)
|
||||
break
|
||||
if not lines[-1] and not lines[-2]:
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
elif not lines[-1]:
|
||||
if cursor_iter.starts_line():
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n- " + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n\n- " + helptext)
|
||||
break
|
||||
|
||||
self.__select_text(text_view, 0, text_length)
|
||||
|
||||
|
@ -83,57 +88,60 @@ class FormatInserter:
|
|||
"""Insert header or mark a selection as a list header"""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
if text_buffer.get_has_selection():
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
text = text_buffer.get_text(start, end, False)
|
||||
text_buffer.delete(start, end)
|
||||
else:
|
||||
text = _("Header")
|
||||
with user_action(text_buffer):
|
||||
if text_buffer.get_has_selection():
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
text = text_buffer.get_text(start, end, False)
|
||||
text_buffer.delete(start, end)
|
||||
else:
|
||||
text = _("Header")
|
||||
|
||||
text_buffer.insert_at_cursor("#" + " " + text)
|
||||
|
||||
text_buffer.insert_at_cursor("#" + " " + text)
|
||||
self.__select_text(text_view, 0, len(text))
|
||||
|
||||
@staticmethod
|
||||
def __wrap(text_view, wrap, helptext=""):
|
||||
"""Inserts wrap format to the selected text (helper text when nothing selected)"""
|
||||
text_buffer = text_view.get_buffer()
|
||||
if text_buffer.get_has_selection():
|
||||
# Find current highlighting
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
moved = False
|
||||
if (start.get_offset() >= len(wrap) and
|
||||
end.get_offset() <= text_buffer.get_char_count() - len(wrap)):
|
||||
moved = True
|
||||
ext_start = start.copy()
|
||||
ext_start.backward_chars(len(wrap))
|
||||
ext_end = end.copy()
|
||||
ext_end.forward_chars(len(wrap))
|
||||
text = text_buffer.get_text(ext_start, ext_end, True)
|
||||
else:
|
||||
text = text_buffer.get_text(start, end, True)
|
||||
with user_action(text_buffer):
|
||||
if text_buffer.get_has_selection():
|
||||
# Find current highlighting
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
moved = False
|
||||
if (start.get_offset() >= len(wrap) and
|
||||
end.get_offset() <= text_buffer.get_char_count() - len(wrap)):
|
||||
moved = True
|
||||
ext_start = start.copy()
|
||||
ext_start.backward_chars(len(wrap))
|
||||
ext_end = end.copy()
|
||||
ext_end.forward_chars(len(wrap))
|
||||
text = text_buffer.get_text(ext_start, ext_end, True)
|
||||
else:
|
||||
text = text_buffer.get_text(start, end, True)
|
||||
|
||||
if moved and text.startswith(wrap) and text.endswith(wrap):
|
||||
text = text[len(wrap):-len(wrap)]
|
||||
new_text = text
|
||||
text_buffer.delete(ext_start, ext_end)
|
||||
move_back = 0
|
||||
else:
|
||||
if moved:
|
||||
if moved and text.startswith(wrap) and text.endswith(wrap):
|
||||
text = text[len(wrap):-len(wrap)]
|
||||
new_text = text.lstrip().rstrip()
|
||||
text = text.replace(new_text, wrap + new_text + wrap)
|
||||
new_text = text
|
||||
text_buffer.delete(ext_start, ext_end)
|
||||
move_back = 0
|
||||
else:
|
||||
if moved:
|
||||
text = text[len(wrap):-len(wrap)]
|
||||
new_text = text.lstrip().rstrip()
|
||||
text = text.replace(new_text, wrap + new_text + wrap)
|
||||
|
||||
text_buffer.delete(start, end)
|
||||
text_buffer.delete(start, end)
|
||||
move_back = len(wrap)
|
||||
|
||||
text_buffer.insert_at_cursor(text)
|
||||
text_length = len(new_text)
|
||||
|
||||
else:
|
||||
text_buffer.insert_at_cursor(wrap + helptext + wrap)
|
||||
text_length = len(helptext)
|
||||
move_back = len(wrap)
|
||||
|
||||
text_buffer.insert_at_cursor(text)
|
||||
text_length = len(new_text)
|
||||
|
||||
else:
|
||||
text_buffer.insert_at_cursor(wrap + helptext + wrap)
|
||||
text_length = len(helptext)
|
||||
move_back = len(wrap)
|
||||
|
||||
cursor_mark = text_buffer.get_insert()
|
||||
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
|
||||
cursor_iter.backward_chars(move_back)
|
|
@ -0,0 +1,386 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
### END LICENSE
|
||||
|
||||
import re
|
||||
from multiprocessing import Pipe, Process
|
||||
|
||||
import gi
|
||||
|
||||
from apostrophe import helpers, markup_regex
|
||||
from apostrophe.markup_regex import STRIKETHROUGH, BOLD_ITALIC, BOLD, ITALIC_ASTERISK, ITALIC_UNDERSCORE, IMAGE, LINK,\
|
||||
LINK_ALT, HORIZONTAL_RULE, LIST, ORDERED_LIST, BLOCK_QUOTE, HEADER, HEADER_UNDER, TABLE, MATH, \
|
||||
CODE
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
from gi.repository import Pango
|
||||
|
||||
|
||||
class MarkupHandler:
|
||||
TAG_NAME_ITALIC = 'italic'
|
||||
TAG_NAME_BOLD = 'bold'
|
||||
TAG_NAME_BOLD_ITALIC = 'bold_italic'
|
||||
TAG_NAME_STRIKETHROUGH = 'strikethrough'
|
||||
TAG_NAME_CENTER = 'center'
|
||||
TAG_NAME_WRAP_NONE = 'wrap_none'
|
||||
TAG_NAME_PLAIN_TEXT = 'plain_text'
|
||||
TAG_NAME_GRAY_TEXT = 'gray_text'
|
||||
TAG_NAME_CODE_TEXT = 'code_text'
|
||||
TAG_NAME_CODE_BLOCK = 'code_block'
|
||||
TAG_NAME_UNFOCUSED_TEXT = 'unfocused_text'
|
||||
TAG_NAME_MARGIN_INDENT = 'margin_indent'
|
||||
|
||||
def __init__(self, text_view):
|
||||
self.text_view = text_view
|
||||
self.text_buffer = text_view.get_buffer()
|
||||
self.marked_up_text = None
|
||||
|
||||
# Tags.
|
||||
buffer = self.text_buffer
|
||||
|
||||
self.tag_italic = buffer.create_tag(self.TAG_NAME_ITALIC,
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.ITALIC)
|
||||
|
||||
self.tag_bold = buffer.create_tag(self.TAG_NAME_BOLD,
|
||||
weight=Pango.Weight.BOLD,
|
||||
style=Pango.Style.NORMAL)
|
||||
|
||||
self.tag_bold_italic = buffer.create_tag(self.TAG_NAME_BOLD_ITALIC,
|
||||
weight=Pango.Weight.BOLD,
|
||||
style=Pango.Style.ITALIC)
|
||||
|
||||
self.tag_strikethrough = buffer.create_tag(self.TAG_NAME_STRIKETHROUGH,
|
||||
strikethrough=True)
|
||||
|
||||
self.tag_center = buffer.create_tag(self.TAG_NAME_CENTER,
|
||||
justification=Gtk.Justification.CENTER)
|
||||
|
||||
self.tag_wrap_none = buffer.create_tag(self.TAG_NAME_WRAP_NONE,
|
||||
wrap_mode=Gtk.WrapMode.NONE,
|
||||
pixels_above_lines=0,
|
||||
pixels_below_lines=0)
|
||||
|
||||
self.tag_plain_text = buffer.create_tag(self.TAG_NAME_PLAIN_TEXT,
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.NORMAL,
|
||||
strikethrough=False,
|
||||
justification=Gtk.Justification.LEFT)
|
||||
|
||||
self.tag_gray_text = buffer.create_tag(self.TAG_NAME_GRAY_TEXT,
|
||||
foreground='gray',
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.NORMAL)
|
||||
|
||||
self.tag_code_text = buffer.create_tag(self.TAG_NAME_CODE_TEXT,
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.NORMAL,
|
||||
strikethrough=False)
|
||||
|
||||
self.tag_code_block = buffer.create_tag(self.TAG_NAME_CODE_BLOCK,
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.NORMAL,
|
||||
strikethrough=False,
|
||||
indent=self.get_margin_indent(0, 1)[1])
|
||||
|
||||
self.tags_markup = {
|
||||
self.TAG_NAME_ITALIC: lambda args: self.tag_italic,
|
||||
self.TAG_NAME_BOLD: lambda args: self.tag_bold,
|
||||
self.TAG_NAME_BOLD_ITALIC: lambda args: self.tag_bold_italic,
|
||||
self.TAG_NAME_STRIKETHROUGH: lambda args: self.tag_strikethrough,
|
||||
self.TAG_NAME_CENTER: lambda args: self.tag_center,
|
||||
self.TAG_NAME_WRAP_NONE: lambda args: self.tag_wrap_none,
|
||||
self.TAG_NAME_PLAIN_TEXT: lambda args: self.tag_plain_text,
|
||||
self.TAG_NAME_GRAY_TEXT: lambda args: self.tag_gray_text,
|
||||
self.TAG_NAME_CODE_TEXT: lambda args: self.tag_code_text,
|
||||
self.TAG_NAME_CODE_BLOCK: lambda args: self.tag_code_block,
|
||||
self.TAG_NAME_MARGIN_INDENT: lambda args: self.get_margin_indent_tag(*args)
|
||||
}
|
||||
|
||||
# Focus mode.
|
||||
self.tag_unfocused_text = buffer.create_tag(self.TAG_NAME_UNFOCUSED_TEXT,
|
||||
foreground='gray',
|
||||
weight=Pango.Weight.NORMAL,
|
||||
style=Pango.Style.NORMAL)
|
||||
|
||||
# Margin and indents.
|
||||
# A baseline margin is set to allow negative offsets for formatting headers, lists, etc.
|
||||
self.tags_margins_indents = {}
|
||||
self.baseline_margin = 0
|
||||
self.char_width = 0
|
||||
self.update_margins_indents()
|
||||
|
||||
# Style.
|
||||
self.on_style_updated()
|
||||
|
||||
# Worker process to handle parsing.
|
||||
self.parsing = False
|
||||
self.apply_pending = False
|
||||
self.parent_conn, child_conn = Pipe()
|
||||
Process(target=self.parse, args=(child_conn,), daemon=True).start()
|
||||
GLib.io_add_watch(
|
||||
self.parent_conn.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.on_parsed)
|
||||
|
||||
def on_style_updated(self, *_):
|
||||
style_context = self.text_view.get_style_context()
|
||||
(found, color) = style_context.lookup_color('code_bg_color')
|
||||
if not found:
|
||||
(_, color) = style_context.lookup_color('background_color')
|
||||
self.tag_code_text.set_property("background", color.to_string())
|
||||
self.tag_code_block.set_property("paragraph-background", color.to_string())
|
||||
|
||||
def apply(self):
|
||||
"""Applies markup, parsing it in a worker process if the text has changed.
|
||||
|
||||
In case parsing is already running, it will re-apply once it finishes. This ensure that
|
||||
the pipe doesn't fill (and block) if multiple requests are made in quick succession."""
|
||||
|
||||
if not self.parsing:
|
||||
self.parsing = True
|
||||
self.apply_pending = False
|
||||
|
||||
text = self.text_buffer.get_slice(
|
||||
self.text_buffer.get_start_iter(), self.text_buffer.get_end_iter(), False)
|
||||
if text != self.marked_up_text:
|
||||
self.parent_conn.send(text)
|
||||
else:
|
||||
self.do_apply(text)
|
||||
else:
|
||||
self.apply_pending = True
|
||||
|
||||
def parse(self, child_conn):
|
||||
"""Parses markup in a worker process."""
|
||||
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
text = child_conn.recv()
|
||||
if not child_conn.poll():
|
||||
break
|
||||
except EOFError:
|
||||
child_conn.close()
|
||||
return
|
||||
|
||||
# List of tuples in the form (tag_name, tag_args, tag_start, tag_end).
|
||||
result = []
|
||||
|
||||
# Find:
|
||||
# - "_italic_" (italic)
|
||||
# - "**bold**" (bold)
|
||||
# - "***bolditalic***" (bold/italic)
|
||||
# - "~~strikethrough~~" (strikethrough)
|
||||
# - "`code`" (colorize)
|
||||
# - "$math$" (colorize)
|
||||
# - "---" table (wrap/pixels)
|
||||
regexps = (
|
||||
(ITALIC_ASTERISK, self.TAG_NAME_ITALIC),
|
||||
(ITALIC_UNDERSCORE, self.TAG_NAME_ITALIC),
|
||||
(BOLD, self.TAG_NAME_BOLD),
|
||||
(BOLD_ITALIC, self.TAG_NAME_BOLD_ITALIC),
|
||||
(STRIKETHROUGH, self.TAG_NAME_STRIKETHROUGH),
|
||||
(CODE, self.TAG_NAME_CODE_TEXT),
|
||||
(MATH, self.TAG_NAME_CODE_TEXT),
|
||||
(TABLE, self.TAG_NAME_WRAP_NONE)
|
||||
)
|
||||
for regexp, tag_name in regexps:
|
||||
matches = re.finditer(regexp, text)
|
||||
for match in matches:
|
||||
result.append((tag_name, (), match.start(), match.end()))
|
||||
|
||||
# Find:
|
||||
# - "[description](url)" (gray out)
|
||||
# - "![description](image_url)" (gray out)
|
||||
regexps = (
|
||||
(LINK, self.TAG_NAME_GRAY_TEXT),
|
||||
(IMAGE, self.TAG_NAME_GRAY_TEXT)
|
||||
)
|
||||
for regexp, tag_name in regexps:
|
||||
matches = re.finditer(regexp, text)
|
||||
for match in matches:
|
||||
result.append((tag_name, (), match.start(), match.start("text")))
|
||||
result.append((tag_name, (), match.end("text"), match.end()))
|
||||
|
||||
# Find "<url>" links (gray out).
|
||||
matches = re.finditer(LINK_ALT, text)
|
||||
for match in matches:
|
||||
result.append((
|
||||
self.TAG_NAME_GRAY_TEXT, (), match.start("text"), match.end("text")))
|
||||
|
||||
# Find "---" horizontal rule (center).
|
||||
matches = re.finditer(HORIZONTAL_RULE, text)
|
||||
for match in matches:
|
||||
result.append((
|
||||
self.TAG_NAME_CENTER, (), match.start("symbols"), match.end("symbols")))
|
||||
|
||||
# Find "* list" (offset).
|
||||
matches = re.finditer(LIST, text)
|
||||
for match in matches:
|
||||
# Lists use character+space (eg. "* ").
|
||||
length = 2
|
||||
nest = len(match.group("indent").replace(" ", "\t"))
|
||||
margin = -length - 2 * nest
|
||||
indent = -length - 2 * length * nest
|
||||
result.append((
|
||||
self.TAG_NAME_MARGIN_INDENT,
|
||||
(margin, indent),
|
||||
match.start("content"),
|
||||
match.end("content")
|
||||
))
|
||||
|
||||
# Find "1. ordered list" (offset).
|
||||
matches = re.finditer(ORDERED_LIST, text)
|
||||
for match in matches:
|
||||
# Ordered lists use numbers/letters+dot/parens+space (eg. "123. ").
|
||||
length = len(match.group("prefix")) + 1
|
||||
nest = len(match.group("indent").replace(" ", "\t"))
|
||||
margin = -length - 2 * nest
|
||||
indent = -length - 2 * length * nest
|
||||
result.append((
|
||||
self.TAG_NAME_MARGIN_INDENT,
|
||||
(margin, indent),
|
||||
match.start("content"),
|
||||
match.end("content")
|
||||
))
|
||||
|
||||
# Find "> blockquote" (offset).
|
||||
matches = re.finditer(BLOCK_QUOTE, text)
|
||||
for match in matches:
|
||||
result.append((self.TAG_NAME_MARGIN_INDENT, (2, -2), match.start(), match.end()))
|
||||
|
||||
# Find "# Header" (offset+bold).
|
||||
matches = re.finditer(HEADER, text)
|
||||
for match in matches:
|
||||
margin = -len(match.group("level")) - 1
|
||||
result.append((
|
||||
self.TAG_NAME_MARGIN_INDENT, (margin, 0), match.start(), match.end()))
|
||||
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
|
||||
|
||||
# Find "=======" header underline (bold).
|
||||
matches = re.finditer(HEADER_UNDER, text)
|
||||
for match in matches:
|
||||
result.append((self.TAG_NAME_BOLD, (), match.start(), match.end()))
|
||||
|
||||
# Find "```" code block tag (offset + colorize paragraph).
|
||||
matches = re.finditer(markup_regex.CODE_BLOCK, text)
|
||||
for match in matches:
|
||||
result.append((
|
||||
self.TAG_NAME_CODE_BLOCK, (), match.start("block"), match.end("block")))
|
||||
|
||||
# Send parsed data back.
|
||||
child_conn.send((text, result))
|
||||
|
||||
def on_parsed(self, _source, _condition):
|
||||
"""Reads the parsing result from the pipe and triggers any pending apply."""
|
||||
|
||||
self.parsing = False
|
||||
if self.apply_pending:
|
||||
self.apply() # self.apply clears the apply pending flag.
|
||||
|
||||
try:
|
||||
if self.parent_conn.poll():
|
||||
self.do_apply(*self.parent_conn.recv())
|
||||
return True
|
||||
except EOFError:
|
||||
return False
|
||||
|
||||
def do_apply(self, original_text, result=[]):
|
||||
"""Applies the result of parsing if the current text matches the original text."""
|
||||
|
||||
buffer = self.text_buffer
|
||||
start = buffer.get_start_iter()
|
||||
end = buffer.get_end_iter()
|
||||
text = self.text_buffer.get_slice(start, end, False)
|
||||
|
||||
# Apply markup tags.
|
||||
if text == original_text and text != self.marked_up_text:
|
||||
buffer.remove_tag(self.tag_italic, start, end)
|
||||
buffer.remove_tag(self.tag_bold, start, end)
|
||||
buffer.remove_tag(self.tag_bold_italic, start, end)
|
||||
buffer.remove_tag(self.tag_strikethrough, start, end)
|
||||
buffer.remove_tag(self.tag_center, start, end)
|
||||
buffer.remove_tag(self.tag_plain_text, start, end)
|
||||
buffer.remove_tag(self.tag_gray_text, start, end)
|
||||
buffer.remove_tag(self.tag_code_text, start, end)
|
||||
buffer.remove_tag(self.tag_code_block, start, end)
|
||||
buffer.remove_tag(self.tag_wrap_none, start, end)
|
||||
for tag in self.tags_margins_indents.values():
|
||||
buffer.remove_tag(tag, start, end)
|
||||
|
||||
for tag_name, tag_args, tag_start, tag_end in result:
|
||||
buffer.apply_tag(
|
||||
self.tags_markup[tag_name](tag_args),
|
||||
buffer.get_iter_at_offset(tag_start),
|
||||
buffer.get_iter_at_offset(tag_end))
|
||||
|
||||
# Apply focus mode tag (grey out before/after current sentence).
|
||||
buffer.remove_tag(self.tag_unfocused_text, start, end)
|
||||
if self.text_view.focus_mode:
|
||||
cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
|
||||
start_sentence = cursor_iter.copy()
|
||||
start_sentence.backward_sentence_start()
|
||||
end_sentence = cursor_iter.copy()
|
||||
end_sentence.forward_sentence_end()
|
||||
buffer.apply_tag(self.tag_unfocused_text, start, start_sentence)
|
||||
buffer.apply_tag(self.tag_unfocused_text, end_sentence, end)
|
||||
|
||||
# Margin and indent are cumulative. They differ in two ways:
|
||||
# * Margin is always in the beginning, which means it effectively only affects the first line
|
||||
# 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.tags_margins_indents:
|
||||
margin, indent = self.get_margin_indent(margin_level, indent_level)
|
||||
tag = self.text_buffer.create_tag(
|
||||
"margin_indent_{}_{}".format(margin_level, indent_level),
|
||||
left_margin=margin, indent=indent)
|
||||
self.tags_margins_indents[level] = tag
|
||||
return tag
|
||||
else:
|
||||
return self.tags_margins_indents[level]
|
||||
|
||||
def get_margin_indent(self, margin_level, indent_level, baseline_margin=None, char_width=None):
|
||||
if baseline_margin is None:
|
||||
baseline_margin = self.text_view.props.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.props.left_margin
|
||||
char_width = helpers.get_char_width(self.text_view)
|
||||
|
||||
# Bail out if neither the baseline margin nor character width change
|
||||
if baseline_margin == self.baseline_margin and char_width == self.char_width:
|
||||
return
|
||||
self.baseline_margin = baseline_margin
|
||||
self.char_width = char_width
|
||||
|
||||
# Adjust tab size
|
||||
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
|
||||
for level, tag in self.tags_margins_indents.items():
|
||||
margin, indent = self.get_margin_indent(*level, baseline_margin, char_width)
|
||||
tag.set_properties(left_margin=margin, indent=indent)
|
||||
|
||||
def stop(self, *_):
|
||||
self.parent_conn.close()
|
|
@ -0,0 +1,110 @@
|
|||
class TextViewScroller:
|
||||
def __init__(self, text_view, scrolled_window):
|
||||
super().__init__()
|
||||
|
||||
self.text_view = text_view
|
||||
self.scrolled_window = scrolled_window
|
||||
self.smooth_scroller = None
|
||||
|
||||
def can_scroll(self):
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
return vap.upper > vap.page_size
|
||||
|
||||
def get_scroll_scale(self):
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
if vap.upper > vap.page_size:
|
||||
return vap.value / (vap.upper - vap.page_size)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def set_scroll_scale(self, scale):
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
vap.value = (vap.upper - vap.page_size) * scale
|
||||
|
||||
def scroll_to_mark(self, mark, center):
|
||||
"""Scrolls until mark is visible, if needed."""
|
||||
|
||||
target_pos = self.get_target_pos_for_mark(mark, center)
|
||||
if target_pos:
|
||||
self.scrolled_window.get_vadjustment().set_value(target_pos)
|
||||
|
||||
def smooth_scroll_to_mark(self, mark, center):
|
||||
"""Smoothly scrolls until mark is visible, if needed."""
|
||||
|
||||
if self.smooth_scroller and self.smooth_scroller.is_started:
|
||||
self.smooth_scroller.end()
|
||||
|
||||
target_pos = self.get_target_pos_for_mark(mark, center)
|
||||
if target_pos:
|
||||
source_pos = self.scrolled_window.get_vadjustment().props.value
|
||||
self.smooth_scroller = SmoothScroller(self.scrolled_window, source_pos, target_pos)
|
||||
self.smooth_scroller.start()
|
||||
|
||||
def get_target_pos_for_mark(self, mark, center):
|
||||
margin = 32
|
||||
|
||||
mark_iter = self.text_view.get_buffer().get_iter_at_mark(mark)
|
||||
mark_rect = self.text_view.get_iter_location(mark_iter)
|
||||
|
||||
vap = self.scrolled_window.get_vadjustment().props
|
||||
|
||||
pos_y = mark_rect.y + mark_rect.height + self.text_view.props.top_margin
|
||||
pos_viewport_y = pos_y - vap.value
|
||||
target_pos = None
|
||||
if center:
|
||||
if pos_viewport_y != vap.page_size / 2:
|
||||
target_pos = pos_y - (vap.page_size / 2)
|
||||
elif pos_viewport_y > vap.page_size - margin:
|
||||
target_pos = pos_y - vap.page_size + margin
|
||||
elif pos_viewport_y < margin:
|
||||
target_pos = pos_y - margin - mark_rect.height
|
||||
|
||||
return target_pos
|
||||
|
||||
|
||||
class SmoothScroller:
|
||||
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(100, (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
|
|
@ -0,0 +1,223 @@
|
|||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
|
||||
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", " "))
|
||||
|
||||
def undo(self, text_buffer):
|
||||
offset = self.offset
|
||||
start = text_buffer.get_iter_at_offset(offset)
|
||||
stop = text_buffer.get_iter_at_offset(offset + self.length)
|
||||
text_buffer.place_cursor(start)
|
||||
text_buffer.delete(start, stop)
|
||||
|
||||
def redo(self, text_buffer):
|
||||
start = text_buffer.get_iter_at_offset(self.offset)
|
||||
text_buffer.insert(start, self.text)
|
||||
new_cursor_pos = text_buffer.get_iter_at_offset(self.offset + self.length)
|
||||
text_buffer.place_cursor(new_cursor_pos)
|
||||
|
||||
def merge(self, next_action):
|
||||
"""Merge a following action into this insert, if possible
|
||||
|
||||
can't merge if prev is not another insert
|
||||
can't merge if prev and cur are not mergeable in the first place
|
||||
can't merge when user set the input bar somewhere else
|
||||
can't merge across word boundaries"""
|
||||
|
||||
if not isinstance(next_action, UndoableInsert):
|
||||
return False
|
||||
if not self.mergeable or not next_action.mergeable:
|
||||
return False
|
||||
if self.offset + self.length != next_action.offset:
|
||||
return False
|
||||
whitespace = (' ', '\t')
|
||||
if self.text in whitespace != next_action.text in whitespace:
|
||||
return False
|
||||
|
||||
self.length += next_action.length
|
||||
self.text += next_action.text
|
||||
return True
|
||||
|
||||
|
||||
class UndoableDelete:
|
||||
"""Something has been deleted from text_buffer"""
|
||||
|
||||
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", " "))
|
||||
|
||||
def undo(self, text_buffer):
|
||||
start = text_buffer.get_iter_at_offset(self.start)
|
||||
text_buffer.insert(start, self.text)
|
||||
if self.delete_key_used:
|
||||
text_buffer.place_cursor(start)
|
||||
else:
|
||||
stop = text_buffer.get_iter_at_offset(self.end)
|
||||
text_buffer.place_cursor(stop)
|
||||
|
||||
def redo(self, text_buffer):
|
||||
start = text_buffer.get_iter_at_offset(self.start)
|
||||
stop = text_buffer.get_iter_at_offset(self.end)
|
||||
text_buffer.delete(start, stop)
|
||||
text_buffer.place_cursor(start)
|
||||
|
||||
def merge(self, next_action):
|
||||
"""Check if this delete can be merged with a previous action
|
||||
|
||||
can't merge if prev is not another delete
|
||||
can't merge if prev and cur are not mergeable in the first place
|
||||
can't merge if delete and backspace key were both used
|
||||
can't merge across word boundaries"""
|
||||
|
||||
if not isinstance(next_action, UndoableDelete):
|
||||
return False
|
||||
if not self.mergeable or not next_action.mergeable:
|
||||
return False
|
||||
if self.delete_key_used != next_action.delete_key_used:
|
||||
return False
|
||||
if self.start != next_action.start and self.start != next_action.end:
|
||||
return False
|
||||
whitespace = (' ', '\t')
|
||||
if self.text in whitespace != next_action.text in whitespace:
|
||||
return False
|
||||
|
||||
if self.delete_key_used:
|
||||
self.text += next_action.text
|
||||
self.end += (next_action.end - next_action.start)
|
||||
else:
|
||||
self.text = "%s%s" % (next_action.text, next_action.text)
|
||||
self.start = next_action.start
|
||||
return True
|
||||
|
||||
|
||||
class UndoableGroup(list):
|
||||
"""A list of undoable actions, usually corresponding to a single user action"""
|
||||
|
||||
def undo(self, text_buffer):
|
||||
for undoable in reversed(self):
|
||||
undoable.undo(text_buffer)
|
||||
|
||||
def redo(self, text_buffer):
|
||||
for undoable in self:
|
||||
undoable.redo(text_buffer)
|
||||
|
||||
def merge(self, next_action):
|
||||
if len(self) == 1:
|
||||
return self[0].merge(next_action)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class UndoRedoHandler:
|
||||
"""Manages undo/redo for a given text_buffer.
|
||||
|
||||
Methods can be called directly, as well as be used as signal callbacks."""
|
||||
|
||||
def __init__(self):
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
self.current_undo_group = None
|
||||
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.undo_in_progress = True
|
||||
undo_action = self.undo_stack.pop()
|
||||
self.redo_stack.append(undo_action)
|
||||
undo_action.undo(text_view.get_buffer())
|
||||
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.undo_in_progress = True
|
||||
redo_action = self.redo_stack.pop()
|
||||
self.undo_stack.append(redo_action)
|
||||
redo_action.redo(text_view.get_buffer())
|
||||
self.undo_in_progress = False
|
||||
|
||||
def clear(self):
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
|
||||
def on_begin_user_action(self, _text_buffer):
|
||||
"""Start of a user action. Refer to TextBuffer's "begin-user-action" signal.
|
||||
|
||||
This method must be registered to TextBuffer's "begin-user-action" signal, or called
|
||||
manually followed by on_end_user_action."""
|
||||
|
||||
self.current_undo_group = UndoableGroup()
|
||||
|
||||
def on_end_user_action(self, _text_buffer):
|
||||
"""End of a user action. Refer to TextBuffer's "end-user-action" signal.
|
||||
|
||||
This method must be registered to TextBuffer's "end-user-action" signal, or called
|
||||
manually preceded by on_start_user_action."""
|
||||
|
||||
if self.current_undo_group:
|
||||
self.undo_stack.append(self.current_undo_group)
|
||||
self.current_undo_group = None
|
||||
|
||||
def on_insert_text(self, _text_buffer, text_iter, text, _length):
|
||||
"""Records a text insert. Refer to TextBuffer's "insert-text" signal.
|
||||
|
||||
This method must be registered to TextBuffer's "insert-text" signal, or called manually
|
||||
in between on_begin_user_action and on_end_user_action."""
|
||||
|
||||
self.__record_undoable(UndoableInsert(text_iter, text, len(text)))
|
||||
|
||||
def on_delete_range(self, text_buffer, start_iter, end_iter):
|
||||
"""Records a range deletion. Refer to TextBuffer's "delete-range" signal.
|
||||
|
||||
This method must be registered to TextBuffer's "delete-range" signal, or called manually
|
||||
in between on_begin_user_action and on_end_user_action."""
|
||||
|
||||
self.__record_undoable(UndoableDelete(text_buffer, start_iter, end_iter))
|
||||
|
||||
def __record_undoable(self, undoable):
|
||||
"""Records a change, merging it to a previous one if possible."""
|
||||
|
||||
if not self.undo_in_progress:
|
||||
self.redo_stack = []
|
||||
else:
|
||||
return
|
||||
|
||||
prev_group_undoable = self.current_undo_group[-1] if self.current_undo_group else None
|
||||
prev_stack_undoable = self.undo_stack[-1] if self.undo_stack else None
|
||||
|
||||
if prev_group_undoable:
|
||||
merged = prev_group_undoable.merge(undoable)
|
||||
elif prev_stack_undoable:
|
||||
merged = prev_stack_undoable.merge(undoable)
|
||||
else:
|
||||
merged = False
|
||||
|
||||
if not merged:
|
||||
if self.current_undo_group is None:
|
||||
LOGGER.warning("Recording a change without a user action.")
|
||||
self.undo_stack.append(undoable)
|
||||
else:
|
||||
self.current_undo_group.append(undoable)
|
|
@ -1,7 +1,7 @@
|
|||
from gi.repository import Gtk
|
||||
|
||||
from uberwriter.settings import Settings
|
||||
from uberwriter.helpers import get_css_path
|
||||
from apostrophe.settings import Settings
|
||||
from apostrophe.helpers import get_css_path
|
||||
|
||||
|
||||
class Theme:
|
||||
|
@ -14,9 +14,8 @@ class Theme:
|
|||
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
|
||||
|
@ -32,10 +31,9 @@ class Theme:
|
|||
@classmethod
|
||||
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()
|
||||
dark_mode = cls.settings.get_boolean('dark-mode')
|
||||
current_theme = cls.get_for_name(theme_name)
|
||||
if not dark_mode_auto and dark_mode != current_theme.is_dark and current_theme.inverse_name:
|
||||
if 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
|
||||
|
@ -49,7 +47,6 @@ class Theme:
|
|||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and \
|
||||
self.name == other.name and \
|
||||
self.gtk_css_path == other.gtk_css_path and \
|
||||
self.web_css_path == other.web_css_path and \
|
||||
self.is_dark == other.is_dark and \
|
||||
self.inverse_name == other.inverse_name
|
||||
|
@ -57,20 +54,13 @@ class Theme:
|
|||
|
||||
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.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.css'), False, 'Arc-Dark'),
|
||||
Theme('Arc-Dark', get_css_path('web/arc.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')
|
||||
]
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="addon">
|
||||
<id>de.wolfvollprecht.UberWriter.Plugin.TexLive</id>
|
||||
<extends>de.wolfvollprecht.UberWriter.desktop</extends>
|
||||
<extends>de.wolfvollprecht.UberWriter</extends>
|
||||
<name>TexLive Plugin</name>
|
||||
<summary>Allows to export to pdf and to show formulas in the inline preview</summary>
|
||||
<url type="homepage">https://www.tug.org/texlive//</url>
|
||||
<project_license>LPPL</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<update_contact>w.vollprecht_AT_gmail.com</update_contact>
|
||||
</component>
|
||||
</component>
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"app-id": "de.wolfvollprecht.UberWriter",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "3.36",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "uberwriter",
|
||||
"finish-args": [
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--filesystem=host",
|
||||
"--env=PATH=/app/bin:/usr/bin:/app/extensions/TexLive/2019/bin/x86_64-linux/",
|
||||
"--metadata=X-DConf=migrate-path=/de/wolfvollprecht/UberWriter/"
|
||||
],
|
||||
"add-extensions": {
|
||||
"de.wolfvollprecht.UberWriter.Plugin": {
|
||||
"directory": "extensions",
|
||||
"version": "stable",
|
||||
"subdirectories": true,
|
||||
"no-autodownload": true,
|
||||
"autodelete": true
|
||||
}
|
||||
},
|
||||
"modules": [{
|
||||
"name":"gspell",
|
||||
"sources":[{
|
||||
"type":"archive",
|
||||
"url":"https://download.gnome.org/sources/gspell/1.8/gspell-1.8.3.tar.xz",
|
||||
"sha256":"5ae514dd0216be069176accf6d0049d6a01cfa6a50df4bc06be85f7080b62de8"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "pandoc",
|
||||
"only-arches": [
|
||||
"x86_64"
|
||||
],
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"cp bin/pandoc /app/bin/pandoc",
|
||||
"cp bin/pandoc-citeproc /app/bin/pandoc-citeproc"
|
||||
],
|
||||
"sources": [{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/jgm/pandoc/releases/download/2.9.2/pandoc-2.9.2-linux-amd64.tar.gz",
|
||||
"sha256": "039f155b6166c1e268479bcb06af2dba99eb7795cbff7b3c13b4875388195d08"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "pipdeps",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} regex pypandoc"
|
||||
],
|
||||
"sources": [{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/75/28/521c6dc7fef23a68368efefdcd682f5b3d1d58c2b90b06dc1d0b805b51ae/wheel-0.34.2.tar.gz",
|
||||
"sha256": "8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/8e/76/66066b7bc71817238924c7e4b448abdb17eb0c92d645769c223f9ace478f/pip-20.0.2.tar.gz",
|
||||
"sha256": "7db0c8ea4c7ea51c8049640e8e6e7fde949de672bfa4949920675563a5a6967f"
|
||||
},
|
||||
{
|
||||
"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/e8/76/8ac7f467617b9cfbafcef3c76df6f22b15de654a62bea719792b00a83195/regex-2020.2.20.tar.gz",
|
||||
"sha256": "9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/14/4b/6f7a3f2bb1e2fa4d3007126578cae0b9910ff46c4957bef5bd4b92733011/pyenchant-3.0.1.tar.gz",
|
||||
"sha256": "1bd26a644abf80196a9de3f2d820ebafb7e7f78385e392ce77cb1552f164d559"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "fonts",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"mkdir -p /app/share/fonts/",
|
||||
"cp ttf/* /app/share/fonts/"
|
||||
],
|
||||
"sources": [{
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozilla/Fira",
|
||||
"tag": "4.202"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "uberwriter",
|
||||
"buildsystem": "meson",
|
||||
"config-opts" : [
|
||||
"-Dprofile=development"
|
||||
],
|
||||
"sources": [{
|
||||
"type" : "dir",
|
||||
"path" : "../../"
|
||||
}],
|
||||
"post-install": [
|
||||
"install -d /app/extensions"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"id": "de.wolfvollprecht.UberWriter.Plugin.TexLive",
|
||||
"runtime": "de.wolfvollprecht.UberWriter",
|
||||
"branch": "stable",
|
||||
"sdk": "org.gnome.Sdk//3.26",
|
||||
"sdk": "org.gnome.Sdk//3.34",
|
||||
"build-extension": true,
|
||||
"separate-locales": false,
|
||||
"appstream-compose": false,
|
||||
|
@ -13,7 +13,7 @@
|
|||
"cflags": "-O2 -g",
|
||||
"cxxflags": "-O2 -g",
|
||||
"env": {
|
||||
"PATH": "/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin"
|
||||
"PATH": "/app/extensions/TexLive/bin:/app/extensions/TexLive/2019/bin/x86_64-linux:/app/bin:/usr/bin"
|
||||
}
|
||||
},
|
||||
"cleanup": ["/bin/wget"],
|
|
@ -2,7 +2,7 @@
|
|||
"id": "de.wolfvollprecht.UberWriter.Plugin.TexLive",
|
||||
"runtime": "de.wolfvollprecht.UberWriter",
|
||||
"branch": "stable",
|
||||
"sdk": "org.gnome.Sdk//3.26",
|
||||
"sdk": "org.gnome.Sdk//3.34",
|
||||
"build-extension": true,
|
||||
"separate-locales": false,
|
||||
"appstream-compose": false,
|
||||
|
@ -13,7 +13,7 @@
|
|||
"cflags": "-O2 -g",
|
||||
"cxxflags": "-O2 -g",
|
||||
"env": {
|
||||
"PATH": "/app/extensions/TexLive/bin:/app/extensions/TexLive/2018/bin/x86_64-linux:/app/bin:/usr/bin"
|
||||
"PATH": "/app/extensions/TexLive/bin:/app/extensions/TexLive/2019/bin/x86_64-linux:/app/bin:/usr/bin"
|
||||
}
|
||||
},
|
||||
"cleanup": ["/bin/wget"],
|
||||
|
@ -55,7 +55,7 @@
|
|||
{
|
||||
"type":"file",
|
||||
"url": "http://mirrors.ctan.org/systems/texlive/Images/texlive.iso",
|
||||
"sha512": "7b7f0dd0eab3bfffe52c5cd1139c7f75d029b9ff4c4ce0e57e06834705522f4ec0c02cd99a80b053c6619abda51c20a60f8e91e91781bdc2b9b60fc2e5708adb"
|
||||
"sha512": "a00a943ce4438fe2aecf8b1e05f9055135ef03c56b6782a49205bac9023d77c781f3cab50f2f9555ac116bb0d97d6570afffe7c60b8745325b9941f82af7ef83 "
|
||||
},
|
||||
{
|
||||
"type": "file",
|
|
@ -1,9 +1,9 @@
|
|||
# Download the installer!
|
||||
# Currently using 2017 edition, upgrade to 2018 tomorrow! (just released, needs)
|
||||
# time to propagate everywhere
|
||||
wget ftp://tug.org/historic/systems/texlive/2018/install-tl-unx.tar.gz
|
||||
wget ftp://tug.org/historic/systems/texlive/2019/install-tl-unx.tar.gz
|
||||
myhash=$(sha256sum install-tl-unx.tar.gz | cut -d' ' -f1)
|
||||
if [ $myhash != "82c13110852af162c4c5ef1579fa2f4f51c2040850ec02fb7f97497da45eb446" ] ; then echo "CHECKSUM MISMATCH!"; exit 1 ; fi
|
||||
if [ $myhash != "44aa41b5783e345b7021387f19ac9637ff1ce5406a59754230c666642dfe7750" ] ; then echo "CHECKSUM MISMATCH!"; exit 1 ; fi
|
||||
|
||||
tar xvf install-tl-unx.tar.gz
|
||||
|
||||
|
@ -18,13 +18,13 @@ cat <<EOF > texlive.profile
|
|||
# It will NOT be updated and reflects only the
|
||||
# installation profile at installation time.
|
||||
selected_scheme scheme-basic
|
||||
TEXDIR /app/extensions/TexLive/2018
|
||||
TEXMFCONFIG ~/.texlive2018/texmf-config
|
||||
TEXDIR /app/extensions/TexLive/2019
|
||||
TEXMFCONFIG ~/.texlive2019/texmf-config
|
||||
TEXMFHOME ~/texmf
|
||||
TEXMFLOCAL /app/extensions/TexLive/texmf-local
|
||||
TEXMFSYSCONFIG /app/extensions/TexLive/2018/texmf-config
|
||||
TEXMFSYSVAR /app/extensions/TexLive/2018/texmf-var
|
||||
TEXMFVAR ~/.texlive2018/texmf-var
|
||||
TEXMFSYSCONFIG /app/extensions/TexLive/2019/texmf-config
|
||||
TEXMFSYSVAR /app/extensions/TexLive/2019/texmf-var
|
||||
TEXMFVAR ~/.texlive2019/texmf-var
|
||||
binary_x86_64-linux 1
|
||||
collection-latex 1
|
||||
collection-binextra 1
|
||||
|
@ -58,5 +58,5 @@ all:
|
|||
echo "I am a pretty empty Makefile."
|
||||
|
||||
install:
|
||||
./install-tl-20180414/install-tl --profile texlive.profile
|
||||
./install-tl-20190410/install-tl --profile texlive.profile
|
||||
EOF
|
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from os import environ, path
|
||||
from subprocess import call
|
||||
|
||||
if not environ.get('DESTDIR', ''):
|
||||
PREFIX = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
|
||||
DATA_DIR = path.join(PREFIX, 'share')
|
||||
print('Updating icon cache...')
|
||||
call(['gtk-update-icon-cache', '-qtf', path.join(DATA_DIR, 'icons/hicolor')])
|
||||
print("compiling new schemas")
|
||||
call(["glib-compile-schemas", path.join(DATA_DIR, 'glib-2.0/schemas')])
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/de/wolfvollprecht/UberWriter/">
|
||||
<file compressed="true" alias="icons/scalable/status/preview-layout-full-width-symbolic.svg">media/icons/preview-layout-full-width-symbolic.svg</file>
|
||||
<file compressed="true" alias="icons/scalable/status/preview-layout-half-width-symbolic.svg">media/icons/preview-layout-half-width-symbolic.svg</file>
|
||||
<file compressed="true" alias="icons/scalable/status/preview-layout-windowed-symbolic.svg">media/icons/preview-layout-windowed-symbolic.svg</file>
|
||||
<file compressed="true" alias="icons/scalable/status/preview-layout-half-height-symbolic.svg">media/icons/preview-layout-half-height-symbolic.svg</file>
|
||||
<file compressed="true">media/css/gtk/base.css</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Export.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/ExportPopover.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Menu.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Preferences.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Recents.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Shortcuts.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Window.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/Headerbar.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">ui/PreviewLayoutSwitcherItem.ui</file>
|
||||
<file compressed="true" preprocess="xml-stripblanks">About.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
|
@ -1,38 +1,65 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>de.wolfvollprecht.UberWriter</id>
|
||||
<launchable type="desktop-id">de.wolfvollprecht.UberWriter.desktop</launchable>
|
||||
<name>UberWriter</name>
|
||||
<summary>An elegant, free distraction GTK+ markdown editor</summary>
|
||||
<id>@app-id@</id>
|
||||
<launchable type="desktop-id">@app-id@.desktop</launchable>
|
||||
<name>Apostrophe</name>
|
||||
<summary>An elegant, distraction-free GTK markdown editor</summary>
|
||||
<description>
|
||||
<p>Uberwriter is a GTK+ based distraction free Markdown editor, mainly developed by Wolf Vollprecht and Manuel Genovés. It uses pandoc as backend for markdown parsing and offers a very clean and sleek user interface.</p>
|
||||
<p>Apostrophe is a GTK based distraction free Markdown editor, originally created by Wolf Vollprecht and maintained by Manuel Genovés. It uses pandoc as backend for markdown parsing and offers a very clean and sleek user interface.</p>
|
||||
<p>You can install the recommended TexLive extension with the command:</p>
|
||||
<p>flatpak install flathub de.wolfvollprecht.UberWriter.Plugin.TexLive</p>
|
||||
<p>or from Gnome-Software</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/uberwriter/master/screenshots/main.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/main.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/uberwriter/master/screenshots/main-dark.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/main-dark.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/uberwriter/master/screenshots/formula.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/formula.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/uberwriter/master/screenshots/preview.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/preview.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/uberwriter/master/screenshots/focus.png</image>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/focus.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release date="2020-03-19" version="2.2.0">
|
||||
<description>
|
||||
<p>UI/UX/Functionality</p>
|
||||
<ul>
|
||||
<li>New headerbar design</li>
|
||||
<li>New preview modes, with the option to sync them to the edit view</li>
|
||||
<li>New preview mode selector</li>
|
||||
<li>New theme selector</li>
|
||||
<li>Rework the autohiding mechanism; now the headerbar fades away when typing, only to reappear when the cursor moves to the top portion of the window</li>
|
||||
<li>Now the content of the texview goes visually bellow the headerbar</li>
|
||||
<li>Overall better styling</li>
|
||||
<li>Added Hemingway mode, which disables the backspace key</li>
|
||||
<li>Added Github Flavoured Markdow, MultiMarkdown, Pandoc's Markdown and Commonmark support, being CommonMark the default from now on</li>
|
||||
<li>New stats counter, with the option to show count of characters/words/sentences/paragrafs/reading time</li>
|
||||
<li>Better handling of DnD events</li>
|
||||
<li>Export to A4 by default</li>
|
||||
</ul>
|
||||
<p>Technical improvements</p>
|
||||
<ul>
|
||||
<li>Port of the buildsystem to Meson. Now you can hit the "build" button on Builder and everything works as expected</li>
|
||||
<li>Port to gspell</li>
|
||||
<li>Partial port to gresources</li>
|
||||
<li>Overall refactorization of the codebase</li>
|
||||
<li>General bugfixes and improvements</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2019-03-10" version="2.1.5">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added italian language</li>
|
||||
<li>Initial themes support: now uberwriter adapts his colors to the current GTK theme</li>
|
||||
<li>Initial themes support: now apostrophe adapts his colors to the current GTK theme</li>
|
||||
<li>Disabled scroll gradient, can be enabled in the preferences dialog</li>
|
||||
<li>Allow to disable headerbar autohidding in Dconf</li>
|
||||
<li>Now a single click is enough to open files in the recent files popover</li>
|
||||
|
@ -54,7 +81,7 @@
|
|||
</release>
|
||||
<release date="2018-07-27" version="2.1.2">
|
||||
<description>
|
||||
<p>This release provides a fix to a bug that caused Uberwriter to not mark properly **bold**, *cursive*, and ***bold and cursive*** words.</p>
|
||||
<p>This release provides a fix to a bug that caused Apostrophe to not mark properly **bold**, *cursive*, and ***bold and cursive*** words.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-07-26" version="2.1.1">
|
||||
|
@ -108,13 +135,13 @@
|
|||
</releases>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<url type="homepage">http://uberwriter.github.io/uberwriter</url>
|
||||
<url type="homepage">http://apostrophe.github.io/apostrophe</url>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<developer_name>Wolf V., Manuel G.</developer_name>
|
||||
<url type="bugtracker">https://github.com/UberWriter/uberwriter/issues</url>
|
||||
<url type="donation">https://liberapay.com/UberWriter/donate</url>
|
||||
<url type="help">http://uberwriter.github.io/uberwriter</url>
|
||||
<url type="bugtracker">https://github.com/Apostrophe/apostrophe/issues</url>
|
||||
<url type="donation">https://liberapay.com/Apostrophe/donate</url>
|
||||
<url type="help">http://apostrophe.github.io/apostrophe</url>
|
||||
<url type="translate">https://poeditor.com/join/project/gxVzFyXb2x</url>
|
||||
<update_contact>manuel.genoves_at_gmail.com</update_contact>
|
||||
<translation type="gettext">uberwriter</translation>
|
||||
</component>
|
||||
<translation type="gettext">@gettext-package@</translation>
|
||||
</component>
|
|
@ -1,9 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Name=UberWriter
|
||||
Comment=UberWriter, a simple and distraction free Markdown Editor
|
||||
Categories=GNOME;GTK;Office;
|
||||
Exec=uberwriter %U
|
||||
Icon=de.wolfvollprecht.UberWriter
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=text/x-markdown;text/plain;
|
|
@ -0,0 +1,11 @@
|
|||
[Desktop Entry]
|
||||
Name=Apostrophe
|
||||
# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
|
||||
Keywords=uberwriter;UberWriter;apostrophe;markdown;editor;
|
||||
Comment=Apostrophe, a simple and distraction free Markdown Editor
|
||||
Categories=GNOME;GTK;Office;
|
||||
Exec=apostrophe %U
|
||||
Icon=@icon@
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=text/x-markdown;text/plain;
|
|
@ -2,18 +2,25 @@
|
|||
|
||||
<schemalist>
|
||||
|
||||
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<enum id='de.wolfvollprecht.UberWriter.PreviewMode'>
|
||||
<value nick='full-width' value='0' />
|
||||
<value nick='half-width' value='1' />
|
||||
<value nick='half-height' value='2' />
|
||||
<value nick='windowed' value='3' />
|
||||
</enum>
|
||||
|
||||
<schema path="/de/wolfvollprecht/UberWriter/" id="de.wolfvollprecht.UberWriter">
|
||||
<key name='dark-mode' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Force dark mode</summary>
|
||||
<summary>Use dark mode</summary>
|
||||
<description>
|
||||
Enable or disable the dark mode.
|
||||
</description>
|
||||
|
@ -25,12 +32,11 @@
|
|||
Enable or disable spellchecking.
|
||||
</description>
|
||||
</key>
|
||||
<key name='gradient-overlay' type='b'>
|
||||
<default>false</default>
|
||||
<summary>Draw scroll gradient</summary>
|
||||
<key name='sync-scroll' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Synchronize editor/preview scrolling</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.
|
||||
Keep the editor and preview scroll positions in sync.
|
||||
</description>
|
||||
</key>
|
||||
<key name='input-format' type='s'>
|
||||
|
@ -40,11 +46,11 @@
|
|||
Input format to use when previewing and exporting using Pandoc.
|
||||
</description>
|
||||
</key>
|
||||
<key name='poll-motion' type='b'>
|
||||
<key name='autohide-headerbar' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Allow Uberwriter to poll cursor motion</summary>
|
||||
<summary>Autohide Headerbar</summary>
|
||||
<description>
|
||||
Hide the header and status bars if the cursor is not moving.
|
||||
Hide the header and status bars when typing.
|
||||
</description>
|
||||
</key>
|
||||
<key name='open-file-path' type='s'>
|
||||
|
@ -54,6 +60,27 @@
|
|||
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>
|
||||
<key name='characters-per-line' type='i'>
|
||||
<default>66</default>
|
||||
<summary>Characters per line</summary>
|
||||
<description>
|
||||
Maximum number of characters per line within the editor.
|
||||
</description>
|
||||
</key>
|
||||
<key name='preview-mode' enum='de.wolfvollprecht.UberWriter.PreviewMode'>
|
||||
<default>"full-width"</default>
|
||||
<summary>Preview mode</summary>
|
||||
<description>
|
||||
How to display the preview.
|
||||
</description>
|
||||
</key>
|
||||
|
||||
</schema>
|
||||
|
||||
|
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,211 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="11.999994" y1="254" x2="116" y2="254" gradientTransform="matrix(1.076923,0,0,1.066667,-4.428073,-187.428352)">
|
||||
<stop offset="0" style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"/>
|
||||
<stop offset="0.0384616" style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"/>
|
||||
<stop offset="0.0769231" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
<stop offset="0.923077" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
<stop offset="0.961538" style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear1" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,233.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear2" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,309.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear3" gradientUnits="userSpaceOnUse" x1="17" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,257.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(68.235296%,67.843139%,67.058825%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(80.784315%,80.000001%,78.431374%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear4" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,253.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear5" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,273.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear6" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,293.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear7" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,313.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear8" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,243.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear9" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,263.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear10" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,283.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear11" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,313.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(90.196079%,38.039216%,0%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(100%,47.058824%,0%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear12" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,237.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear13" gradientUnits="userSpaceOnUse" x1="11.999994" y1="254" x2="116" y2="254" gradientTransform="matrix(1.076923,0,0,1.066667,-4.428073,-187.428352)">
|
||||
<stop offset="0" style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"/>
|
||||
<stop offset="0.0384616" style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"/>
|
||||
<stop offset="0.0769231" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
<stop offset="0.923077" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
<stop offset="0.961538" style="stop-color:rgb(75.294119%,74.901962%,73.725492%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(60.392159%,60.000002%,58.823532%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear14" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,233.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear15" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,309.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear16" gradientUnits="userSpaceOnUse" x1="17" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,257.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(68.235296%,67.843139%,67.058825%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(80.784315%,80.000001%,78.431374%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear17" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,253.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear18" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,273.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear19" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,293.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear20" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,313.495007,20.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear21" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,243.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear22" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,263.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear23" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,283.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear24" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,313.495007,42.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(90.196079%,38.039216%,0%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(100%,47.058824%,0%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear25" gradientUnits="userSpaceOnUse" x1="19" y1="209" x2="31" y2="209" gradientTransform="matrix(0.000000000000000061,1,-1,0.000000000000000061,237.495007,64.504974)">
|
||||
<stop offset="0" style="stop-color:rgb(36.862746%,36.078432%,39.215687%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(46.666667%,46.27451%,48.235294%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip2">
|
||||
<rect x="0" y="0" width="128" height="128"/>
|
||||
</clipPath>
|
||||
<g id="surface50195" clip-path="url(#clip2)">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear13);" d="M 16.496094 51.503906 L 112.496094 51.503906 C 116.914062 51.503906 120.496094 55.085938 120.496094 59.503906 L 120.496094 107.503906 C 120.496094 111.921875 116.914062 115.503906 112.496094 115.503906 L 16.496094 115.503906 C 12.078125 115.503906 8.496094 111.921875 8.496094 107.503906 L 8.496094 59.503906 C 8.496094 55.085938 12.078125 51.503906 16.496094 51.503906 Z M 16.496094 51.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.470588%,96.078432%,95.686275%);fill-opacity:1;" d="M 16.496094 27.503906 L 112.496094 27.503906 C 116.914062 27.503906 120.496094 31.464844 120.496094 36.347656 L 120.496094 102.664062 C 120.496094 107.546875 116.914062 111.503906 112.496094 111.503906 L 16.496094 111.503906 C 12.078125 111.503906 8.496094 107.546875 8.496094 102.664062 L 8.496094 36.347656 C 8.496094 31.464844 12.078125 27.503906 16.496094 27.503906 Z M 16.496094 27.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 21.496094 39.503906 L 27.496094 39.503906 C 29.710938 39.503906 31.273438 41.300781 31.496094 43.503906 L 32.496094 53.503906 C 32.714844 55.710938 30.710938 57.503906 28.496094 57.503906 L 20.496094 57.503906 C 18.277344 57.503906 16.273438 55.710938 16.496094 53.503906 L 17.496094 43.503906 C 17.714844 41.300781 19.277344 39.503906 21.496094 39.503906 Z M 21.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear14);" d="M 31.496094 43.503906 L 31.496094 47.503906 C 31.496094 49.722656 29.710938 51.503906 27.496094 51.503906 L 21.496094 51.503906 C 19.277344 51.503906 17.496094 49.722656 17.496094 47.503906 L 17.496094 43.503906 C 17.496094 41.289062 19.277344 39.503906 21.496094 39.503906 L 27.496094 39.503906 C 29.710938 39.503906 31.496094 41.289062 31.496094 43.503906 Z M 31.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 41.496094 39.503906 L 47.496094 39.503906 C 49.710938 39.503906 51.273438 41.300781 51.496094 43.503906 L 52.496094 53.503906 C 52.714844 55.710938 50.710938 57.503906 48.496094 57.503906 L 40.496094 57.503906 C 38.277344 57.503906 36.273438 55.710938 36.496094 53.503906 L 37.496094 43.503906 C 37.714844 41.300781 39.277344 39.503906 41.496094 39.503906 Z M 41.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 61.496094 39.503906 L 67.496094 39.503906 C 69.710938 39.503906 71.273438 41.300781 71.496094 43.503906 L 72.496094 53.503906 C 72.714844 55.710938 70.710938 57.503906 68.496094 57.503906 L 60.496094 57.503906 C 58.277344 57.503906 56.273438 55.710938 56.496094 53.503906 L 57.496094 43.503906 C 57.714844 41.300781 59.277344 39.503906 61.496094 39.503906 Z M 61.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 81.496094 39.503906 L 87.496094 39.503906 C 89.710938 39.503906 91.273438 41.300781 91.496094 43.503906 L 92.496094 53.503906 C 92.714844 55.710938 90.710938 57.503906 88.496094 57.503906 L 80.496094 57.503906 C 78.277344 57.503906 76.273438 55.710938 76.496094 53.503906 L 77.496094 43.503906 C 77.714844 41.300781 79.277344 39.503906 81.496094 39.503906 Z M 81.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 101.496094 39.503906 L 107.496094 39.503906 C 109.710938 39.503906 111.273438 41.300781 111.496094 43.503906 L 112.496094 53.503906 C 112.714844 55.710938 110.710938 57.503906 108.496094 57.503906 L 100.496094 57.503906 C 98.277344 57.503906 96.273438 55.710938 96.496094 53.503906 L 97.496094 43.503906 C 97.714844 41.300781 99.277344 39.503906 101.496094 39.503906 Z M 101.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 31.496094 61.503906 L 37.496094 61.503906 C 39.710938 61.503906 41.273438 63.300781 41.496094 65.503906 L 42.496094 75.503906 C 42.714844 77.710938 40.710938 79.503906 38.496094 79.503906 L 30.496094 79.503906 C 28.277344 79.503906 26.273438 77.710938 26.496094 75.503906 L 27.496094 65.503906 C 27.714844 63.300781 29.277344 61.503906 31.496094 61.503906 Z M 31.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 51.496094 61.503906 L 57.496094 61.503906 C 59.710938 61.503906 61.273438 63.300781 61.496094 65.503906 L 62.496094 75.503906 C 62.714844 77.710938 60.710938 79.503906 58.496094 79.503906 L 50.496094 79.503906 C 48.277344 79.503906 46.273438 77.710938 46.496094 75.503906 L 47.496094 65.503906 C 47.714844 63.300781 49.277344 61.503906 51.496094 61.503906 Z M 51.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 71.496094 61.503906 L 77.496094 61.503906 C 79.710938 61.503906 81.273438 63.300781 81.496094 65.503906 L 82.496094 75.503906 C 82.714844 77.710938 80.710938 79.503906 78.496094 79.503906 L 70.496094 79.503906 C 68.277344 79.503906 66.273438 77.710938 66.496094 75.503906 L 67.496094 65.503906 C 67.714844 63.300781 69.277344 61.503906 71.496094 61.503906 Z M 71.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.64706%,27.450982%,0%);fill-opacity:1;" d="M 91.496094 61.503906 L 98.496094 61.503906 C 100.710938 61.503906 102.273438 63.300781 102.496094 65.503906 L 103.496094 75.503906 C 103.714844 77.710938 101.710938 79.503906 99.496094 79.503906 L 90.496094 79.503906 C 88.277344 79.503906 86.273438 77.710938 86.496094 75.503906 L 87.496094 65.503906 C 87.714844 63.300781 89.277344 61.503906 91.496094 61.503906 Z M 91.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 25.496094 83.503906 L 31.496094 83.503906 C 33.710938 83.503906 35.273438 85.300781 35.496094 87.503906 L 36.496094 97.503906 C 36.714844 99.710938 34.710938 101.503906 32.496094 101.503906 L 24.496094 101.503906 C 22.277344 101.503906 20.273438 99.710938 20.496094 97.503906 L 21.496094 87.503906 C 21.714844 85.300781 23.277344 83.503906 25.496094 83.503906 Z M 25.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 97.496094 83.503906 L 103.496094 83.503906 C 105.710938 83.503906 107.273438 85.300781 107.496094 87.503906 L 108.496094 97.503906 C 108.714844 99.710938 106.710938 101.503906 104.496094 101.503906 L 96.496094 101.503906 C 94.277344 101.503906 92.273438 99.710938 92.496094 97.503906 L 93.496094 87.503906 C 93.714844 85.300781 95.277344 83.503906 97.496094 83.503906 Z M 97.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear15);" d="M 107.496094 87.503906 L 107.496094 91.503906 C 107.496094 93.722656 105.710938 95.503906 103.496094 95.503906 L 97.496094 95.503906 C 95.277344 95.503906 93.496094 93.722656 93.496094 91.503906 L 93.496094 87.503906 C 93.496094 85.289062 95.277344 83.503906 97.496094 83.503906 L 103.496094 83.503906 C 105.710938 83.503906 107.496094 85.289062 107.496094 87.503906 Z M 107.496094 87.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(50.980395%,50.588238%,52.549022%);fill-opacity:1;" d="M 45.496094 83.503906 L 83.496094 83.503906 C 85.710938 83.503906 87.273438 85.300781 87.496094 87.503906 L 88.496094 97.503906 C 88.714844 99.710938 86.710938 101.503906 84.496094 101.503906 L 44.496094 101.503906 C 42.277344 101.503906 40.273438 99.710938 40.496094 97.503906 L 41.496094 87.503906 C 41.714844 85.300781 43.277344 83.503906 45.496094 83.503906 Z M 45.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear16);" d="M 87.496094 87.503906 L 87.496094 91.503906 C 87.496094 93.722656 85.710938 95.503906 83.496094 95.503906 L 45.496094 95.503906 C 43.277344 95.503906 41.496094 93.722656 41.496094 91.503906 L 41.496094 87.503906 C 41.496094 85.289062 43.277344 83.503906 45.496094 83.503906 L 83.496094 83.503906 C 85.710938 83.503906 87.496094 85.289062 87.496094 87.503906 Z M 87.496094 87.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear17);" d="M 51.496094 43.503906 L 51.496094 47.503906 C 51.496094 49.722656 49.710938 51.503906 47.496094 51.503906 L 41.496094 51.503906 C 39.277344 51.503906 37.496094 49.722656 37.496094 47.503906 L 37.496094 43.503906 C 37.496094 41.289062 39.277344 39.503906 41.496094 39.503906 L 47.496094 39.503906 C 49.710938 39.503906 51.496094 41.289062 51.496094 43.503906 Z M 51.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear18);" d="M 71.496094 43.503906 L 71.496094 47.503906 C 71.496094 49.722656 69.710938 51.503906 67.496094 51.503906 L 61.496094 51.503906 C 59.277344 51.503906 57.496094 49.722656 57.496094 47.503906 L 57.496094 43.503906 C 57.496094 41.289062 59.277344 39.503906 61.496094 39.503906 L 67.496094 39.503906 C 69.710938 39.503906 71.496094 41.289062 71.496094 43.503906 Z M 71.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear19);" d="M 91.496094 43.503906 L 91.496094 47.503906 C 91.496094 49.722656 89.710938 51.503906 87.496094 51.503906 L 81.496094 51.503906 C 79.277344 51.503906 77.496094 49.722656 77.496094 47.503906 L 77.496094 43.503906 C 77.496094 41.289062 79.277344 39.503906 81.496094 39.503906 L 87.496094 39.503906 C 89.710938 39.503906 91.496094 41.289062 91.496094 43.503906 Z M 91.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear20);" d="M 111.496094 43.503906 L 111.496094 47.503906 C 111.496094 49.722656 109.710938 51.503906 107.496094 51.503906 L 101.496094 51.503906 C 99.277344 51.503906 97.496094 49.722656 97.496094 47.503906 L 97.496094 43.503906 C 97.496094 41.289062 99.277344 39.503906 101.496094 39.503906 L 107.496094 39.503906 C 109.710938 39.503906 111.496094 41.289062 111.496094 43.503906 Z M 111.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear21);" d="M 41.496094 65.503906 L 41.496094 69.503906 C 41.496094 71.722656 39.710938 73.503906 37.496094 73.503906 L 31.496094 73.503906 C 29.277344 73.503906 27.496094 71.722656 27.496094 69.503906 L 27.496094 65.503906 C 27.496094 63.289062 29.277344 61.503906 31.496094 61.503906 L 37.496094 61.503906 C 39.710938 61.503906 41.496094 63.289062 41.496094 65.503906 Z M 41.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear22);" d="M 61.496094 65.503906 L 61.496094 69.503906 C 61.496094 71.722656 59.710938 73.503906 57.496094 73.503906 L 51.496094 73.503906 C 49.277344 73.503906 47.496094 71.722656 47.496094 69.503906 L 47.496094 65.503906 C 47.496094 63.289062 49.277344 61.503906 51.496094 61.503906 L 57.496094 61.503906 C 59.710938 61.503906 61.496094 63.289062 61.496094 65.503906 Z M 61.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear23);" d="M 81.496094 65.503906 L 81.496094 69.503906 C 81.496094 71.722656 79.710938 73.503906 77.496094 73.503906 L 71.496094 73.503906 C 69.277344 73.503906 67.496094 71.722656 67.496094 69.503906 L 67.496094 65.503906 C 67.496094 63.289062 69.277344 61.503906 71.496094 61.503906 L 77.496094 61.503906 C 79.710938 61.503906 81.496094 63.289062 81.496094 65.503906 Z M 81.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear24);" d="M 102.496094 65.503906 L 102.496094 69.503906 C 102.496094 71.722656 100.710938 73.503906 98.496094 73.503906 L 91.496094 73.503906 C 89.277344 73.503906 87.496094 71.722656 87.496094 69.503906 L 87.496094 65.503906 C 87.496094 63.289062 89.277344 61.503906 91.496094 61.503906 L 98.496094 61.503906 C 100.710938 61.503906 102.496094 63.289062 102.496094 65.503906 Z M 102.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear25);" d="M 35.496094 87.503906 L 35.496094 91.503906 C 35.496094 93.722656 33.710938 95.503906 31.496094 95.503906 L 25.496094 95.503906 C 23.277344 95.503906 21.496094 93.722656 21.496094 91.503906 L 21.496094 87.503906 C 21.496094 85.289062 23.277344 83.503906 25.496094 83.503906 L 31.496094 83.503906 C 33.710938 83.503906 35.496094 85.289062 35.496094 87.503906 Z M 35.496094 87.503906 "/>
|
||||
</g>
|
||||
<clipPath id="clip1">
|
||||
<rect x="0" y="0" width="128" height="128"/>
|
||||
</clipPath>
|
||||
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
||||
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<g id="surface50198" clip-path="url(#clip1)" filter="url(#alpha)">
|
||||
<use xlink:href="#surface50195"/>
|
||||
</g>
|
||||
<mask id="mask0">
|
||||
<use xlink:href="#surface50198"/>
|
||||
</mask>
|
||||
<mask id="mask1">
|
||||
<g filter="url(#alpha)">
|
||||
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.8;stroke:none;"/>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="linear26" gradientUnits="userSpaceOnUse" x1="300" y1="235" x2="428" y2="235" gradientTransform="matrix(0.000000000000000023,0.37,-0.98462,0.00000000000000006,295.38501,-30.360001)">
|
||||
<stop offset="0" style="stop-color:rgb(97.647059%,94.117647%,41.960785%);stop-opacity:1;"/>
|
||||
<stop offset="1" style="stop-color:rgb(96.078432%,76.078433%,6.666667%);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip4">
|
||||
<rect x="0" y="0" width="128" height="128"/>
|
||||
</clipPath>
|
||||
<g id="surface50192" clip-path="url(#clip4)">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear26);" d="M 128 80.640625 L 128 128 L 0 128 L 0 80.640625 Z M 128 80.640625 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 13.308594 80.640625 L 60.664062 128 L 81.878906 128 L 34.519531 80.640625 Z M 55.730469 80.640625 L 103.09375 128 L 124.308594 128 L 76.945312 80.640625 Z M 98.160156 80.640625 L 128 110.480469 L 128 89.269531 L 119.371094 80.640625 Z M 0 88.546875 L 0 109.761719 L 18.238281 128 L 39.453125 128 Z M 0 88.546875 "/>
|
||||
</g>
|
||||
<clipPath id="clip3">
|
||||
<rect x="0" y="0" width="128" height="128"/>
|
||||
</clipPath>
|
||||
<g id="surface50197" clip-path="url(#clip3)">
|
||||
<use xlink:href="#surface50192" mask="url(#mask1)"/>
|
||||
</g>
|
||||
</defs>
|
||||
<g id="surface50185">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 16.496094 51.503906 L 112.496094 51.503906 C 116.914062 51.503906 120.496094 55.085938 120.496094 59.503906 L 120.496094 107.503906 C 120.496094 111.921875 116.914062 115.503906 112.496094 115.503906 L 16.496094 115.503906 C 12.078125 115.503906 8.496094 111.921875 8.496094 107.503906 L 8.496094 59.503906 C 8.496094 55.085938 12.078125 51.503906 16.496094 51.503906 Z M 16.496094 51.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.470588%,96.078432%,95.686275%);fill-opacity:1;" d="M 16.496094 27.503906 L 112.496094 27.503906 C 116.914062 27.503906 120.496094 31.464844 120.496094 36.347656 L 120.496094 102.664062 C 120.496094 107.546875 116.914062 111.503906 112.496094 111.503906 L 16.496094 111.503906 C 12.078125 111.503906 8.496094 107.546875 8.496094 102.664062 L 8.496094 36.347656 C 8.496094 31.464844 12.078125 27.503906 16.496094 27.503906 Z M 16.496094 27.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 21.496094 39.503906 L 27.496094 39.503906 C 29.710938 39.503906 31.273438 41.300781 31.496094 43.503906 L 32.496094 53.503906 C 32.714844 55.710938 30.710938 57.503906 28.496094 57.503906 L 20.496094 57.503906 C 18.277344 57.503906 16.273438 55.710938 16.496094 53.503906 L 17.496094 43.503906 C 17.714844 41.300781 19.277344 39.503906 21.496094 39.503906 Z M 21.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear1);" d="M 31.496094 43.503906 L 31.496094 47.503906 C 31.496094 49.722656 29.710938 51.503906 27.496094 51.503906 L 21.496094 51.503906 C 19.277344 51.503906 17.496094 49.722656 17.496094 47.503906 L 17.496094 43.503906 C 17.496094 41.289062 19.277344 39.503906 21.496094 39.503906 L 27.496094 39.503906 C 29.710938 39.503906 31.496094 41.289062 31.496094 43.503906 Z M 31.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 41.496094 39.503906 L 47.496094 39.503906 C 49.710938 39.503906 51.273438 41.300781 51.496094 43.503906 L 52.496094 53.503906 C 52.714844 55.710938 50.710938 57.503906 48.496094 57.503906 L 40.496094 57.503906 C 38.277344 57.503906 36.273438 55.710938 36.496094 53.503906 L 37.496094 43.503906 C 37.714844 41.300781 39.277344 39.503906 41.496094 39.503906 Z M 41.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 61.496094 39.503906 L 67.496094 39.503906 C 69.710938 39.503906 71.273438 41.300781 71.496094 43.503906 L 72.496094 53.503906 C 72.714844 55.710938 70.710938 57.503906 68.496094 57.503906 L 60.496094 57.503906 C 58.277344 57.503906 56.273438 55.710938 56.496094 53.503906 L 57.496094 43.503906 C 57.714844 41.300781 59.277344 39.503906 61.496094 39.503906 Z M 61.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 81.496094 39.503906 L 87.496094 39.503906 C 89.710938 39.503906 91.273438 41.300781 91.496094 43.503906 L 92.496094 53.503906 C 92.714844 55.710938 90.710938 57.503906 88.496094 57.503906 L 80.496094 57.503906 C 78.277344 57.503906 76.273438 55.710938 76.496094 53.503906 L 77.496094 43.503906 C 77.714844 41.300781 79.277344 39.503906 81.496094 39.503906 Z M 81.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 101.496094 39.503906 L 107.496094 39.503906 C 109.710938 39.503906 111.273438 41.300781 111.496094 43.503906 L 112.496094 53.503906 C 112.714844 55.710938 110.710938 57.503906 108.496094 57.503906 L 100.496094 57.503906 C 98.277344 57.503906 96.273438 55.710938 96.496094 53.503906 L 97.496094 43.503906 C 97.714844 41.300781 99.277344 39.503906 101.496094 39.503906 Z M 101.496094 39.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 31.496094 61.503906 L 37.496094 61.503906 C 39.710938 61.503906 41.273438 63.300781 41.496094 65.503906 L 42.496094 75.503906 C 42.714844 77.710938 40.710938 79.503906 38.496094 79.503906 L 30.496094 79.503906 C 28.277344 79.503906 26.273438 77.710938 26.496094 75.503906 L 27.496094 65.503906 C 27.714844 63.300781 29.277344 61.503906 31.496094 61.503906 Z M 31.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 51.496094 61.503906 L 57.496094 61.503906 C 59.710938 61.503906 61.273438 63.300781 61.496094 65.503906 L 62.496094 75.503906 C 62.714844 77.710938 60.710938 79.503906 58.496094 79.503906 L 50.496094 79.503906 C 48.277344 79.503906 46.273438 77.710938 46.496094 75.503906 L 47.496094 65.503906 C 47.714844 63.300781 49.277344 61.503906 51.496094 61.503906 Z M 51.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 71.496094 61.503906 L 77.496094 61.503906 C 79.710938 61.503906 81.273438 63.300781 81.496094 65.503906 L 82.496094 75.503906 C 82.714844 77.710938 80.710938 79.503906 78.496094 79.503906 L 70.496094 79.503906 C 68.277344 79.503906 66.273438 77.710938 66.496094 75.503906 L 67.496094 65.503906 C 67.714844 63.300781 69.277344 61.503906 71.496094 61.503906 Z M 71.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.64706%,27.450982%,0%);fill-opacity:1;" d="M 91.496094 61.503906 L 98.496094 61.503906 C 100.710938 61.503906 102.273438 63.300781 102.496094 65.503906 L 103.496094 75.503906 C 103.714844 77.710938 101.710938 79.503906 99.496094 79.503906 L 90.496094 79.503906 C 88.277344 79.503906 86.273438 77.710938 86.496094 75.503906 L 87.496094 65.503906 C 87.714844 63.300781 89.277344 61.503906 91.496094 61.503906 Z M 91.496094 61.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 25.496094 83.503906 L 31.496094 83.503906 C 33.710938 83.503906 35.273438 85.300781 35.496094 87.503906 L 36.496094 97.503906 C 36.714844 99.710938 34.710938 101.503906 32.496094 101.503906 L 24.496094 101.503906 C 22.277344 101.503906 20.273438 99.710938 20.496094 97.503906 L 21.496094 87.503906 C 21.714844 85.300781 23.277344 83.503906 25.496094 83.503906 Z M 25.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.921569%,21.960784%,27.450982%);fill-opacity:1;" d="M 97.496094 83.503906 L 103.496094 83.503906 C 105.710938 83.503906 107.273438 85.300781 107.496094 87.503906 L 108.496094 97.503906 C 108.714844 99.710938 106.710938 101.503906 104.496094 101.503906 L 96.496094 101.503906 C 94.277344 101.503906 92.273438 99.710938 92.496094 97.503906 L 93.496094 87.503906 C 93.714844 85.300781 95.277344 83.503906 97.496094 83.503906 Z M 97.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear2);" d="M 107.496094 87.503906 L 107.496094 91.503906 C 107.496094 93.722656 105.710938 95.503906 103.496094 95.503906 L 97.496094 95.503906 C 95.277344 95.503906 93.496094 93.722656 93.496094 91.503906 L 93.496094 87.503906 C 93.496094 85.289062 95.277344 83.503906 97.496094 83.503906 L 103.496094 83.503906 C 105.710938 83.503906 107.496094 85.289062 107.496094 87.503906 Z M 107.496094 87.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(50.980395%,50.588238%,52.549022%);fill-opacity:1;" d="M 45.496094 83.503906 L 83.496094 83.503906 C 85.710938 83.503906 87.273438 85.300781 87.496094 87.503906 L 88.496094 97.503906 C 88.714844 99.710938 86.710938 101.503906 84.496094 101.503906 L 44.496094 101.503906 C 42.277344 101.503906 40.273438 99.710938 40.496094 97.503906 L 41.496094 87.503906 C 41.714844 85.300781 43.277344 83.503906 45.496094 83.503906 Z M 45.496094 83.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear3);" d="M 87.496094 87.503906 L 87.496094 91.503906 C 87.496094 93.722656 85.710938 95.503906 83.496094 95.503906 L 45.496094 95.503906 C 43.277344 95.503906 41.496094 93.722656 41.496094 91.503906 L 41.496094 87.503906 C 41.496094 85.289062 43.277344 83.503906 45.496094 83.503906 L 83.496094 83.503906 C 85.710938 83.503906 87.496094 85.289062 87.496094 87.503906 Z M 87.496094 87.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear4);" d="M 51.496094 43.503906 L 51.496094 47.503906 C 51.496094 49.722656 49.710938 51.503906 47.496094 51.503906 L 41.496094 51.503906 C 39.277344 51.503906 37.496094 49.722656 37.496094 47.503906 L 37.496094 43.503906 C 37.496094 41.289062 39.277344 39.503906 41.496094 39.503906 L 47.496094 39.503906 C 49.710938 39.503906 51.496094 41.289062 51.496094 43.503906 Z M 51.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear5);" d="M 71.496094 43.503906 L 71.496094 47.503906 C 71.496094 49.722656 69.710938 51.503906 67.496094 51.503906 L 61.496094 51.503906 C 59.277344 51.503906 57.496094 49.722656 57.496094 47.503906 L 57.496094 43.503906 C 57.496094 41.289062 59.277344 39.503906 61.496094 39.503906 L 67.496094 39.503906 C 69.710938 39.503906 71.496094 41.289062 71.496094 43.503906 Z M 71.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear6);" d="M 91.496094 43.503906 L 91.496094 47.503906 C 91.496094 49.722656 89.710938 51.503906 87.496094 51.503906 L 81.496094 51.503906 C 79.277344 51.503906 77.496094 49.722656 77.496094 47.503906 L 77.496094 43.503906 C 77.496094 41.289062 79.277344 39.503906 81.496094 39.503906 L 87.496094 39.503906 C 89.710938 39.503906 91.496094 41.289062 91.496094 43.503906 Z M 91.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear7);" d="M 111.496094 43.503906 L 111.496094 47.503906 C 111.496094 49.722656 109.710938 51.503906 107.496094 51.503906 L 101.496094 51.503906 C 99.277344 51.503906 97.496094 49.722656 97.496094 47.503906 L 97.496094 43.503906 C 97.496094 41.289062 99.277344 39.503906 101.496094 39.503906 L 107.496094 39.503906 C 109.710938 39.503906 111.496094 41.289062 111.496094 43.503906 Z M 111.496094 43.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear8);" d="M 41.496094 65.503906 L 41.496094 69.503906 C 41.496094 71.722656 39.710938 73.503906 37.496094 73.503906 L 31.496094 73.503906 C 29.277344 73.503906 27.496094 71.722656 27.496094 69.503906 L 27.496094 65.503906 C 27.496094 63.289062 29.277344 61.503906 31.496094 61.503906 L 37.496094 61.503906 C 39.710938 61.503906 41.496094 63.289062 41.496094 65.503906 Z M 41.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear9);" d="M 61.496094 65.503906 L 61.496094 69.503906 C 61.496094 71.722656 59.710938 73.503906 57.496094 73.503906 L 51.496094 73.503906 C 49.277344 73.503906 47.496094 71.722656 47.496094 69.503906 L 47.496094 65.503906 C 47.496094 63.289062 49.277344 61.503906 51.496094 61.503906 L 57.496094 61.503906 C 59.710938 61.503906 61.496094 63.289062 61.496094 65.503906 Z M 61.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear10);" d="M 81.496094 65.503906 L 81.496094 69.503906 C 81.496094 71.722656 79.710938 73.503906 77.496094 73.503906 L 71.496094 73.503906 C 69.277344 73.503906 67.496094 71.722656 67.496094 69.503906 L 67.496094 65.503906 C 67.496094 63.289062 69.277344 61.503906 71.496094 61.503906 L 77.496094 61.503906 C 79.710938 61.503906 81.496094 63.289062 81.496094 65.503906 Z M 81.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear11);" d="M 102.496094 65.503906 L 102.496094 69.503906 C 102.496094 71.722656 100.710938 73.503906 98.496094 73.503906 L 91.496094 73.503906 C 89.277344 73.503906 87.496094 71.722656 87.496094 69.503906 L 87.496094 65.503906 C 87.496094 63.289062 89.277344 61.503906 91.496094 61.503906 L 98.496094 61.503906 C 100.710938 61.503906 102.496094 63.289062 102.496094 65.503906 Z M 102.496094 65.503906 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear12);" d="M 35.496094 87.503906 L 35.496094 91.503906 C 35.496094 93.722656 33.710938 95.503906 31.496094 95.503906 L 25.496094 95.503906 C 23.277344 95.503906 21.496094 93.722656 21.496094 91.503906 L 21.496094 87.503906 C 21.496094 85.289062 23.277344 83.503906 25.496094 83.503906 L 31.496094 83.503906 C 33.710938 83.503906 35.496094 85.289062 35.496094 87.503906 Z M 35.496094 87.503906 "/>
|
||||
<use xlink:href="#surface50197" mask="url(#mask0)"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 137 KiB |
|
@ -0,0 +1,10 @@
|
|||
install_data(
|
||||
'@0@.svg'.format(application_id),
|
||||
install_dir: datadir / 'icons' / 'hicolor' / 'scalable' / 'apps'
|
||||
)
|
||||
|
||||
install_data(
|
||||
'de.wolfvollprecht.UberWriter-symbolic.svg',
|
||||
install_dir: datadir / 'icons' / 'hicolor' / 'symbolic' / 'apps',
|
||||
rename: '@0@-symbolic.svg'.format(application_id)
|
||||
)
|
|
@ -6,7 +6,7 @@ function fix_path (path)
|
|||
if string.starts(path, "/") then
|
||||
return path
|
||||
else
|
||||
return (os.getenv('PANDOC_PREFIX') or '') .. path
|
||||
return (pandoc.system.get_working_directory() or '') .. "/" .. path
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Markdown Tutorial for UberWriter
|
||||
Markdown Tutorial for Apostrophe
|
||||
================================
|
||||
|
||||
I will try to give a short impressions on how I use markdown/pandocs capabilities to greatly reduce the time spent on formatting anything -- from websites to PDF Documents.
|
||||
|
@ -95,7 +95,7 @@ To give your document some meta-information and a nice title, you can use title
|
|||
|
||||
Emphasizing some text is done by surrounding it with *s:
|
||||
|
||||
This is *emphasized with asterisks*, and this will be a **bold text**. And even more ***krass***. And if you want to erase something: ~~completely gone~~ (sorrounded by ~)
|
||||
This is *emphasized with asterisks*, and this will be a **bold text**. And even more ***krass***. And if you want to erase something: ~~completely gone~~ (surrounded by ~)
|
||||
|
||||
### Horizontal Rules
|
||||
|
|
@ -1,209 +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-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" () };
|
||||
}
|
||||
|
||||
/* Main window and text colors */
|
||||
|
||||
.uberwriter-window {
|
||||
/*border-radius: 7px 7px 3px 3px;*/
|
||||
background: @background_color;
|
||||
caret-color: @foreground_color;
|
||||
}
|
||||
|
||||
.uberwriter-window .uberwriter-editor {
|
||||
font-family: 'Fira Mono', monospace;
|
||||
font-size: 16px;
|
||||
padding-top: 80px;
|
||||
padding-bottom: 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: @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;
|
||||
}
|
||||
|
||||
.uberwriter-window 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;
|
||||
}
|
||||
|
||||
/* .quick-preview-popup {
|
||||
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-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: @background_color;
|
||||
}
|
||||
|
||||
.quick-preview-popup grid {
|
||||
background-color: @background_color;
|
||||
color: @foreground_color;
|
||||
border-color: @background_color;
|
||||
}
|
||||
|
||||
.quick-preview-popup label {
|
||||
color: @foreground_color;
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
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 code_bg_color mix(@theme_base_color, @theme_fg_color, 0.05);
|
||||
|
||||
/* Main window and text colors */
|
||||
|
||||
.apostrophe-window {
|
||||
background: @theme_base_color;
|
||||
color: @theme_fg_color;
|
||||
caret-color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.apostrophe-window.focus:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen) {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.apostrophe-window.focus:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen):dir(ltr) scrollbar {
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.apostrophe-window.focus:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized):not(.fullscreen):dir(rtl) scrollbar {
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
|
||||
#titlebar-revealer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.scrollbars-junction,
|
||||
.scrollbar.trough {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#titlebar-container {
|
||||
background: @theme_base_color;
|
||||
}
|
||||
|
||||
.apostrophe-editor {
|
||||
-gtk-key-bindings: editor-bindings;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
text-decoration-color: @error_color;
|
||||
font-family: 'Fira Mono', monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.apostrophe-editor.size14 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.apostrophe-editor.size15 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.apostrophe-editor.size16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.apostrophe-editor.size17 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.apostrophe-editor.size18 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.apostrophe-editor text {
|
||||
background-color: @theme_base_color;
|
||||
color: @theme_fg_color;
|
||||
caret-color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.apostrophe-editor text selection {
|
||||
background-color: @theme_selected_bg_color;
|
||||
color: @theme_selected_fg_color;
|
||||
}
|
||||
|
||||
.apostrophe-editor button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/*background: #CCC;*/
|
||||
}
|
||||
|
||||
.apostrophe-editor toolbar {
|
||||
/*background: transparent;*/
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-button {
|
||||
color: alpha(@theme_fg_color, 0.6);
|
||||
background-color: alpha(@theme_base_color, 0.9);
|
||||
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;
|
||||
}
|
||||
|
||||
.inline-button:hover,
|
||||
.inline-button:checked {
|
||||
color: @theme_fg_color;
|
||||
background-color: mix(@theme_base_color, @theme_bg_color, 0.5);
|
||||
}
|
||||
|
||||
.apostrophe-window treeview {
|
||||
padding: 4px 4px 4px 4px;
|
||||
}
|
||||
|
||||
.lexikon {
|
||||
font-family: serif;
|
||||
font-size: 12px;
|
||||
background: @theme_bg_color;
|
||||
border: 4px solid @theme_bg_color;
|
||||
}
|
||||
|
||||
.lexikon .header {
|
||||
font-family: serif;
|
||||
font-size: 14px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.lexikon .header.first {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.lexikon .number {
|
||||
padding-left: 16px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.quick-preview-popup {
|
||||
background-color: @theme_bg_color;
|
||||
padding: 8px 12px 8px 12px;
|
||||
}
|
||||
|
||||
.quick-preview-popup grid {
|
||||
background-color: @theme_bg_color;
|
||||
color: @theme_fg_color;
|
||||
border-color: @theme_bg_color;
|
||||
}
|
||||
|
||||
.quick-preview-popup label {
|
||||
color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.plain-listview {
|
||||
background-color: @fg-color;
|
||||
}
|
||||
|
||||
/* theme selector */
|
||||
|
||||
.color-button {
|
||||
padding: 12px;
|
||||
border-radius: 999px;
|
||||
-gtk-outline-radius: 999px;
|
||||
outline-offset: 1px;
|
||||
border: none;
|
||||
-gtk-icon-shadow: none;
|
||||
min-width: 0px;
|
||||
min-height: 0px;
|
||||
color: transparent;
|
||||
transition: all 200ms ease-out;
|
||||
}
|
||||
|
||||
.color-light {
|
||||
background: #ffffff;
|
||||
box-shadow: inset 0 0 0 1px #2e3436;
|
||||
}
|
||||
|
||||
.color-light:checked {
|
||||
color: #2e3436;
|
||||
box-shadow: inset 0 0 0 2px @theme_selected_bg_color;
|
||||
}
|
||||
|
||||
.color-dark {
|
||||
background: #2d2d2d;
|
||||
box-shadow: inset 0 0 0 1px alpha(black, .35);
|
||||
}
|
||||
.color-dark:checked {
|
||||
color: #eeeeec;
|
||||
box-shadow: inset 0 0 0 2px @theme_selected_bg_color;
|
||||
}
|
||||
|
||||
.color-button:disabled {
|
||||
background: #929292;
|
||||
box-shadow: inset 0 0 0 1px #2e3436;
|
||||
}
|
|
@ -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");
|