Compare commits
557 Commits
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 | |
somas95 | f5d7f518fa | |
Gonçalo Silva | 432ef9d55e | |
Gonçalo Silva | 53d3fc6026 | |
Gonçalo Silva | 567e74c99e | |
Gonçalo Silva | fc824fc2ef | |
Gonçalo Silva | d98208e0e9 | |
Gonçalo Silva | 8159ad9e25 | |
Gonçalo Silva | c9958b6c12 | |
Gonçalo Silva | 41377c24bb | |
Gonçalo Silva | 9f41bfac8d | |
Gonçalo Silva | ce4a13278d | |
Gonçalo Silva | 351f7834a5 | |
Gonçalo Silva | 0c94f0c168 | |
Gonçalo Silva | 13ea2cc8fe | |
Gonçalo Silva | 13535ab839 | |
Gonçalo Silva | fbef0143e2 | |
Gonçalo Silva | 4b32617ca7 | |
Gonçalo Silva | 50729b0d34 | |
Manuel Genovés | 9ae1eaedda | |
Manuel Genovés | 844890507c | |
Manuel Genovés | c629137ed1 | |
Manuel Genovés | 97b4963fa7 | |
somas95 | e2cb547648 | |
Gonçalo Silva | 1842a849ad | |
Gonçalo Silva | af9d1adf7c | |
Gonçalo Silva | 58e2d2e0be | |
Gonçalo Silva | ef54c752ba | |
Gonçalo Silva | 5be563ccc7 | |
Gonçalo Silva | ac7e18b0b3 | |
Gonçalo Silva | bf657891c6 | |
Gonçalo Silva | 86c924972b | |
Gonçalo Silva | c5d2322b96 | |
Gonçalo Silva | 372d2c8a65 | |
Gonçalo Silva | 6688eb259e | |
Gonçalo Silva | 16a8ac78db | |
Gonçalo Silva | 99641125ba | |
Gonçalo Silva | 30df10cab6 | |
Gonçalo Silva | 7a9b878d02 | |
somas95 | cada4f6702 | |
Manuel Genovés | 30649551b6 | |
Manuel Genovés | 23c1b2e42a | |
Manuel Genovés | 80270a45ee | |
Wolf Vollprecht | 72760ad7d1 | |
somas95 | 37a8eb1cd1 | |
Manuel Genovés | 129041795a | |
Manuel Genovés | c91d8018dc | |
Manuel Genovés | 258000b300 | |
Manuel Genovés | d19068bc2c | |
Manuel Genovés | 3bb9e0b575 | |
Manuel Genovés | 5bf0d877d3 | |
somas95 | 0097ba667e | |
somas95 | 97fd6d62b4 | |
Gonçalo Silva | 3f87bd1d22 | |
Manuel Genovés | 6d7bfe82cb | |
somas95 | fc0c68f41c | |
Gonçalo Silva | bef52648c7 | |
Gonçalo Silva | 152db98f8a | |
Gonçalo Silva | 95e6d89514 | |
Gonçalo Silva | 52f1c9f692 | |
Gonçalo Silva | e7359c5776 | |
Gonçalo Silva | ea566b8d73 | |
Gonçalo Silva | bbc4cec049 | |
Gonçalo Silva | da7bf940f3 | |
somas95 | 52c0e87765 | |
Gonçalo Silva | 6a2de711cf | |
Gonçalo Silva | 174e027076 | |
Gonçalo Silva | 928254e992 | |
Gonçalo Silva | 4b146f69e6 | |
Manuel Genovés | a767f66a9f | |
Manuel Genovés | 741cda968a | |
Manuel Genovés | f0ad4f4d94 | |
Manuel Genovés | a0a13079fc | |
Manuel Genovés | 284d582654 | |
Manuel Genovés | 43c9584369 | |
Manuel Genovés | 886b292a10 | |
Manuel Genovés | 3130129149 | |
Manuel Genovés | f71e358c0a | |
somas95 | a1caf64686 | |
somas95 | 80f05937d7 | |
somas95 | 856bb7a04f | |
somas95 | bb8763f29b | |
somas95 | 717719599c | |
somas95 | 23270be7b8 | |
somas95 | 5d2152e744 | |
somas95 | d65008a92a | |
somas95 | 8372eee85c | |
somas95 | d12f8b6ef7 | |
Manuel Genovés | 412f468f75 | |
Manuel Genovés | fb47539dd8 | |
Manuel Genovés | 5618ff38b6 | |
Manuel Genovés | 6528702f55 | |
Manuel Genovés | 9f731d7339 | |
Manuel Genovés | 7169bf9a03 | |
Manuel Genovés | a1a50d4890 | |
Manuel Genovés | 3375349d6b | |
Manuel Genovés | 66ccfb47c3 | |
Manuel Genovés | 7d3a6fd6de | |
Manuel Genovés | aa4a878dbc | |
Manuel Genovés | 7066f446d2 | |
Manuel Genovés | 7e926b86fe | |
Manuel Genovés | a2661be802 | |
Manuel Genovés | 56e718be15 | |
Manuel Genovés | 3fd91923ee | |
Manuel Genovés | 424bb7f8f4 | |
Manuel Genovés | 209699a91c | |
somas95 | 226b4e81dc | |
somas95 | 46a6a9db09 | |
Manuel Genovés | 4623a27779 | |
Manuel Genovés | 4bb6862f0e | |
Manuel Genovés | 47c169882e | |
Manuel Genovés | d664308efd | |
Manuel Genovés | 772b1ba6db | |
Manuel Genovés | d084f24d89 | |
Manuel Genovés | 136122dd08 | |
Manuel Genovés | d589729e1f | |
Manuel Genovés | 3068f28ff0 | |
Manuel Genovés | 65f2f987f7 | |
Manuel Genovés | 69204aa650 | |
Manuel Genovés | fbc43928fd | |
Manuel Genovés | c467f23162 | |
Manuel Genovés | ee23d13ae8 | |
Manuel Genovés | d215f52fac | |
somas95 | 296bdafcef | |
somas95 | 28433a811c | |
somas95 | 0ae17b6df0 | |
somas95 | 40c6b6545f | |
somas95 | bed340306d | |
somas95 | 6bb8a3b07f | |
somas95 | 3e82acd4e5 | |
somas95 | c112999d35 | |
somas95 | 6eb4000df5 | |
somas95 | 1282e9b164 | |
somas95 | aa783cae29 | |
somas95 | 32d1e6b397 | |
somas95 | 5c1fa47c06 | |
somas95 | 8b0ff5943d | |
somas95 | b7a398b788 | |
somas95 | 9a8a72ad1f | |
somas95 | 0f22111e3b | |
somas95 | df6c368268 | |
Manuel Genovés | 528a279919 | |
Manuel Genovés | fb683114c2 | |
Manuel Genovés | 5ad641d959 | |
Manuel Genovés | 473cac657c | |
Manuel Genovés | 9288c54a36 | |
Manuel Genovés | 2e2dc33e7f | |
Manuel Genovés | cbd59b5f0b | |
Manuel Genovés | 8b3252d68e | |
Manuel Genovés | d3767c5957 | |
Manuel Genovés | 5b63b75ad3 | |
Manuel Genovés | 517791284f | |
Manuel Genovés | 355880ab1a | |
Christopher Davis | 917a92485b | |
Christopher Davis | 03f2c21f8a | |
Manuel Genovés | fcab2687f1 | |
Manuel Genovés | 540671186e | |
Manuel Genovés | ca758621d5 | |
Manuel Genovés | 61f3122afc | |
Manuel Genovés | 4e8d8fa491 | |
Manuel Genovés | fbb5dc05db | |
Manuel Genovés | 9f30b0ffda | |
Manuel Genovés | 44c648f94e | |
Manuel Genovés | 10996e49e8 | |
somas95 | 202ac174f2 | |
somas95 | ac4b69a1d1 | |
somas95 | 0dbaac828a | |
somas95 | 2b824ee029 | |
somas95 | 2744c2a87e | |
somas95 | 766b3a4b43 | |
somas95 | cf9d3d6fbc | |
somas95 | 2b2568128d | |
somas95 | 600f4b9c37 | |
somas95 | b39b89a261 | |
somas95 | b09fbba870 | |
somas95 | ed164496a0 | |
somas95 | 61e6ea0ade | |
somas95 | 2d2d7ba957 | |
somas95 | 66e6931d63 | |
somas95 | ecb76ad280 | |
somas95 | 31b680542f | |
somas95 | a48b12d962 | |
somas95 | fd7fd701dd | |
somas95 | 37eaf836e1 | |
somas95 | 5e47e68410 | |
somas95 | 50d00a8705 | |
somas95 | c9ba82e1cb | |
somas95 | c191483eca | |
somas95 | a7a81cebfe | |
somas95 | 1551c42c32 | |
somas95 | 2d575c0a60 | |
somas95 | a24f20bf89 | |
somas95 | e5cc588f3b | |
somas95 | 70f9b480d2 | |
somas95 | 0cc3108dbf | |
somas95 | a2c6df0f26 | |
somas95 | 4d5b7ede79 | |
somas95 | 5b2ec52976 | |
somas95 | d2b55e4de9 | |
somas95 | 8dfe54af09 | |
somas95 | 14528e5594 | |
somas95 | a067f32832 | |
somas95 | e5027303bc | |
somas95 | 1980afa3ab | |
somas95 | 0dcae5925a | |
somas95 | 47a44b25fd | |
somas95 | 3c480e735d | |
somas95 | c8313472f8 | |
somas95 | 1acd4f575d | |
somas95 | 1b67e9fb8f | |
somas95 | f0008cc827 | |
somas95 | 06632694a7 | |
somas95 | f4667d8774 | |
somas95 | 64fd2bf84c | |
somas95 | 5bd1b1cd45 | |
somas95 | 0e18bdc19b | |
somas95 | ea45129bf1 | |
somas95 | 59bfe731a3 | |
somas95 | 9e4be3e0c5 | |
somas95 | 52def543fe | |
somas95 | 81a9b3fb71 | |
somas95 | 2ce88698d3 | |
somas95 | 96731ef8f6 | |
somas95 | ca1a9d6364 | |
somas95 | e70f6fb82e | |
somas95 | 84f1cf28de | |
somas95 | 3ae75a4b73 | |
somas95 | 9d435b5714 | |
somas95 | 1e9994a37f | |
somas95 | b05628ab36 | |
somas95 | cad0f3adc1 | |
somas95 | 91ada48f24 | |
somas95 | afe4461504 | |
somas95 | fbf8aff97d | |
somas95 | 82ea5d2f5a | |
somas95 | 234b82102b | |
somas95 | 549f028455 | |
somas95 | f04bd7c627 | |
somas95 | bc23dbb593 | |
somas95 | 0b53b67b97 | |
somas95 | 32b0f09a04 | |
somas95 | 2b753c9031 | |
somas95 | 9a1aad7043 | |
somas95 | 9817a3a22e | |
somas95 | dbc901b4b4 | |
somas95 | 59b9a75384 | |
somas95 | 6734a0cce0 | |
somas95 | e64d10e7e3 | |
somas95 | 2507e35c14 | |
somas95 | 130716afb4 | |
somas95 | f8c2510c36 | |
somas95 | 27eed48ba8 | |
somas95 | 642f8f38e0 | |
somas95 | 80635ac4c0 | |
somas95 | 93282de5c6 | |
somas95 | 8282fbad62 | |
somas95 | bad7870e1f | |
somas95 | c4d88677ee | |
somas95 | 4aec402b09 | |
somas95 | c672091b22 | |
somas95 | 0f7f85b567 | |
somas95 | 03df4ff8ef |
|
@ -1,20 +1,11 @@
|
|||
build/lib.linux-x86_64-2.7
|
||||
*.pyc
|
||||
__pycache__/
|
||||
_build/*
|
||||
build/
|
||||
debian/uberwriter/DEBIAN
|
||||
debian/uberwriter/opt
|
||||
debian/uberwriter/usr
|
||||
bin/
|
||||
*.*~
|
||||
.vscode/
|
||||
.idea/
|
||||
builddir/*
|
||||
build-aux/flatpak/_build/*
|
||||
build-aux/flatpak/.flatpak-builder/*
|
||||
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/
|
|
@ -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
|
5
.quickly
|
@ -1,5 +0,0 @@
|
|||
project = uberwriter
|
||||
version = 12.08.1
|
||||
template = ubuntu-application
|
||||
bzrbranch = lp:~w-vollprecht/uberwriter/quickly_trunk
|
||||
lp_id = uberwriter
|
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
|
|
@ -1 +0,0 @@
|
|||
recursive-include po *
|
6
Makefile
|
@ -1,6 +0,0 @@
|
|||
all:
|
||||
python3 ./setup.py build
|
||||
|
||||
install:
|
||||
python3 ./setup.py install --prefix=/app --skip-build --optimize=1
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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://apostrophe.github.io/apostrophe/'
|
||||
license=('GPL3')
|
||||
depends=('gtk3' 'pandoc' 'gspell')
|
||||
makedepends=('python-setuptools')
|
||||
optdepends=('texlive-core' 'otf-fira-mono: Recommended font')
|
||||
provides=("$_pkgname")
|
||||
conflicts=("$_pkgname")
|
||||
source=('git+https://github.com/Apostrophe/apostrophe.git#branch=refactoring')
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd $_pkgname
|
||||
git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g'
|
||||
}
|
||||
|
||||
build() {
|
||||
cd $_pkgname
|
||||
python setup.py build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $_pkgname
|
||||
python setup.py install --skip-build --root="$pkgdir" --optimize=1
|
||||
}
|
||||
|
||||
post_install() {
|
||||
/usr/bin/glib-compile-schemas /usr/share/glib-2.0/schemas/
|
||||
}
|
||||
post_upgrade() {
|
||||
/usr/bin/glib-compile-schemas /usr/share/glib-2.0/schemas/
|
||||
}
|
||||
post_remove() {
|
||||
/usr/bin/glib-compile-schemas /usr/share/glib-2.0/schemas/
|
||||
}
|
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()
|
|
@ -0,0 +1,568 @@
|
|||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
locally-enabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=msvs
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=yes
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: he (hspell), en_IE
|
||||
# (hunspell), es_BO (hunspell), en_NZ (hunspell), es_DO (hunspell), es_MX
|
||||
# (hunspell), en_ZA (hunspell), en_IN (hunspell), en_TT (hunspell), ca
|
||||
# (aspell), ca_FR (hunspell), es_HN (hunspell), ca_AD (hunspell), es_SV
|
||||
# (hunspell), es_PA (hunspell), en_DK (hunspell), es_NI (hunspell), es_PE
|
||||
# (hunspell), en_SG (hunspell), es_UY (hunspell), en_BS (hunspell), en_BW
|
||||
# (hunspell), es_CL (hunspell), es_AR (hunspell), en_BZ (hunspell), es_CO
|
||||
# (hunspell), en_ZW (hunspell), en_HK (hunspell), es_CR (hunspell), en_NA
|
||||
# (hunspell), es_PR (hunspell), en_JM (hunspell), es_VE (hunspell), en_AG
|
||||
# (hunspell), es_CU (hunspell), en_NG (hunspell), ca_ES (hunspell), es_ES
|
||||
# (hunspell), es_ANY (hunspell), es_EC (hunspell), es_GT (hunspell), en_PH
|
||||
# (hunspell), en_GB (hunspell), en_US (hunspell), ca_IT (hunspell), es_PY
|
||||
# (hunspell), en_GH (hunspell)..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=yes
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception".
|
||||
overgeneral-exceptions=Exception
|
|
@ -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.
|
||||
|
@ -15,33 +15,21 @@
|
|||
### END LICENSE
|
||||
import sys
|
||||
|
||||
import locale
|
||||
import os
|
||||
|
||||
import gettext
|
||||
from gettext import gettext as _
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk # pylint: disable=E0611
|
||||
|
||||
from . import UberwriterWindow
|
||||
from uberwriter_lib import AppWindow
|
||||
gi.require_version('Gtk', '3.0')
|
||||
|
||||
from apostrophe import main_window
|
||||
from apostrophe import application
|
||||
from apostrophe.helpers import set_up_logging
|
||||
from apostrophe.config import get_version
|
||||
|
||||
|
||||
def main():
|
||||
'constructor for your class instances'
|
||||
# (options, args) = parse_options()
|
||||
|
||||
|
||||
# Run the application.
|
||||
app = AppWindow.Application()
|
||||
|
||||
# ~ if args:
|
||||
# ~ for arg in args:
|
||||
# ~ pass
|
||||
# ~ else:
|
||||
# ~ pass
|
||||
# ~ if options.experimental_features:
|
||||
# ~ window.use_experimental_features(True)
|
||||
|
||||
app = application.Application()
|
||||
|
||||
app.run(sys.argv)
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
# 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/>.
|
||||
|
||||
import argparse
|
||||
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 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):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, application_id="de.wolfvollprecht.UberWriter",
|
||||
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
|
||||
**kwargs)
|
||||
self.window = None
|
||||
self.settings = Settings.new()
|
||||
|
||||
def do_startup(self, *args, **kwargs):
|
||||
|
||||
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)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("open", None)
|
||||
action.connect("activate", self.on_open)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("open_recent", None)
|
||||
action.connect("activate", self.on_open_recent)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("save", None)
|
||||
action.connect("activate", self.on_save)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("search", None)
|
||||
action.connect("activate", self.on_search)
|
||||
self.add_action(action)
|
||||
|
||||
# App Menu
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"focus_mode", None, GLib.Variant.new_boolean(False))
|
||||
action.connect("change-state", self.on_focus_mode)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"hemingway_mode", None, GLib.Variant.new_boolean(False))
|
||||
action.connect("change-state", self.on_hemingway_mode)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"preview", None, GLib.Variant.new_boolean(False))
|
||||
action.connect("change-state", self.on_preview)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"fullscreen", None, GLib.Variant.new_boolean(False))
|
||||
action.connect("change-state", self.on_fullscreen)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("save_as", None)
|
||||
action.connect("activate", self.on_save_as)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("export", GLib.VariantType("s"))
|
||||
action.connect("activate", self.on_export)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("copy_html", None)
|
||||
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)
|
||||
|
||||
action = Gio.SimpleAction.new("shortcuts", None)
|
||||
action.connect("activate", self.on_shortcuts)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("open_tutorial", None)
|
||||
action.connect("activate", self.on_open_tutorial)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("about", None)
|
||||
action.connect("activate", self.on_about)
|
||||
self.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new("quit", None)
|
||||
action.connect("activate", self.on_quit)
|
||||
self.add_action(action)
|
||||
|
||||
# Stats Menu
|
||||
|
||||
stat_default = self.settings.get_string("stat-default")
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
"stat_default", GLib.VariantType.new("s"), GLib.Variant.new_string(stat_default))
|
||||
action.connect("activate", self.on_stat_default)
|
||||
self.add_action(action)
|
||||
|
||||
# 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 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"])
|
||||
self.set_accels_for_action("app.open", ["<Ctl>o"])
|
||||
self.set_accels_for_action("app.save", ["<Ctl>s"])
|
||||
self.set_accels_for_action("app.save_as", ["<Ctl><shift>s"])
|
||||
self.set_accels_for_action("app.quit", ["<Ctl>w", "<Ctl>q"])
|
||||
|
||||
def do_activate(self, *args, **kwargs):
|
||||
# We only allow a single window and raise any existing ones
|
||||
if not self.window:
|
||||
# Windows are associated with the application
|
||||
# when the last one is closed the application shuts down
|
||||
# self.window = Window(application=self, title="Apostrophe")
|
||||
self.window = MainWindow(self)
|
||||
if self.args:
|
||||
self.window.load_file(self.args[0])
|
||||
|
||||
self.window.present()
|
||||
|
||||
def do_command_line(self, _command_line):
|
||||
"""Support for command line options"""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="count", dest="verbose",
|
||||
help=_("Show debug messages (-vv debugs apostrophe also)"))
|
||||
parser.add_argument(
|
||||
"-e", "--experimental-features", help=_("Use experimental features"),
|
||||
action='store_true')
|
||||
(self.options, self.args) = parser.parse_known_args()
|
||||
|
||||
set_up_logging(self.options)
|
||||
|
||||
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":
|
||||
self._set_dark_mode ()
|
||||
elif key == "spellcheck":
|
||||
self.window.toggle_spellcheck(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()
|
||||
|
||||
def on_open(self, _action, _value):
|
||||
self.window.open_document()
|
||||
|
||||
def on_open_recent(self, file):
|
||||
self.window.load_file(file.get_current_uri())
|
||||
|
||||
def on_save(self, _action, _value):
|
||||
self.window.save_document()
|
||||
|
||||
def on_search(self, _action, _value):
|
||||
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)
|
||||
self.window.set_focus_mode(value)
|
||||
|
||||
def on_hemingway_mode(self, action, value):
|
||||
action.set_state(value)
|
||||
self.window.set_hemingway_mode(value)
|
||||
|
||||
def on_preview(self, action, value):
|
||||
action.set_state(value)
|
||||
self.window.toggle_preview(value)
|
||||
|
||||
def on_fullscreen(self, action, value):
|
||||
action.set_state(value)
|
||||
self.window.set_fullscreen(value)
|
||||
|
||||
def on_save_as(self, _action, _value):
|
||||
self.window.save_document_as()
|
||||
|
||||
def on_export(self, _action, value):
|
||||
self.window.open_advanced_export(value.get_string())
|
||||
|
||||
def on_copy_html(self, _action, _value):
|
||||
self.window.copy_html_to_clipboard()
|
||||
|
||||
def on_preferences(self, _action, _value):
|
||||
PreferencesDialog(self.settings).show(self.window)
|
||||
|
||||
def on_shortcuts(self, _action, _param):
|
||||
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_apostrophe_markdown()
|
||||
|
||||
def on_about(self, _action, _param):
|
||||
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)
|
||||
|
||||
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,32 +1,30 @@
|
|||
# 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
|
||||
from gi.repository import Gtk, Gdk
|
||||
# CURRENTLY DISABLED
|
||||
|
||||
import uberwriter_lib.pressagio as pressagio
|
||||
import enchant
|
||||
|
||||
|
||||
# d = enchant.Dict("de_DE")
|
||||
import re
|
||||
|
||||
import uberwriter_lib.pressagio.predictor
|
||||
import uberwriter_lib.pressagio.tokenizer
|
||||
import uberwriter_lib.pressagio.dbconnector
|
||||
import uberwriter_lib.pressagio.context_tracker
|
||||
import uberwriter_lib.pressagio.callback
|
||||
import os
|
||||
import pickle
|
||||
import configparser
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import pickle
|
||||
|
||||
from Levenshtein import distance
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
import configparser
|
||||
from uberwriter_lib.helpers import get_media_path
|
||||
import enchant
|
||||
|
||||
from apostrophe import pressagio
|
||||
# import apostrophe.pressagio.predictor
|
||||
# import apostrophe.pressagio.tokenizer
|
||||
# import apostrophe.pressagio.dbconnector
|
||||
# import apostrophe.pressagio.context_tracker
|
||||
# import apostrophe.pressagio.callback
|
||||
|
||||
# from Levenshtein import distance
|
||||
|
||||
from apostrophe.helpers import get_media_path
|
||||
|
||||
# Define and create PresageCallback object
|
||||
|
||||
class PressagioCallback(pressagio.callback.Callback):
|
||||
def __init__(self, buffer):
|
||||
super().__init__()
|
||||
|
@ -34,19 +32,19 @@ class PressagioCallback(pressagio.callback.Callback):
|
|||
|
||||
def past_stream(self):
|
||||
return self.buffer
|
||||
|
||||
|
||||
def future_stream(self):
|
||||
return ''
|
||||
|
||||
class UberwriterAutoCorrect:
|
||||
class AutoCorrect:
|
||||
|
||||
def show_bubble(self, iter, suggestion):
|
||||
def show_bubble(self, iterator, suggestion):
|
||||
self.suggestion = suggestion
|
||||
if self.bubble:
|
||||
self.bubble_label.set_text(suggestion)
|
||||
else:
|
||||
pos = self.TextView.get_iter_location(iter)
|
||||
pos_adjusted = self.TextView.buffer_to_window_coords(
|
||||
pos = self.text_view.get_iter_location(iterator)
|
||||
pos_adjusted = self.text_view.buffer_to_window_coords(
|
||||
Gtk.TextWindowType.TEXT, pos.x, pos.y + pos.height)
|
||||
self.bubble_eventbox = Gtk.EventBox.new()
|
||||
self.bubble = Gtk.Grid.new()
|
||||
|
@ -54,8 +52,9 @@ class UberwriterAutoCorrect:
|
|||
self.bubble_eventbox.add(self.bubble)
|
||||
self.bubble_eventbox.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
|
||||
self.bubble_eventbox.connect("button_press_event", self.clicked_bubble)
|
||||
self.TextView.add_child_in_window(self.bubble_eventbox,
|
||||
Gtk.TextWindowType.TEXT, pos_adjusted[0], pos_adjusted[1])
|
||||
self.text_view.add_child_in_window(self.bubble_eventbox,
|
||||
Gtk.TextWindowType.TEXT,
|
||||
pos_adjusted[0], pos_adjusted[1])
|
||||
|
||||
self.bubble_label = Gtk.Label.new(suggestion)
|
||||
|
||||
|
@ -68,10 +67,10 @@ class UberwriterAutoCorrect:
|
|||
self.bubble.attach(self.bubble_close_eventbox, 1, 0, 1, 1)
|
||||
self.bubble_eventbox.show_all()
|
||||
|
||||
def clicked_bubble(self, widget, data=None):
|
||||
def clicked_bubble(self, _widget, _data=None):
|
||||
self.accept_suggestion()
|
||||
|
||||
def clicked_close(self, widget, data=None):
|
||||
def clicked_close(self, _widget, _data=None):
|
||||
self.destroy_bubble()
|
||||
|
||||
def suggest(self, stump, context):
|
||||
|
@ -85,7 +84,7 @@ class UberwriterAutoCorrect:
|
|||
if self.use_pressagio:
|
||||
predictions = self.prsgio.predict(6, None)
|
||||
prediction = None
|
||||
if not len(predictions):
|
||||
if not predictions:
|
||||
if self.enchant_dict.check(stump):
|
||||
self.destroy_bubble()
|
||||
return
|
||||
|
@ -93,45 +92,46 @@ class UberwriterAutoCorrect:
|
|||
suggestions_map = []
|
||||
for suggestion in predictions:
|
||||
if suggestion in self.frequency_dict:
|
||||
suggestions_map.append({'suggestion': suggestion, 'freq': self.frequency_dict[suggestion]})
|
||||
suggestions_map.append({'suggestion': suggestion,
|
||||
'freq': self.frequency_dict[suggestion]})
|
||||
else:
|
||||
suggestions_map.append({'suggestion': suggestion, 'freq': 0})
|
||||
|
||||
|
||||
suggestions_map.sort(key=lambda x: x['freq'])
|
||||
suggestions_map.reverse()
|
||||
prediction = suggestions_map[0]
|
||||
print(predictions)
|
||||
prediction = predictions[0]
|
||||
else:
|
||||
else:
|
||||
prediction = predictions[0].word
|
||||
anchor_iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
|
||||
anchor_iter.backward_visible_word_start()
|
||||
if len(stump) >= 1:
|
||||
self.show_bubble(anchor_iter, prediction)
|
||||
|
||||
def destroy_bubble(self, *args):
|
||||
def destroy_bubble(self, *_args):
|
||||
if not self.bubble:
|
||||
return
|
||||
self.bubble.destroy()
|
||||
self.bubble = None
|
||||
self.suggestion = ''
|
||||
|
||||
def get_frequency_dict(self, language):
|
||||
def get_frequency_dict(self, _language):
|
||||
self.frequency_dict = {}
|
||||
pp_pickled = get_media_path("frequency_dict_" + self.language + ".pickle")
|
||||
if pp_pickled and os.path.isfile(pp_pickled):
|
||||
f = open(pp_pickled, 'rb')
|
||||
self.frequency_dict = pickle.load(f)
|
||||
f.close()
|
||||
frequency_file = open(pp_pickled, 'rb')
|
||||
self.frequency_dict = pickle.load(frequency_file)
|
||||
frequency_file.close()
|
||||
else:
|
||||
pp = get_media_path('wordlists/en_us_wordlist.xml')
|
||||
frequencies = ET.parse(pp)
|
||||
root = frequencies.getroot()
|
||||
for child in root:
|
||||
self.frequency_dict[child.text] = int(child.attrib['f'])
|
||||
f = open('pickled_dict', 'wb+')
|
||||
pickle.dump(self.frequency_dict, f)
|
||||
f.close()
|
||||
frequency_file = open('pickled_dict', 'wb+')
|
||||
pickle.dump(self.frequency_dict, frequency_file)
|
||||
frequency_file.close()
|
||||
|
||||
def accept_suggestion(self, append=""):
|
||||
print("called")
|
||||
|
@ -142,19 +142,20 @@ class UberwriterAutoCorrect:
|
|||
self.buffer.insert_at_cursor(self.suggestion + append)
|
||||
self.destroy_bubble()
|
||||
|
||||
def key_pressed(self, widget, event):
|
||||
def key_pressed(self, _widget, event):
|
||||
if not self.bubble:
|
||||
return False
|
||||
if event.keyval in [Gdk.KEY_Escape, Gdk.KEY_BackSpace]:
|
||||
self.destroy_bubble()
|
||||
return False
|
||||
|
||||
def text_insert(self, buffer, location,
|
||||
text, len, data=None):
|
||||
def text_insert(self, buffer, location,
|
||||
text, _length, _data=None):
|
||||
# check if at end of a word
|
||||
# if yes, check if suggestion available
|
||||
# then display suggetion
|
||||
if self.suggestion and text in [' ', '\t', '\n', '.', '?', '!', ',', ';', '\'', '"', ')', ':']:
|
||||
if self.suggestion and text in [' ', '\t', '\n', '.', '?', '!',
|
||||
',', ';', '\'', '"', ')', ':']:
|
||||
self.accept_suggestion(append=text)
|
||||
location.assign(self.buffer.get_iter_at_mark(self.buffer.get_insert()))
|
||||
elif location.ends_word():
|
||||
|
@ -174,47 +175,48 @@ class UberwriterAutoCorrect:
|
|||
print("Language changed to: %s" % language)
|
||||
|
||||
# handle 2 char cases e.g. "en"
|
||||
if(len(language) == 2):
|
||||
if len(language) == 2:
|
||||
if "en":
|
||||
language = "en_US"
|
||||
|
||||
if self.language == language:
|
||||
return
|
||||
|
||||
else:
|
||||
self.language = language
|
||||
print("Language changing")
|
||||
config_file = get_media_path("pressagio_config.ini")
|
||||
pres_config = configparser.ConfigParser()
|
||||
pres_config.read(config_file)
|
||||
pres_config.set("Database", "database", get_media_path("corpora/" + self.language + ".sqlite"))
|
||||
self.context_tracker = pressagio.context_tracker.ContextTracker(
|
||||
pres_config, self.predictor_registry, self.callback)
|
||||
self.prsgio = self.predictor_registry[0]
|
||||
self.language = language
|
||||
print("Language changing")
|
||||
config_file = get_media_path("pressagio_config.ini")
|
||||
pres_config = configparser.ConfigParser()
|
||||
pres_config.read(config_file)
|
||||
pres_config.set("Database", "database",
|
||||
get_media_path("corpora/" + self.language + ".sqlite"))
|
||||
self.context_tracker = pressagio.context_tracker.ContextTracker(
|
||||
pres_config, self.predictor_registry, self.callback)
|
||||
self.prsgio = self.predictor_registry[0]
|
||||
|
||||
self.enchant_dict = enchant.Dict(self.language)
|
||||
self.enchant_dict = enchant.Dict(self.language)
|
||||
|
||||
def __init__(self, textview, textbuffer):
|
||||
self.TextView = textview
|
||||
self.text_view = textview
|
||||
self.buffer = textbuffer
|
||||
self.suggestion = ""
|
||||
self.bubble = self.bubble_label = None
|
||||
self.buffer.connect_after('insert-text', self.text_insert)
|
||||
self.TextView.connect('key-press-event', self.key_pressed)
|
||||
self.text_view.connect('key-press-event', self.key_pressed)
|
||||
|
||||
self.language = "en_US"
|
||||
self.frequency_dict = {}
|
||||
self.get_frequency_dict(self.language)
|
||||
self.enchant_dict = enchant.Dict(self.language)
|
||||
|
||||
|
||||
self.use_pressagio = False
|
||||
config_file = get_media_path("pressagio_config.ini")
|
||||
pres_config = configparser.ConfigParser()
|
||||
pres_config.read(config_file)
|
||||
pres_config.set("Database", "database", get_media_path("corpora/" + self.language + ".sqlite"))
|
||||
pres_config.set("Database", "database",
|
||||
get_media_path("corpora/" + self.language + ".sqlite"))
|
||||
self.callback = PressagioCallback("")
|
||||
|
||||
self.predictor_registry = pressagio.predictor.PredictorRegistry(pres_config)
|
||||
self.context_tracker = pressagio.context_tracker.ContextTracker(
|
||||
pres_config, self.predictor_registry, self.callback)
|
||||
self.prsgio = self.predictor_registry[0]
|
||||
self.prsgio = self.predictor_registry[0]
|
|
@ -1,34 +1,35 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# 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
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
### END LICENSE
|
||||
# END LICENSE
|
||||
|
||||
### DO NOT EDIT THIS FILE ###
|
||||
|
||||
'''Enhances builder connections, provides object to access glade objects'''
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GObject, Gtk # pylint: disable=E0611
|
||||
|
||||
import inspect
|
||||
import functools
|
||||
import logging
|
||||
logger = logging.getLogger('uberwriter_lib')
|
||||
|
||||
from xml.etree.cElementTree import ElementTree
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0') # pylint: disable=wrong-import-position
|
||||
from gi.repository import GObject, Gtk # pylint: disable=E0611
|
||||
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
# this module is big so uses some conventional prefixes and postfixes
|
||||
# *s list, except self.widgets is a dictionary
|
||||
# *_dict dictionary
|
||||
|
@ -57,7 +58,7 @@ class Builder(Gtk.Builder):
|
|||
# pylint: disable=R0201
|
||||
# this is a method so that a subclass of Builder can redefine it
|
||||
def default_handler(self,
|
||||
handler_name, filename, *args, **kwargs):
|
||||
handler_name, filename, *args, **kwargs):
|
||||
'''helps the apprentice guru
|
||||
|
||||
glade defined handlers that do not exist come here instead.
|
||||
|
@ -65,7 +66,7 @@ class Builder(Gtk.Builder):
|
|||
now he can define any likely candidates in glade and notice which
|
||||
ones get triggered when he plays with the project.
|
||||
this method does not appear in Gtk.Builder'''
|
||||
logger.debug('''tried to call non-existent function:%s()
|
||||
LOGGER.debug('''tried to call non-existent function:%s()
|
||||
expected in %s
|
||||
args:%s
|
||||
kwargs:%s''', handler_name, filename, args, kwargs)
|
||||
|
@ -101,8 +102,8 @@ class Builder(Gtk.Builder):
|
|||
|
||||
connections = [
|
||||
(name,
|
||||
ele_signal.attrib['name'],
|
||||
ele_signal.attrib['handler']) for ele_signal in ele_signals]
|
||||
ele_signal.attrib['name'],
|
||||
ele_signal.attrib['handler']) for ele_signal in ele_signals]
|
||||
|
||||
if connections:
|
||||
self.connections.extend(connections)
|
||||
|
@ -110,7 +111,7 @@ class Builder(Gtk.Builder):
|
|||
ele_signals = tree.getiterator("signal")
|
||||
for ele_signal in ele_signals:
|
||||
self.glade_handler_dict.update(
|
||||
{ele_signal.attrib["handler"]: None})
|
||||
{ele_signal.attrib["handler"]: None})
|
||||
|
||||
def connect_signals(self, callback_obj):
|
||||
'''connect the handlers defined in glade
|
||||
|
@ -131,8 +132,8 @@ class Builder(Gtk.Builder):
|
|||
connection_dict[item[0]] = handler
|
||||
|
||||
# replace the run time warning
|
||||
logger.warn("expected handler '%s' in %s",
|
||||
item[0], filename)
|
||||
LOGGER.warning("expected handler '%s' in %s",
|
||||
item[0], filename)
|
||||
|
||||
# connect glade define handlers
|
||||
Gtk.Builder.connect_signals(self, connection_dict)
|
||||
|
@ -140,8 +141,8 @@ class Builder(Gtk.Builder):
|
|||
# let's tell the user how we applied the glade design
|
||||
for connection in self.connections:
|
||||
widget_name, signal_name, handler_name = connection
|
||||
logger.debug("connect builder by design '%s', '%s', '%s'",
|
||||
widget_name, signal_name, handler_name)
|
||||
LOGGER.debug("connect builder by design '%s', '%s', '%s'",
|
||||
widget_name, signal_name, handler_name)
|
||||
|
||||
def get_ui(self, callback_obj=None, by_name=True):
|
||||
'''Creates the ui object with widgets as attributes
|
||||
|
@ -167,6 +168,7 @@ class Builder(Gtk.Builder):
|
|||
# apart from the glade widgets
|
||||
class UiFactory():
|
||||
''' provides an object with attributes as glade widgets'''
|
||||
|
||||
def __init__(self, widget_dict):
|
||||
self._widget_dict = widget_dict
|
||||
for (widget_name, widget) in widget_dict.items():
|
||||
|
@ -177,14 +179,14 @@ class UiFactory():
|
|||
cannot_message = """cannot bind ui.%s, name already exists
|
||||
consider using a pythonic name instead of design name '%s'"""
|
||||
consider_message = """consider using a pythonic name instead of design name '%s'"""
|
||||
|
||||
|
||||
for (widget_name, widget) in widget_dict.items():
|
||||
pyname = make_pyname(widget_name)
|
||||
if pyname != widget_name:
|
||||
if hasattr(self, pyname):
|
||||
logger.debug(cannot_message, pyname, widget_name)
|
||||
LOGGER.debug(cannot_message, pyname, widget_name)
|
||||
else:
|
||||
logger.debug(consider_message, widget_name)
|
||||
LOGGER.debug(consider_message, widget_name)
|
||||
setattr(self, pyname, widget)
|
||||
|
||||
def iterator():
|
||||
|
@ -203,14 +205,14 @@ def make_pyname(name):
|
|||
pyname = ''
|
||||
for character in name:
|
||||
if (character.isalpha() or character == '_' or
|
||||
(pyname and character.isdigit())):
|
||||
(pyname and character.isdigit())):
|
||||
pyname += character
|
||||
else:
|
||||
pyname += '_'
|
||||
return pyname
|
||||
|
||||
|
||||
# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
|
||||
# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we
|
||||
# need to reimplement inspect.getmembers. GObject introspection doesn't
|
||||
# play nice with it.
|
||||
def getmembers(obj, check):
|
||||
|
@ -233,10 +235,10 @@ def dict_from_callback_obj(callback_obj):
|
|||
aliased_methods = [x[1] for x in methods if hasattr(x[1], 'aliases')]
|
||||
|
||||
# a method may have several aliases
|
||||
#~ @alias('on_btn_foo_clicked')
|
||||
#~ @alias('on_tool_foo_activate')
|
||||
#~ on_menu_foo_activate():
|
||||
#~ pass
|
||||
# ~ @alias('on_btn_foo_clicked')
|
||||
# ~ @alias('on_tool_foo_activate')
|
||||
# ~ on_menu_foo_activate():
|
||||
# ~ pass
|
||||
alias_groups = [(x.aliases, x) for x in aliased_methods]
|
||||
|
||||
aliases = []
|
||||
|
@ -287,13 +289,13 @@ def auto_connect_by_name(callback_obj, builder):
|
|||
handler_names.append("on_%s" % sig)
|
||||
|
||||
do_connect(item, sig, handler_names,
|
||||
callback_handler_dict, builder.connections)
|
||||
callback_handler_dict, builder.connections)
|
||||
|
||||
log_unconnected_functions(callback_handler_dict, builder.connections)
|
||||
|
||||
|
||||
def do_connect(item, signal_name, handler_names,
|
||||
callback_handler_dict, connections):
|
||||
callback_handler_dict, connections):
|
||||
'''connect this signal to an unused handler'''
|
||||
widget_name, widget = item
|
||||
|
||||
|
@ -305,8 +307,8 @@ def do_connect(item, signal_name, handler_names,
|
|||
widget.connect(signal_name, callback_handler_dict[handler_name])
|
||||
connections.append(connection)
|
||||
|
||||
logger.debug("connect builder by name '%s','%s', '%s'",
|
||||
widget_name, signal_name, handler_name)
|
||||
LOGGER.debug("connect builder by name '%s','%s', '%s'",
|
||||
widget_name, signal_name, handler_name)
|
||||
|
||||
|
||||
def log_unconnected_functions(callback_handler_dict, connections):
|
||||
|
@ -324,4 +326,4 @@ def log_unconnected_functions(callback_handler_dict, connections):
|
|||
pass
|
||||
|
||||
for handler_name in unconnected:
|
||||
logger.debug("Not connected to builder '%s'", handler_name)
|
||||
LOGGER.debug("Not connected to builder '%s'", handler_name)
|
|
@ -1,16 +1,16 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2012, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# 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
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
### END LICENSE
|
||||
|
||||
|
@ -24,15 +24,13 @@ __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'
|
||||
|
||||
import os
|
||||
|
||||
from locale import gettext as _
|
||||
|
||||
class project_path_not_found(Exception):
|
||||
class ProjectPathNotFound(Exception):
|
||||
"""Raised when we can't find the project directory."""
|
||||
|
||||
|
||||
|
@ -47,28 +45,27 @@ def get_data_file(*path_segments):
|
|||
|
||||
|
||||
def get_data_path():
|
||||
"""Retrieve uberwriter data path
|
||||
"""Retrieve apostrophe data path
|
||||
|
||||
This path is by default <uberwriter_lib_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/opt/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 = '/opt/uberwriter/data/'
|
||||
abs_data_path = '/usr/share/apostrophe/'
|
||||
elif not os.path.exists(abs_data_path):
|
||||
raise project_path_not_found
|
||||
raise ProjectPathNotFound
|
||||
|
||||
return abs_data_path
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
# -*- 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
|
||||
"""Manages all the export operations and dialogs
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
from gettext import gettext as _
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib
|
||||
|
||||
from apostrophe import helpers
|
||||
from apostrophe.theme import Theme
|
||||
|
||||
LOGGER = logging.getLogger('apostrophe')
|
||||
|
||||
|
||||
class Export:
|
||||
"""
|
||||
Manages all the export operations and dialogs
|
||||
"""
|
||||
|
||||
__gtype_name__ = "export_dialog"
|
||||
|
||||
formats = [
|
||||
{
|
||||
"name": "LaTeX (pdf)",
|
||||
"ext": "pdf",
|
||||
"to": "pdf"
|
||||
},
|
||||
{
|
||||
"name": "LaTeX Beamer Slideshow (pdf)",
|
||||
"ext": "pdf",
|
||||
"to": "beamer"
|
||||
},
|
||||
{
|
||||
"name": "LaTeX (tex)",
|
||||
"ext": "tex",
|
||||
"to": "latex"
|
||||
},
|
||||
{
|
||||
"name": "LaTeX Beamer Slideshow (tex)",
|
||||
"ext": "tex",
|
||||
"to": "beamer"
|
||||
},
|
||||
{
|
||||
"name": "ConTeXt",
|
||||
"ext": "tex",
|
||||
"to": "context"
|
||||
},
|
||||
{
|
||||
"name": "HTML",
|
||||
"ext": "html",
|
||||
"to": "html"
|
||||
},
|
||||
{
|
||||
"name": "HTML and JavaScript Slideshow (Slidy)",
|
||||
"ext": "html",
|
||||
"to": "slidy"
|
||||
},
|
||||
{
|
||||
"name": "HTML and JavaScript Slideshow (Slideous)",
|
||||
"ext": "html",
|
||||
"to": "slideous"
|
||||
},
|
||||
{
|
||||
"name": "HTML5 and JavaScript Slideshow (DZSlides)",
|
||||
"ext": "html",
|
||||
"to": "dzslides"
|
||||
},
|
||||
{
|
||||
"name": "HTML5 and JavaScript Slideshow (reveal.js)",
|
||||
"ext": "html",
|
||||
"to": "revealjs"
|
||||
},
|
||||
{
|
||||
"name": "HTML and JavaScript Slideshow (S5)",
|
||||
"ext": "html",
|
||||
"to": "s5"
|
||||
},
|
||||
{
|
||||
"name": "Textile",
|
||||
"ext": "txt",
|
||||
"to": "textile"
|
||||
},
|
||||
{
|
||||
"name": "reStructuredText",
|
||||
"ext": "txt",
|
||||
"to": "rst"
|
||||
},
|
||||
{
|
||||
"name": "MediaWiki Markup",
|
||||
"ext": "txt",
|
||||
"to": "mediawiki"
|
||||
},
|
||||
{
|
||||
"name": "OpenDocument (xml)",
|
||||
"ext": "xml",
|
||||
"to": "opendocument"
|
||||
},
|
||||
{
|
||||
"name": "OpenDocument (texi)",
|
||||
"ext": "texi",
|
||||
"to": "texinfo"
|
||||
},
|
||||
{
|
||||
"name": "LibreOffice Text Document",
|
||||
"ext": "odt",
|
||||
"to": "odt"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft Word (docx)",
|
||||
"ext": "docx",
|
||||
"to": "docx"
|
||||
},
|
||||
{
|
||||
"name": "Rich Text Format",
|
||||
"ext": "rtf",
|
||||
"to": "rtf"
|
||||
},
|
||||
{
|
||||
"name": "Groff Man",
|
||||
"ext": "man",
|
||||
"to": "man"
|
||||
},
|
||||
{
|
||||
"name": "EPUB v3",
|
||||
"ext": "epub",
|
||||
"to": "epub"
|
||||
}
|
||||
]
|
||||
|
||||
def __init__(self, filename, export_format, text):
|
||||
"""Set up the export dialog"""
|
||||
|
||||
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.filename = filename or _("Untitled document.md")
|
||||
self.export_format = export_format
|
||||
|
||||
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 (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))
|
||||
|
||||
format_store = Gtk.ListStore(int, str)
|
||||
for i, fmt in enumerate(self.formats):
|
||||
format_store.append([i, fmt["name"]])
|
||||
self.format_field = self.builder.get_object('choose_format')
|
||||
self.format_field.set_model(format_store)
|
||||
|
||||
format_renderer = Gtk.CellRendererText()
|
||||
self.format_field.pack_start(format_renderer, True)
|
||||
self.format_field.add_attribute(format_renderer, "text", 1)
|
||||
self.format_field.set_active(0)
|
||||
|
||||
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.
|
||||
|
||||
Keyword Arguments:
|
||||
text {str} -- Text to export (default: {""})
|
||||
"""
|
||||
|
||||
args = []
|
||||
if export_type == "advanced":
|
||||
filename = self.adv_export_name.get_text()
|
||||
|
||||
# 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()]
|
||||
to = fmt["to"]
|
||||
ext = fmt["ext"]
|
||||
|
||||
if self.builder.get_object("html5").get_active() and to == "html":
|
||||
to = "html5"
|
||||
if self.builder.get_object("smart").get_active():
|
||||
to += "+smart"
|
||||
|
||||
args.extend(self.get_advanced_arguments(to, ext))
|
||||
|
||||
else:
|
||||
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))
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
to = export_type
|
||||
ext = export_type
|
||||
|
||||
if export_type == "html":
|
||||
to = "html5"
|
||||
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, to_fmt, ext_fmt):
|
||||
"""Retrieve a list of the selected advanced arguments
|
||||
|
||||
For most of the advanced option checkboxes, returns a list
|
||||
of the related pandoc flags
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
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",
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("highlight").get_active(),
|
||||
"yes": "--highlight-style=%s" % highlight_style,
|
||||
"no": "--no-highlight"
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("standalone").get_active(),
|
||||
"yes": "--standalone",
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("number_sections").get_active(),
|
||||
"yes": "--number-sections",
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("strict").get_active(),
|
||||
"yes": "--strict",
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("incremental").get_active(),
|
||||
"yes": "--incremental",
|
||||
"no": None
|
||||
},
|
||||
{
|
||||
"condition": self.builder.get_object("self_contained").get_active(),
|
||||
"yes": "--self-contained",
|
||||
"no": None
|
||||
}
|
||||
]
|
||||
|
||||
args = []
|
||||
|
||||
args.extend([c["yes"] if c["condition"] else c["no"] for c in conditions])
|
||||
|
||||
args = list(filter(lambda arg: arg is not None, args))
|
||||
|
||||
css_uri = self.builder.get_object("css_filechooser").get_uri()
|
||||
if css_uri:
|
||||
if css_uri.startswith("file://"):
|
||||
css_uri = css_uri[7:]
|
||||
args.append("--css=%s" % css_uri)
|
||||
|
||||
bib_uri = self.builder.get_object("bib_filechooser").get_uri()
|
||||
if bib_uri:
|
||||
if bib_uri.startswith("file://"):
|
||||
bib_uri = bib_uri[7:]
|
||||
args.append("--bibliography=%s" % bib_uri)
|
||||
|
||||
return args
|
||||
|
||||
def get_paper_size(self):
|
||||
paper_size = self.paper_size.get_active_text()
|
||||
|
||||
paper_formats = {
|
||||
"A4": "a4",
|
||||
"US Letter": "letter"
|
||||
}
|
||||
|
||||
return paper_formats[paper_size]
|
||||
|
||||
def disabled_text():
|
||||
"""Return the TexLive installation instructions
|
||||
|
||||
Returns:
|
||||
{str} -- [TexLive installation instructions]
|
||||
"""
|
||||
|
||||
if os.path.isfile("/.flatpak-info"):
|
||||
text = _("Please, install the TexLive extension from Gnome Software or running\n")\
|
||||
+ ("flatpak install flathub de.wolfvollprecht.UberWriter.Plugin.TexLive")
|
||||
else:
|
||||
text = _("Please, install TexLive from your distribuiton repositories")
|
||||
return text
|
|
@ -1,8 +1,10 @@
|
|||
import re
|
||||
import gi
|
||||
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)
|
|
@ -0,0 +1,214 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
# BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# END LICENSE
|
||||
|
||||
### DO NOT EDIT THIS FILE ###
|
||||
|
||||
"""Helpers for the application."""
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from contextlib import contextmanager
|
||||
|
||||
import gi
|
||||
import pypandoc
|
||||
from gi.overrides.Pango import Pango
|
||||
|
||||
from apostrophe.settings import Settings
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk # pylint: disable=E0611
|
||||
|
||||
from apostrophe.config import get_data_file
|
||||
from apostrophe.builder import 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
|
||||
|
||||
|
||||
def get_media_file(media_file_path):
|
||||
"""Return the full path of a given filename under the media dir
|
||||
(starts with file:///)
|
||||
"""
|
||||
|
||||
return path_to_file(get_media_path(media_file_path))
|
||||
|
||||
|
||||
def get_media_path(media_file_name):
|
||||
"""Return the full path of a given filename under the media dir
|
||||
(doesn't start with file:///)
|
||||
"""
|
||||
|
||||
media_path = get_data_file('media', '%s' % (media_file_name,))
|
||||
if not os.path.exists(media_path):
|
||||
media_path = None
|
||||
return media_path
|
||||
|
||||
|
||||
def get_css_path(css_file_name):
|
||||
"""Return the full path of a given filename under the css dir
|
||||
(doesn't start with file:///)
|
||||
"""
|
||||
return get_media_path("css/{}".format(css_file_name))
|
||||
|
||||
|
||||
def get_script_path(script_file_name):
|
||||
"""Return the full path of a given filename under the script dir
|
||||
"""
|
||||
script_path = get_data_file('lua', '%s' % (script_file_name,))
|
||||
if not os.path.exists(script_path):
|
||||
script_path = None
|
||||
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
|
||||
|
||||
|
||||
def set_up_logging(opts):
|
||||
# add a handler to prevent basicConfig
|
||||
root = logging.getLogger()
|
||||
null_handler = NullHandler()
|
||||
root.addHandler(null_handler)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(levelname)s:%(name)s: %(funcName)s() '%(message)s'")
|
||||
|
||||
logger = logging.getLogger('apostrophe')
|
||||
logger_sh = logging.StreamHandler()
|
||||
logger_sh.setFormatter(formatter)
|
||||
logger.addHandler(logger_sh)
|
||||
|
||||
lib_logger = logging.getLogger('apostrophe')
|
||||
lib_logger_sh = logging.StreamHandler()
|
||||
lib_logger_sh.setFormatter(formatter)
|
||||
lib_logger.addHandler(lib_logger_sh)
|
||||
|
||||
# Set the logging level to show debug messages.
|
||||
if opts.verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.debug('logging enabled')
|
||||
if opts.verbose and opts.verbose > 1:
|
||||
lib_logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def get_help_uri(page=None):
|
||||
# help_uri from source tree - default language
|
||||
here = os.path.dirname(__file__)
|
||||
help_uri = os.path.abspath(os.path.join(here, '..', 'help', 'C'))
|
||||
|
||||
if not os.path.exists(help_uri):
|
||||
# installed so use gnome help tree - user's language
|
||||
help_uri = 'apostrophe'
|
||||
|
||||
# unspecified page is the index.page
|
||||
if page is not None:
|
||||
help_uri = '%s#%s' % (help_uri, page)
|
||||
|
||||
return help_uri
|
||||
|
||||
|
||||
def show_uri(parent, link):
|
||||
screen = parent.get_screen()
|
||||
Gtk.show_uri(screen, link, Gtk.get_current_event_time())
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def exist_executable(command):
|
||||
"""return if a command can be executed in the SO
|
||||
|
||||
Arguments:
|
||||
command {str} -- a command
|
||||
|
||||
Returns:
|
||||
{bool} -- if the given command exists in the system
|
||||
"""
|
||||
|
||||
return shutil.which(command) is not None
|
||||
|
||||
|
||||
def get_descendant(widget, child_name, level, doPrint=False):
|
||||
if widget is not None:
|
||||
if doPrint: print("-" * level + str(Gtk.Buildable.get_name(widget)) +
|
||||
" :: " + widget.get_name())
|
||||
else:
|
||||
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() !
|
||||
return widget
|
||||
# /*** 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)
|
||||
# /*** Ity might have many children, so search them ***/
|
||||
elif (hasattr(widget, 'get_children') and
|
||||
callable(getattr(widget, 'get_children')) and
|
||||
child_name != ""):
|
||||
children = widget.get_children()
|
||||
# /*** For each child ***/
|
||||
found = None
|
||||
for child in children:
|
||||
if child is not None:
|
||||
found = get_descendant(child, child_name, level + 1, doPrint) # //search the child
|
||||
if found: return found
|
||||
|
||||
|
||||
def get_char_width(widget):
|
||||
return Pango.units_to_double(
|
||||
widget.get_pango_context().get_metrics().get_approximate_char_width())
|
||||
|
||||
|
||||
def pandoc_convert(text, to="html5", args=[], outputfile=None):
|
||||
fr = Settings.new().get_value('input-format').get_string() or "markdown"
|
||||
args.extend(["--quiet"])
|
||||
return pypandoc.convert_text(text, to, fr, extra_args=args, outputfile=outputfile)
|
|
@ -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 |
|
@ -0,0 +1,117 @@
|
|||
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
|
||||
### BEGIN LICENSE
|
||||
# Copyright (C) 2019, Wolf Vollprecht <w.vollprecht@gmail.com>
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License version 3, as published
|
||||
# by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
||||
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
### END LICENSE
|
||||
|
||||
### DO NOT EDIT THIS FILE ###
|
||||
|
||||
"""this dialog adjusts values in gsettings
|
||||
"""
|
||||
import webbrowser
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Pango, GLib # pylint: disable=E0611
|
||||
import logging
|
||||
logger = logging.getLogger('apostrophe')
|
||||
|
||||
|
||||
|
||||
class PreferencesDialog:
|
||||
|
||||
__gtype_name__ = "PreferencesDialog"
|
||||
|
||||
formats = [
|
||||
{
|
||||
"name": "Pandoc's Markdown",
|
||||
"format": "markdown",
|
||||
"help": "https://pandoc.org/MANUAL.html#pandocs-markdown"
|
||||
},
|
||||
{
|
||||
"name": "CommonMark",
|
||||
"format": "commonmark",
|
||||
"help": "https://commonmark.org"
|
||||
},
|
||||
{
|
||||
"name": "GitHub Flavored Markdown",
|
||||
"format": "gfm",
|
||||
"help": "https://help.github.com/en/categories/writing-on-github"
|
||||
},
|
||||
{
|
||||
"name": "MultiMarkdown",
|
||||
"format": "markdown_mmd",
|
||||
"help": "https://fletcherpenney.net/multimarkdown"
|
||||
},
|
||||
{
|
||||
"name": "Plain Markdown",
|
||||
"format": "markdown_strict",
|
||||
"help": "https://daringfireball.net/projects/markdown"
|
||||
}
|
||||
]
|
||||
|
||||
def __init__(self, settings):
|
||||
self.settings = settings
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_resource(
|
||||
"/de/wolfvollprecht/UberWriter/ui/Preferences.ui")
|
||||
|
||||
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)
|
||||
|
||||
input_format_store = Gtk.ListStore(int, str)
|
||||
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"]])
|
||||
if fmt["format"] == input_format:
|
||||
input_format_active = i
|
||||
self.input_format_combobox = self.builder.get_object("input_format_combobox")
|
||||
self.input_format_combobox.set_model(input_format_store)
|
||||
input_format_renderer = Gtk.CellRendererText()
|
||||
self.input_format_combobox.pack_start(input_format_renderer, True)
|
||||
self.input_format_combobox.add_attribute(input_format_renderer, "text", 1)
|
||||
self.input_format_combobox.set_active(input_format_active)
|
||||
self.input_format_combobox.connect("changed", self.on_input_format)
|
||||
|
||||
self.input_format_help_button = self.builder.get_object("input_format_help_button")
|
||||
self.input_format_help_button.connect('clicked', self.on_input_format_help)
|
||||
|
||||
def show(self, window):
|
||||
preferences_window = self.builder.get_object("PreferencesWindow")
|
||||
preferences_window.set_application(window.get_application())
|
||||
preferences_window.set_transient_for(window)
|
||||
preferences_window.show()
|
||||
|
||||
def on_autohide_headerbar(self, _, state):
|
||||
self.settings.set_boolean("autohide-headerbar", state)
|
||||
return False
|
||||
|
||||
def on_spellcheck(self, _, state):
|
||||
self.settings.set_boolean("spellcheck", state)
|
||||
return False
|
||||
|
||||
def on_input_format(self, combobox):
|
||||
fmt = self.formats[combobox.get_active()]
|
||||
self.settings.set_string("input-format", fmt["format"])
|
||||
|
||||
def on_input_format_help(self, _):
|
||||
fmt = self.formats[self.input_format_combobox.get_active()]
|
||||
webbrowser.open(fmt["help"])
|
||||
|
|
@ -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
|
|
@ -0,0 +1,195 @@
|
|||
# -*- 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 logging
|
||||
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('apostrophe')
|
||||
|
||||
|
||||
class SearchAndReplace:
|
||||
"""
|
||||
Adds (regex) search and replace functionality to
|
||||
apostrophe
|
||||
"""
|
||||
|
||||
def __init__(self, parentwindow, textview, builder):
|
||||
self.parentwindow = parentwindow
|
||||
self.textview = textview
|
||||
self.textbuffer = textview.get_buffer()
|
||||
|
||||
self.box = builder.get_object("searchbar_placeholder")
|
||||
self.box.set_reveal_child(False)
|
||||
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 = builder.get_object("replace")
|
||||
self.open_replace_button.connect("toggled", self.toggle_replace)
|
||||
|
||||
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 = builder.get_object("replace_placeholder")
|
||||
self.replacebox.set_reveal_child(False)
|
||||
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)
|
||||
self.replaceentry.connect('activate', self.replace_clicked)
|
||||
|
||||
self.nextbutton.connect('clicked', self.scrolltonext)
|
||||
self.prevbutton.connect('clicked', self.scrolltoprev)
|
||||
self.regexbutton.connect('toggled', self.search)
|
||||
self.casesensitivebutton.connect('toggled', self.search)
|
||||
self.highlight = self.textbuffer.create_tag('search_highlight', background="yellow")
|
||||
|
||||
self.textview.connect("focus-in-event", self.focused_texteditor)
|
||||
|
||||
self.matches = []
|
||||
self.active = 0
|
||||
|
||||
def toggle_replace(self, widget, _data=None):
|
||||
"""toggle the replace box
|
||||
"""
|
||||
self.replacebox.set_reveal_child(widget.get_active())
|
||||
|
||||
def key_pressed(self, _widget, event, _data=None):
|
||||
"""hide the search and replace content box when ESC is pressed
|
||||
"""
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.hide()
|
||||
|
||||
def focused_texteditor(self, _widget, _data=None):
|
||||
"""hide the search and replace content box
|
||||
"""
|
||||
self.hide()
|
||||
|
||||
def toggle_search(self, replace=False):
|
||||
"""
|
||||
toggle search box
|
||||
"""
|
||||
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)
|
||||
|
||||
def search(self, _widget=None, _data=None, scroll=True):
|
||||
searchtext = self.searchentry.get_text()
|
||||
context_start = self.textbuffer.get_start_iter()
|
||||
context_end = self.textbuffer.get_end_iter()
|
||||
text = self.textbuffer.get_slice(context_start, context_end, False)
|
||||
|
||||
self.textbuffer.remove_tag(self.highlight, context_start, context_end)
|
||||
|
||||
# case sensitive?
|
||||
flags = False
|
||||
if not self.casesensitivebutton.get_active():
|
||||
flags = flags | re.I
|
||||
|
||||
# regex?
|
||||
if not self.regexbutton.get_active():
|
||||
searchtext = re.escape(searchtext)
|
||||
|
||||
matches = re.finditer(searchtext, text, flags)
|
||||
|
||||
self.matches = []
|
||||
self.active = 0
|
||||
for match in matches:
|
||||
self.matches.append((match.start(), match.end()))
|
||||
start_iter = self.textbuffer.get_iter_at_offset(match.start())
|
||||
end_iter = self.textbuffer.get_iter_at_offset(match.end())
|
||||
self.textbuffer.apply_tag(self.highlight, start_iter, end_iter)
|
||||
if scroll:
|
||||
self.scrollto(self.active)
|
||||
LOGGER.debug(searchtext)
|
||||
|
||||
def scrolltonext(self, _widget, _data=None):
|
||||
self.scrollto(self.active + 1)
|
||||
|
||||
def scrolltoprev(self, _widget, _data=None):
|
||||
self.scrollto(self.active - 1)
|
||||
|
||||
def scrollto(self, index):
|
||||
if not self.matches:
|
||||
return
|
||||
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):
|
||||
self.box.set_reveal_child(False)
|
||||
self.textbuffer.remove_tag(self.highlight,
|
||||
self.textbuffer.get_start_iter(),
|
||||
self.textbuffer.get_end_iter())
|
||||
self.textview.grab_focus()
|
||||
|
||||
def replace_clicked(self, _widget, _data=None):
|
||||
self.replace(self.active)
|
||||
|
||||
def replace_all(self, _widget=None, _data=None):
|
||||
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):
|
||||
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):
|
||||
start_iter = self.textbuffer.get_iter_at_offset(match[0])
|
||||
end_iter = self.textbuffer.get_iter_at_offset(match[1])
|
||||
self.textbuffer.delete(start_iter, end_iter)
|
||||
start_iter = self.textbuffer.get_iter_at_offset(match[0])
|
||||
self.textbuffer.insert(start_iter, self.replaceentry.get_text())
|
|
@ -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.
|
||||
|
@ -13,26 +13,25 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
### END LICENSE
|
||||
|
||||
from gi.repository import Gtk, Gdk, GLib, Gio
|
||||
|
||||
from gettext import gettext as _
|
||||
from gi.repository import Gio
|
||||
|
||||
class Settings(Gio.Settings):
|
||||
|
||||
"""
|
||||
UberWriter Settings
|
||||
Apostrophe Settings
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Init Settings
|
||||
"""
|
||||
Gio.Settings.__init__(self)
|
||||
|
||||
def new():
|
||||
@classmethod
|
||||
def new(cls):
|
||||
"""
|
||||
Return a new Settings object
|
||||
"""
|
||||
settings = Gio.Settings.new("de.wolfvollprecht.UberWriter")
|
||||
settings.__class__ = Settings
|
||||
return settings
|
||||
return settings
|
|
@ -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.
|
||||
|
@ -16,12 +16,14 @@
|
|||
|
||||
import os
|
||||
import subprocess
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
# from plugins import plugins
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('uberwriter')
|
||||
logger = logging.getLogger('apostrophe')
|
||||
|
||||
class Shelve():
|
||||
"""
|
||||
|
@ -51,12 +53,12 @@ class Shelve():
|
|||
store.append(node[root], [filename, root + "/" + filename])
|
||||
|
||||
|
||||
class UberwriterSidebar():
|
||||
class Sidebar():
|
||||
"""
|
||||
Presentational class for shelves and files managed by the "sidebar"
|
||||
|
||||
parentwindow:
|
||||
Reference to UberwriterWindow
|
||||
Reference to Window
|
||||
"""
|
||||
def __init__(self, parentwindow):
|
||||
"""
|
||||
|
@ -151,10 +153,10 @@ class UberwriterSidebar():
|
|||
filename = self.store.get_value(treeiter, 1)
|
||||
item = Gtk.MenuItem.new()
|
||||
item.set_label("Open ...")
|
||||
item.connect("activate", self.context_menu_open_file)
|
||||
item.filename = filename
|
||||
# item.connect("activate", self.context_menu_open_file)
|
||||
# item.filename = filename
|
||||
item.show()
|
||||
self.popup.append(item)
|
||||
# self.popup.append(item)
|
||||
self.popup.show()
|
||||
self.popup.popup(None, None, None, None, event.button, event.time)
|
||||
return True
|
|
@ -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
|
|
@ -0,0 +1,162 @@
|
|||
from gettext import gettext as _
|
||||
|
||||
from apostrophe.helpers import user_action
|
||||
|
||||
|
||||
class FormatInserter:
|
||||
"""Manages insertion of formatting.
|
||||
|
||||
Methods can be called directly, as well as be used as signal callbacks."""
|
||||
|
||||
def insert_italic(self, text_view, _data=None):
|
||||
"""Insert italic or mark a selection as bold"""
|
||||
|
||||
self.__wrap(text_view, "_", _("italic text"))
|
||||
|
||||
def insert_bold(self, text_view, _data=None):
|
||||
"""Insert bold or mark a selection as bold"""
|
||||
|
||||
self.__wrap(text_view, "**", _("bold text"))
|
||||
|
||||
def insert_strikethrough(self, text_view, _data=None):
|
||||
"""Insert strikethrough or mark a selection as strikethrough"""
|
||||
|
||||
self.__wrap(text_view, "~~", _("strikethrough text"))
|
||||
|
||||
def insert_horizontal_rule(self, text_view, _data=None):
|
||||
"""Insert horizontal rule"""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
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):
|
||||
"""Insert list item or mark a selection as list item"""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
if text_buffer.get_has_selection():
|
||||
(start, end) = text_buffer.get_selection_bounds()
|
||||
if start.starts_line():
|
||||
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)
|
||||
|
||||
cursor_mark = text_buffer.get_insert()
|
||||
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
|
||||
|
||||
start_ext = cursor_iter.copy()
|
||||
start_ext.backward_lines(3)
|
||||
text = text_buffer.get_text(cursor_iter, start_ext, False)
|
||||
lines = text.splitlines()
|
||||
|
||||
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(line[:2] + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n" + line[:2] + helptext)
|
||||
break
|
||||
else:
|
||||
if not lines[-1] and not lines[-2]:
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
elif not lines[-1]:
|
||||
if cursor_iter.starts_line():
|
||||
text_buffer.insert_at_cursor("- " + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n- " + helptext)
|
||||
else:
|
||||
text_buffer.insert_at_cursor("\n\n- " + helptext)
|
||||
break
|
||||
|
||||
self.__select_text(text_view, 0, text_length)
|
||||
|
||||
def insert_ordered_list_item(self, _text_view, _data=None):
|
||||
# TODO: implement ordered lists
|
||||
pass
|
||||
|
||||
def insert_header(self, text_view, _data=None):
|
||||
"""Insert header or mark a selection as a list header"""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
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)
|
||||
|
||||
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()
|
||||
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:
|
||||
text = text[len(wrap):-len(wrap)]
|
||||
new_text = text.lstrip().rstrip()
|
||||
text = text.replace(new_text, wrap + new_text + wrap)
|
||||
|
||||
text_buffer.delete(start, end)
|
||||
move_back = len(wrap)
|
||||
|
||||
text_buffer.insert_at_cursor(text)
|
||||
text_length = len(new_text)
|
||||
|
||||
else:
|
||||
text_buffer.insert_at_cursor(wrap + helptext + wrap)
|
||||
text_length = len(helptext)
|
||||
move_back = len(wrap)
|
||||
|
||||
cursor_mark = text_buffer.get_insert()
|
||||
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
|
||||
cursor_iter.backward_chars(move_back)
|
||||
text_buffer.move_mark_by_name('selection_bound', cursor_iter)
|
||||
cursor_iter.backward_chars(text_length)
|
||||
text_buffer.move_mark_by_name('insert', cursor_iter)
|
||||
|
||||
@staticmethod
|
||||
def __select_text(text_view, offset, length):
|
||||
"""Selects text starting at the current cursor minus offset, length characters."""
|
||||
|
||||
text_buffer = text_view.get_buffer()
|
||||
cursor_mark = text_buffer.get_insert()
|
||||
cursor_iter = text_buffer.get_iter_at_mark(cursor_mark)
|
||||
cursor_iter.backward_chars(offset)
|
||||
text_buffer.move_mark_by_name('selection_bound', cursor_iter)
|
||||
cursor_iter.backward_chars(length)
|
||||
text_buffer.move_mark_by_name('insert', cursor_iter)
|
|
@ -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)
|
|
@ -0,0 +1,66 @@
|
|||
from gi.repository import Gtk
|
||||
|
||||
from apostrophe.settings import Settings
|
||||
from apostrophe.helpers import get_css_path
|
||||
|
||||
|
||||
class Theme:
|
||||
"""
|
||||
The Theme enum lists all supported themes using their "gtk-theme-name" value.
|
||||
|
||||
The light variant is listed first, followed by the dark variant, if any.
|
||||
"""
|
||||
|
||||
previous = None
|
||||
settings = Settings.new()
|
||||
|
||||
def __init__(self, name, web_css_path, is_dark, inverse_name):
|
||||
self.name = name
|
||||
self.web_css_path = web_css_path
|
||||
self.is_dark = is_dark
|
||||
self.inverse_name = inverse_name
|
||||
|
||||
@classmethod
|
||||
def get_for_name(cls, name, default=None):
|
||||
current_theme = default or defaultThemes[0]
|
||||
for theme in defaultThemes:
|
||||
if name == theme.name:
|
||||
current_theme = theme
|
||||
return current_theme
|
||||
|
||||
@classmethod
|
||||
def get_current_changed(cls):
|
||||
theme_name = Gtk.Settings.get_default().get_property('gtk-theme-name')
|
||||
dark_mode = cls.settings.get_boolean('dark-mode')
|
||||
current_theme = cls.get_for_name(theme_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
|
||||
return current_theme, changed
|
||||
|
||||
@classmethod
|
||||
def get_current(cls):
|
||||
current_theme, _ = cls.get_current_changed()
|
||||
return current_theme
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and \
|
||||
self.name == other.name and \
|
||||
self.web_css_path == other.web_css_path and \
|
||||
self.is_dark == other.is_dark and \
|
||||
self.inverse_name == other.inverse_name
|
||||
|
||||
|
||||
defaultThemes = [
|
||||
# https://gitlab.gnome.org/GNOME/gtk/tree/master/gtk/theme/Adwaita
|
||||
Theme('Adwaita', get_css_path('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('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('web/highcontrast.css'), False, 'HighContrastInverse'),
|
||||
Theme('HighContrastInverse', get_css_path('web/highcontrast_inverse.css'), True, 'HighContrast')
|
||||
]
|
|
@ -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>
|
|
@ -0,0 +1,147 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<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>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/apostrophe/master/screenshots/main.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<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/apostrophe/master/screenshots/formula.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image type="source">https://raw.githubusercontent.com/UberWriter/apostrophe/master/screenshots/preview.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<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 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>
|
||||
<li>Spellchecking status is now saved between sessions</li>
|
||||
<li>Minor UI fixes</li>
|
||||
<li>Added -d flag to enable webdev tools</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-12-06" version="2.1.4">
|
||||
<description>
|
||||
<p>Updated css styles.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-11-28" version="2.1.3">
|
||||
<description>
|
||||
<p>This release features a new logo, polishes the Appmenu, fixes usability bugs and flatpak related bugs.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-07-27" version="2.1.2">
|
||||
<description>
|
||||
<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">
|
||||
<description>
|
||||
<p>This release solves two minor bugs:</p>
|
||||
<ul>
|
||||
<li>One on focus mode which caused the lines to be highlighted on edit rather than on click</li>
|
||||
<li>Non symbolic icons on the searchbar</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-07-18" version="2.1.0">
|
||||
<description>
|
||||
<p>This release features a ton of UX/UI improvements, like:</p>
|
||||
<ul>
|
||||
<li>Drop AppMenu support</li>
|
||||
<li>HeaderBar and menus redesign, with a new unified menu and quick access buttons on the headerbar</li>
|
||||
<li>Now the fullscreen view shows a headerbar when the cursor approaches the top of the screen</li>
|
||||
<li>A new unified export dialog, with updated options, and quick access to pdf, odt and html export</li>
|
||||
<li>Bugfixes.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-06-24" version="2.0.4">
|
||||
<description>
|
||||
<p>Now the menu is a Popover instead a regular menu.</p>
|
||||
<p>The headerbar matches the theme selected for the application.</p>
|
||||
<p>Updated translations.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-06-14" version="2.0.3">
|
||||
<description>
|
||||
<p>Small bug fixes, updated links.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-05-16" version="2.0.2">
|
||||
<description>
|
||||
<p>Fix a bug with the preview mode.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-05-14" version="2.0.1">
|
||||
<description>
|
||||
<p>Don't use env variable to check if in flatpak.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-05-12" version="2.0.0">
|
||||
<description>
|
||||
<p>First re-release</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<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/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">@gettext-package@</translation>
|
||||
</component>
|
|
@ -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,15 +2,55 @@
|
|||
|
||||
<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>
|
||||
|
||||
<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>Dark mode</summary>
|
||||
<summary>Use dark mode</summary>
|
||||
<description>
|
||||
If enabled, the window will be dark themed
|
||||
If disabled, the window will be light themed
|
||||
asked to install them manually.
|
||||
Enable or disable the dark mode.
|
||||
</description>
|
||||
</key>
|
||||
<key name='spellcheck' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Check spelling while typing</summary>
|
||||
<description>
|
||||
Enable or disable spellchecking.
|
||||
</description>
|
||||
</key>
|
||||
<key name='sync-scroll' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Synchronize editor/preview scrolling</summary>
|
||||
<description>
|
||||
Keep the editor and preview scroll positions in sync.
|
||||
</description>
|
||||
</key>
|
||||
<key name='input-format' type='s'>
|
||||
<default>"markdown"</default>
|
||||
<summary>Input format</summary>
|
||||
<description>
|
||||
Input format to use when previewing and exporting using Pandoc.
|
||||
</description>
|
||||
</key>
|
||||
<key name='autohide-headerbar' type='b'>
|
||||
<default>true</default>
|
||||
<summary>Autohide Headerbar</summary>
|
||||
<description>
|
||||
Hide the header and status bars when typing.
|
||||
</description>
|
||||
</key>
|
||||
<key name='open-file-path' type='s'>
|
||||
|
@ -20,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>
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
version="1.1"
|
||||
id="svg7384"
|
||||
height="16">
|
||||
<metadata
|
||||
id="metadata90">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title>Gnome Symbolic Icon Theme</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<title
|
||||
id="title9167">Gnome Symbolic Icon Theme</title>
|
||||
<defs
|
||||
id="defs7386" />
|
||||
<g
|
||||
transform="translate(-201.0004,-867)"
|
||||
id="layer11">
|
||||
<path
|
||||
id="rect1890"
|
||||
transform="translate(201.0004,867)"
|
||||
d="M 2 2 C 0.892 2 0 2.892 0 4 L 0 12 C 0 13.108 0.892 14 2 14 L 14 14 C 15.108 14 16 13.108 16 12 L 16 4 C 16 2.892 15.108 2 14 2 L 2 2 z M 2.5 4 L 3.5 4 C 3.777 4 4 4.223 4 4.5 L 4 5.5 C 4 5.777 3.777 6 3.5 6 L 2.5 6 C 2.223 6 2 5.777 2 5.5 L 2 4.5 C 2 4.223 2.223 4 2.5 4 z M 5.5 4 L 6.5 4 C 6.777 4 7 4.223 7 4.5 L 7 5.5 C 7 5.777 6.777 6 6.5 6 L 5.5 6 C 5.223 6 5 5.777 5 5.5 L 5 4.5 C 5 4.223 5.223 4 5.5 4 z M 8.5 4 L 9.5 4 C 9.777 4 10 4.223 10 4.5 L 10 5.5 C 10 5.777 9.777 6 9.5 6 L 8.5 6 C 8.223 6 8 5.777 8 5.5 L 8 4.5 C 8 4.223 8.223 4 8.5 4 z M 11.5 4 L 12.5 4 C 12.777 4 13 4.223 13 4.5 L 13 5.5 C 13 5.777 12.777 6 12.5 6 L 11.5 6 C 11.223 6 11 5.777 11 5.5 L 11 4.5 C 11 4.223 11.223 4 11.5 4 z M 4.5 7 L 5.5 7 C 5.777 7 6 7.223 6 7.5 L 6 8.5 C 6 8.777 5.777 9 5.5 9 L 4.5 9 C 4.223 9 4 8.777 4 8.5 L 4 7.5 C 4 7.223 4.223 7 4.5 7 z M 7.5 7 L 8.5 7 C 8.777 7 9 7.223 9 7.5 L 9 8.5 C 9 8.777 8.777 9 8.5 9 L 7.5 9 C 7.223 9 7 8.777 7 8.5 L 7 7.5 C 7 7.223 7.223 7 7.5 7 z M 10.5 7 L 13.5 7 C 13.777 7 14 7.223 14 7.5 L 14 8.5 C 14 8.777 13.777 9 13.5 9 L 10.5 9 C 10.223 9 10 8.777 10 8.5 L 10 7.5 C 10 7.223 10.223 7 10.5 7 z M 2.5 10 L 3.5 10 C 3.777 10 4 10.223 4 10.5 L 4 11.5 C 4 11.777 3.777 12 3.5 12 L 2.5 12 C 2.223 12 2 11.777 2 11.5 L 2 10.5 C 2 10.223 2.223 10 2.5 10 z M 5.5 10 L 10.5 10 C 10.777 10 11 10.223 11 10.5 L 11 11.5 C 11 11.777 10.777 12 10.5 12 L 5.5 12 C 5.223 12 5 11.777 5 11.5 L 5 10.5 C 5 10.223 5.223 10 5.5 10 z M 12.5 10 L 13.5 10 C 13.777 10 14 10.223 14 10.5 L 14 11.5 C 14 11.777 13.777 12 13.5 12 L 12.5 12 C 12.223 12 12 11.777 12 11.5 L 12 10.5 C 12 10.223 12.223 10 12.5 10 z "
|
||||
style="opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;stroke:none;stroke-width:0.96824586;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
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 |
|
@ -0,0 +1,379 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 128 128"
|
||||
style="display:inline;enable-background:new"
|
||||
version="1.0"
|
||||
id="svg11300"
|
||||
height="128"
|
||||
width="128">
|
||||
<title
|
||||
id="title4162">Adwaita Icon Template</title>
|
||||
<defs
|
||||
id="defs3">
|
||||
<linearGradient
|
||||
id="linearGradient1234">
|
||||
<stop
|
||||
style="stop-color:#aeadab;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1230" />
|
||||
<stop
|
||||
style="stop-color:#ceccc8;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop1232" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient1228">
|
||||
<stop
|
||||
style="stop-color:#e66100;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1224" />
|
||||
<stop
|
||||
style="stop-color:#ff7800;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop1226" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient1222">
|
||||
<stop
|
||||
id="stop1210"
|
||||
offset="0"
|
||||
style="stop-color:#9a9996;stop-opacity:1" />
|
||||
<stop
|
||||
style="stop-color:#c0bfbc;stop-opacity:1"
|
||||
offset="0.03846159"
|
||||
id="stop1212" />
|
||||
<stop
|
||||
id="stop1214"
|
||||
offset="0.07692312"
|
||||
style="stop-color:#77767b;stop-opacity:1" />
|
||||
<stop
|
||||
style="stop-color:#77767b;stop-opacity:1"
|
||||
offset="0.92307693"
|
||||
id="stop1216" />
|
||||
<stop
|
||||
id="stop1218"
|
||||
offset="0.96153843"
|
||||
style="stop-color:#c0bfbc;stop-opacity:1" />
|
||||
<stop
|
||||
id="stop1220"
|
||||
offset="1"
|
||||
style="stop-color:#9a9996;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient1339">
|
||||
<stop
|
||||
id="stop1335"
|
||||
offset="0"
|
||||
style="stop-color:#5e5c64;stop-opacity:1;" />
|
||||
<stop
|
||||
id="stop1337"
|
||||
offset="1"
|
||||
style="stop-color:#77767b;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
y2="254"
|
||||
x2="116"
|
||||
y1="254"
|
||||
x1="11.999994"
|
||||
gradientTransform="matrix(1.0769231,0,0,1.0666667,-4.92308,-14.933327)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1820"
|
||||
xlink:href="#linearGradient1222" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,20,213)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1822"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,36,273)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1824"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="17"
|
||||
gradientTransform="rotate(90,10,247)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1826"
|
||||
xlink:href="#linearGradient1234" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,30,223)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1828"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,40,233)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1830"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,50,243)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1832"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,60,253)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1834"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,14,229)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1836"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,24,239)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1838"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,34,249)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1840"
|
||||
xlink:href="#linearGradient1339" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,49,264)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1842"
|
||||
xlink:href="#linearGradient1228" />
|
||||
<linearGradient
|
||||
y2="209"
|
||||
x2="31"
|
||||
y1="209"
|
||||
x1="19"
|
||||
gradientTransform="rotate(90,0,237)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="linearGradient1844"
|
||||
xlink:href="#linearGradient1339" />
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata4">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>GNOME Design Team</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:source />
|
||||
<cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
|
||||
<dc:title>Adwaita Icon Template</dc:title>
|
||||
<dc:subject>
|
||||
<rdf:Bag />
|
||||
</dc:subject>
|
||||
<dc:date />
|
||||
<dc:rights>
|
||||
<cc:Agent>
|
||||
<dc:title />
|
||||
</cc:Agent>
|
||||
</dc:rights>
|
||||
<dc:publisher>
|
||||
<cc:Agent>
|
||||
<dc:title />
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:identifier />
|
||||
<dc:relation />
|
||||
<dc:language />
|
||||
<dc:coverage />
|
||||
<dc:description />
|
||||
<dc:contributor>
|
||||
<cc:Agent>
|
||||
<dc:title />
|
||||
</cc:Agent>
|
||||
</dc:contributor>
|
||||
</cc:Work>
|
||||
<cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Notice" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#Attribution" />
|
||||
<cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
|
||||
<cc:requires
|
||||
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
transform="translate(0,-172)"
|
||||
style="display:inline"
|
||||
id="layer1">
|
||||
<g
|
||||
style="display:inline"
|
||||
id="layer9">
|
||||
<rect
|
||||
y="224.00003"
|
||||
x="8"
|
||||
height="64"
|
||||
width="112.00001"
|
||||
id="rect1768"
|
||||
style="display:inline;opacity:1;vector-effect:none;fill:url(#linearGradient1820);fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
|
||||
rx="8"
|
||||
ry="8" />
|
||||
<rect
|
||||
ry="8.8421087"
|
||||
rx="8"
|
||||
style="opacity:1;vector-effect:none;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:4.2052598;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect1770"
|
||||
width="112"
|
||||
height="84.000015"
|
||||
x="8"
|
||||
y="200" />
|
||||
<path
|
||||
id="path1772"
|
||||
d="m 21,212 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1822);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 31,216 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1774" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 41,212 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1776" />
|
||||
<path
|
||||
id="path1778"
|
||||
d="m 61,212 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 81,212 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1780" />
|
||||
<path
|
||||
id="path1782"
|
||||
d="m 101,212 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
id="path1784"
|
||||
d="m 31,234 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 51,234 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1786" />
|
||||
<path
|
||||
id="path1788"
|
||||
d="m 71,234 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#c64600;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 91,234 h 7 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -9 c -2.216,0 -4.220501,-1.79499 -4,-4 l 1,-10 c 0.220501,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1790" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 25,256 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1792" />
|
||||
<path
|
||||
id="path1794"
|
||||
d="m 97,256 h 6 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 h -8 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
style="opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1824);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 107,260 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1796" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#828186;fill-opacity:1;stroke:none;stroke-width:3.99999952;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 45,256 h 38 c 2.216,0 3.7795,1.79499 4,4 l 1,10 c 0.2205,2.20501 -1.784,4 -4,4 H 44 c -2.216,0 -4.2205,-1.79499 -4,-4 l 1,-10 c 0.2205,-2.20501 1.784,-4 4,-4 z"
|
||||
id="path1798" />
|
||||
<path
|
||||
id="path1800"
|
||||
d="m 87,260 v 4 c 0,2.216 -1.784,4 -4,4 H 45 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 38 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1826);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
id="path1802"
|
||||
d="m 51,216 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1828);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
id="path1804"
|
||||
d="m 71,216 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1830);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1832);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 91,216 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1806" />
|
||||
<path
|
||||
id="path1808"
|
||||
d="m 111,216 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1834);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1836);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 41,238 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1810" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1838);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 61,238 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1812" />
|
||||
<path
|
||||
id="path1814"
|
||||
d="m 81,238 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1840);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1842);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 102,238 v 4 c 0,2.216 -1.784,4 -4,4 h -7 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 7 c 2.216,0 4,1.784 4,4 z"
|
||||
id="path1816" />
|
||||
<path
|
||||
id="path1818"
|
||||
d="m 35,260 v 4 c 0,2.216 -1.784,4 -4,4 h -6 c -2.216,0 -4,-1.784 -4,-4 v -4 c 0,-2.216 1.784,-4 4,-4 h 6 c 2.216,0 4,1.784 4,4 z"
|
||||
style="opacity:1;vector-effect:none;fill:url(#linearGradient1844);fill-opacity:1;stroke:none;stroke-width:3.99999928;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
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
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ end
|
|||
|
||||
--- Replace the todo marker in the given block, if any.
|
||||
function M.replace_todo_markers (blk, format)
|
||||
if blk.t ~= 'Para' and blk.t ~= 'Plain' then
|
||||
if not blk or blk.t ~= 'Para' and blk.t ~= 'Plain' then
|
||||
return blk
|
||||
end
|
||||
local inlines = blk.content
|
||||
|
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 132 KiB |