├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── requirements.txt └── src └── popup_dictionary ├── __init__.py ├── _addon.py ├── _version.py ├── browser.py ├── config.json ├── config.md ├── config.py ├── consts.py ├── libaddon ├── LICENSE ├── __init__.py ├── _vendor │ ├── PYTHON.LICENSE │ ├── __init__.py │ ├── logging │ │ ├── __init__.py │ │ ├── config.py │ │ └── handlers.py │ └── packaging │ │ ├── LICENSE │ │ ├── LICENSE.APACHE │ │ ├── LICENSE.BSD │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── _compat.py │ │ ├── _structures.py │ │ ├── _typing.py │ │ ├── markers.py │ │ ├── py.typed │ │ ├── requirements.py │ │ ├── specifiers.py │ │ ├── tags.py │ │ ├── utils.py │ │ └── version.py ├── _vendor_legacy │ ├── PYTHON.LICENSE │ ├── types.py │ └── typing.py ├── _version.py ├── anki │ ├── __init__.py │ ├── configeditor.py │ ├── configmanager.py │ ├── editor.py │ └── utils.py ├── config │ ├── abstract │ │ ├── anki.py │ │ ├── base.py │ │ └── interface.py │ ├── errors.py │ ├── manager.py │ ├── manager_old.py │ ├── signals.py │ └── storages │ │ ├── __init__.py │ │ ├── anki.py │ │ ├── json.py │ │ └── local.py ├── consts.py ├── debug.py ├── gui │ ├── __init__.py │ ├── about.py │ ├── basic │ │ ├── __init__.py │ │ ├── dialog_basic.py │ │ ├── dialog_mapped.py │ │ ├── interface.py │ │ └── widgets │ │ │ ├── __init__.py │ │ │ ├── qcolorbutton.py │ │ │ ├── qkeygrabber.py │ │ │ ├── qt.py │ │ │ └── qutils.py │ ├── dialog_configeditor.py │ ├── dialog_contrib.py │ ├── dialog_htmlview.py │ ├── dialog_options.py │ └── labelformatter.py ├── packaging.py ├── platform.py ├── util │ ├── __init__.py │ ├── filesystem.py │ ├── nesting.py │ ├── packaging.py │ ├── types.py │ └── version.py └── utils.py ├── migrate.py ├── results.py ├── reviewer.py ├── template.py ├── web.py └── web ├── LICENSE_JQUERY_MIGRATE.txt ├── LICENSE_QTIP2.txt ├── jquery-migrate-3.0.0.min.js ├── jquery.highlight.min.js ├── jquery.qtip.css ├── jquery.qtip.min.js ├── popup.css └── popup.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Pop-up Dictionary will be documented here. You can click on each release number to be directed to a detailed log of all code commits for that particular release. The download links will direct you to the GitHub release page, allowing you to manually install a release if you want. 4 | 5 | If you enjoy Pop-up Dictionary, please consider supporting my work on Patreon, or by buying me a cup of coffee :coffee:: 6 | 7 |

8 |      9 |

10 | 11 | :heart: My heartfelt thanks goes out to everyone who has supported this add-on through their tips, contributions, or any other means (you know who you are!). All of this would not have been possible without you. Thank you for being awesome! 12 | 13 | ## [Unreleased] 14 | 15 | ## [1.0.0-beta.1] - 2021-08-04 16 | 17 | ### [Download](https://github.com/glutanimate/popup-dictionary/releases/tag/v1.0.0-beta.1) 18 | 19 | ### Added 20 | 21 | - Added suport for modern Anki versions (tested up to Anki 2.1.46) 22 | - The add-on now sports a sleek new pop-up design and effects, optimized both for light mode and dark mode 23 | - Searches can now also be triggered by selecting a phrase and right-clicking 24 | - Added an option to exclude new notes from snippet results (thanks to @zjosua!) 25 | 26 | ### Fixed 27 | 28 | - Changed the default add-on hotkey to "Alt+Shift+D" (⌥+⇧+D on macOS) to avoid a conflict with default key bindings on Anki 2.1.41 and up 29 | - Fixed an issue that would cause an error when opening cards in the browser (thanks to @Nanco300 for the report!) 30 | - Fixed an issue that would cause images within the pop-ups to not be sized correctly (thanks to @padenw24 for the report!) 31 | - Made the add-on more resilient against conflicts with user-made deck styling 32 | - Made the add-on more resilient against conflicts with other add-ons (tested against the top 200 most popular add-ons) 33 | 34 | ### Changed 35 | 36 | - Refactored major parts of the add-on, making it more robust and future-proof 37 | - Dropped support for older Anki versions. The minimum supported version is now 2.1.23. 38 | - Upgraded qTip2 to v3.0.3. This should hopefully fix a number of bugs with the pop-ups and make for an overall smoother user experience (thanks to @ansaso!) 39 | 40 | ## [1.0.0-dev.1] - 2019-08-27 41 | 42 | ### [Download](https://github.com/glutanimate/popup-dictionary/releases/tag/v1.0.0-dev.1) 43 | 44 | ### Changed 45 | 46 | - Dropped 2.0.x support 47 | - Refactored major parts of the add-on 48 | 49 | ## [0.5.0-beta.1] - 2019-02-28 50 | 51 | ### Changed 52 | 53 | - Added footer to tooltip 54 | - Updated packaging scheme to simplify installation process 55 | 56 | ## [0.4.2] - 2018-08-19 57 | 58 | ### Fixed 59 | 60 | - Quick fix for Anki 2.1 support. 61 | 62 | ## [0.4.1] - 2018-03-18 63 | 64 | ### Changed 65 | 66 | - Changed: Renamed add-on to Pop-up Dictionary 67 | - Changed: New default values for excluded fields 68 | 69 | ### Fixed 70 | 71 | - Fixed: Anki 2.1 support 72 | 73 | ## [0.4.0] - 2018-03-06 74 | 75 | ### Added 76 | 77 | - **New**: Anki 2.1 support (please wait for the next 2.1 beta) 78 | - **New**: merged original dictionary lookup functionality with new note snippets 79 | - **New**: highlight search terms 80 | - **New**: show note in browser 81 | - **New**: hotkey to invoke tooltip manually on custom selection (Ctrl+Shift+D) 82 | - **New**: warn when looking up term with too many hits 83 | - **New**: invoke tooltip on double-click rather than select 84 | - **New**: config file with full support of Anki 2.1's config editor 85 | 86 | ### Fixed 87 | 88 | - **Fixed**: a lot of smaller issues and bugs 89 | 90 | ## [0.3.0] - 2018-03-05 91 | 92 | ### Added 93 | 94 | - nested tooltips 95 | - refined theme 96 | 97 | ## [0.2.0] - 2018-03-03 98 | 99 | ### Added 100 | 101 | - Switch to displaying snippets of notes in the current deck, instead of using the dictionary deck 102 | 103 | ## v0.1.0 - 2018-02-19 104 | 105 | ### Added 106 | 107 | Initial release 108 | 109 | [Unreleased]: https://github.com/glutanimate/popup-dictionary/compare/v1.0.0-dev.1...HEAD 110 | [1.0.0-dev.1]: https://github.com/glutanimate/popup-dictionary/compare/v0.5.0-beta.1...v1.0.0-dev.1 111 | [0.5.0-beta.1]: https://github.com/glutanimate/popup-dictionary/compare/v0.4.2...v0.5.0-beta.1 112 | [0.4.2]: https://github.com/glutanimate/popup-dictionary/compare/v0.4.1...v0.4.2 113 | [0.4.1]: https://github.com/glutanimate/popup-dictionary/compare/v0.4.0...v0.4.1 114 | [0.4.0]: https://github.com/glutanimate/popup-dictionary/compare/v0.3.0...v0.4.0 115 | [0.3.0]: https://github.com/glutanimate/popup-dictionary/compare/v0.2.0...v0.3.0 116 | [0.2.0]: https://github.com/glutanimate/popup-dictionary/compare/v0.1.0...v0.2.0 117 | 118 | ----- 119 | 120 | The format of this file is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to Contribute to this Project 2 | 3 | Please see the [common contribution guidelines](https://github.com/glutanimate/docs/blob/master/anki/add-ons/CONTRIBUTING.md#how-to-contribute-to-my-anki-add-ons) for my Anki add-ons. 4 | 5 | Thanks! -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Pop-up Dictionary for Anki

4 | 5 |

6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |

14 | 15 | > Connecting the flashcard-dots 16 | 17 | This is an add-on for the spaced-repetition flashcard app [Anki](https://apps.ankiweb.net/). It provides the ability to quickly draw up related facts on words or phrases, just by double-clicking on them. 18 | 19 | ### Table of Contents 20 | 21 | 22 | 23 | - [Installation](#installation) 24 | - [Documentation](#documentation) 25 | - [Building](#building) 26 | - [Contributing](#contributing) 27 | - [License and Credits](#license-and-credits) 28 | 29 | 30 | 31 | ### Installation 32 | 33 | #### AnkiWeb 34 | 35 | The easiest way to install Pop-up Dictionary is through [AnkiWeb](https://ankiweb.net/shared/info/153625306). 36 | 37 | #### Manual installation 38 | 39 | 40 |
41 | 42 | Click here to see the instructions 43 | 44 | 1. Make sure you have the [latest version](https://apps.ankiweb.net/#download) of Anki 2.1 installed. 45 | 2. Download the latest `.ankiaddon` package from the [releases tab](https://github.com/glutanimate/popup-dictionary/releases) (you might need to click on *Assets* below the description to reveal the download links) 46 | 3. From Anki's main window, head to *Tools* → *Add-ons* 47 | 4. Drag-and-drop the `.ankiaddon` package onto the add-ons list 48 | 5. Restart Anki 49 | 50 |
51 | 52 | ### Documentation 53 | 54 | For further information on the use of this add-on please check out [the description text](docs/description.md) for AnkiWeb. 55 | 56 | ### Building 57 | 58 | With [Anki add-on builder](https://github.com/glutanimate/anki-addon-builder/) installed: 59 | 60 | git clone https://github.com/glutanimate/popup-dictionary.git 61 | cd popup-dictionary 62 | aab build 63 | 64 | For more information on the build process please refer to [`aab`'s documentation](https://github.com/glutanimate/anki-addon-builder/#usage). 65 | 66 | ### Contributing 67 | 68 | Contributions are welcome! Please review the [contribution guidelines](./CONTRIBUTING.md) on how to: 69 | 70 | - Report issues 71 | - File pull requests 72 | - Support the project as a non-developer 73 | 74 | ### License and Credits 75 | 76 | *Pop-up Dictionary* is *Copyright © 2018-2021 [Aristotelis P.](https://glutanimate.com/) (Glutanimate)* 77 | 78 | My work on the initial version of this add-on was partially funded by two fellow Anki users. I would like to thank both of them for their help. 79 | 80 | Ships with the following javascript libraries: 81 | 82 | - jQuery (v1.12.4), (c) jQuery Foundation, licensed under the MIT license 83 | - qTip2 (v3.0.3), (c) 2011-2018 Craig Michael Thompson, licensed under the MIT license 84 | - jQuery.highlight, (c) 2007-2014 Johann Burkard, licensed under the MIT license 85 | - jQuery Migrate, (c) OpenJS Foundation and other contributors, https://openjsf.org/, licensed under the MIT license 86 | 87 | Pop-up Dictionary is free and open-source software. The add-on code that runs within Anki is released under the GNU AGPLv3 license, extended by a number of additional terms. For more information please see the [LICENSE](https://github.com/glutanimate/popup-dictionary/blob/master/LICENSE) file that accompanied this program. 88 | 89 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. 90 | 91 | ---- 92 | 93 | 94 |
The continued development of this add-on is made possible
thanks to my Patreon and Ko-Fi supporters. 95 |
You guys rock ❤️ !
96 |
-------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Please note that these are only build dependencies. They are not 2 | # required to run compiled builds of the add-on within Anki 3 | aab 4 | -------------------------------------------------------------------------------- /src/popup_dictionary/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | import os 33 | 34 | if not os.environ.get("ADDON_TEST_ENV"): 35 | from ._addon import * # noqa: F401, F403 36 | -------------------------------------------------------------------------------- /src/popup_dictionary/_addon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | from ._version import __version__ # noqa: F401 33 | 34 | from .consts import ADDON 35 | from .libaddon.consts import setAddonProperties 36 | 37 | setAddonProperties(ADDON) 38 | 39 | from .migrate import migrate_addon 40 | from .reviewer import initialize_reviewer 41 | from .template import initialize_template 42 | from .web import initialize_web 43 | 44 | migrate_addon() 45 | initialize_template() 46 | initialize_web() 47 | initialize_reviewer() 48 | -------------------------------------------------------------------------------- /src/popup_dictionary/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Version information 34 | """ 35 | 36 | __version__ = "1.0.0-beta.1" 37 | -------------------------------------------------------------------------------- /src/popup_dictionary/browser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | # 32 | 33 | from typing import TYPE_CHECKING, Union 34 | 35 | import aqt 36 | from aqt import mw 37 | from aqt.browser import Browser 38 | 39 | if TYPE_CHECKING: 40 | from anki.notes import NoteId 41 | 42 | try: # 2.1.41+ 43 | from anki.collection import SearchNode 44 | 45 | NEW_SEARCH_SUPPORT = True 46 | except (ImportError, ModuleNotFoundError): 47 | NEW_SEARCH_SUPPORT = False 48 | 49 | 50 | def browse_to_nid(note_id: Union["NoteId", int]): 51 | """Open browser and find cards by nid""" 52 | 53 | if NEW_SEARCH_SUPPORT: 54 | aqt.dialogs.open("Browser", mw, search=(SearchNode(nid=note_id),)) 55 | else: 56 | browser: Browser = aqt.dialogs.open("Browser", mw) 57 | browser.form.searchEdit.lineEdit().setText(f"nid:{note_id}") 58 | browser.onSearchActivated() 59 | -------------------------------------------------------------------------------- /src/popup_dictionary/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "generalConfirmEmpty": true, 3 | "generalHotkey": "Alt+Shift+D", 4 | "dictionaryEnabled": true, 5 | "dictionaryNoteTypeName": "Dictionary Entry", 6 | "dictionaryTermFieldName": "Term", 7 | "dictionaryDefinitionFieldName": "Definition", 8 | "snippetsEnabled": true, 9 | "snippetsExcludedFields": ["Note ID", "ID (hidden)"], 10 | "snippetsExcludeNewNotes": false, 11 | "snippetsLimitToCurrentDeck": true, 12 | "snippetsResultsWarnLimit": 1000 13 | } 14 | -------------------------------------------------------------------------------- /src/popup_dictionary/config.md: -------------------------------------------------------------------------------- 1 | ### Pop-up Dictionary Configuration 2 | 3 | *If Pop-up Dictionary has been a valuable asset in your studies, please consider supporting my efforts by [buying me a coffee](https://ko-fi.com/X8X0L4YV), or by [pledging your support on Patreon](https://www.patreon.com/glutanimate). Each and every contribution is greatly appreciated and will help me maintain and improve Pop-up Dictionary as time goes by!* 4 | 5 | Please note that the following settings do not sync and require a restart to apply: 6 | 7 | - `dictionaryEnabled` (true/false): Whether or not to enable results drawn from the dictionary note type. Default: `true`. 8 | - `dictionaryDefinitionFieldName` (string): Name of the dictionary definition field in the dictionary note type. Default: `"Definition"`. 9 | - `dictionaryNoteTypeName` (string): Name of the dictionary note type. Default: `"Dictionary Entry"`. 10 | - `dictionaryTermFieldName` (string): Name of the dictionary term field in the dictionary note type. Default: `"Term"`. 11 | - `generalConfirmEmpty` (true/false): Whether or not to show tooltip when no results have been found. Default: `true`. 12 | - `generalHotkey` (string): Hotkey to invoke tooltip manually. Default: `"Ctrl+Shift+D"`. 13 | - `snippetsEnabled` (true/false): Whether or not to enable results drawn from any type of note in your collection. Default: `true`. 14 | - `snippetsExcludedFields` (list): List of fields to exclude from being shown in the note snippet section of the tooltip. Default: `["Note ID", "ID (hidden)"]`. 15 | - `snippetsExcludeNewNotes` (true/false): Whether or not to exclude snippet results from new notes. Default: `false`. 16 | - `snippetsLimitToCurrentDeck` (true/false): Whether or not to limit note snippet results to current deck. Default: `true`. 17 | - `snippetsResultsWarnLimit` (integer): Number of results above which to show a warning on the potential slowdowns they could cause. Set to `0` to disable warning. Default: `1000`. 18 | -------------------------------------------------------------------------------- /src/popup_dictionary/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Handles add-on configuration 34 | """ 35 | 36 | from aqt import mw 37 | from .libaddon.anki.configmanager import ConfigManager 38 | 39 | config = ConfigManager(mw) 40 | -------------------------------------------------------------------------------- /src/popup_dictionary/consts.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | 4 | # Pop-up Dictionary Add-on for Anki 5 | # 6 | # Copyright (C) 2018-2021 Aristotelis P. 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License as 10 | # published by the Free Software Foundation, either version 3 of the 11 | # License, or (at your option) any later version, with the additions 12 | # listed at the end of the license file that accompanied this program. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with this program. If not, see . 21 | # 22 | # NOTE: This program is subject to certain additional terms pursuant to 23 | # Section 7 of the GNU Affero General Public License. You should have 24 | # received a copy of these additional terms immediately following the 25 | # terms and conditions of the GNU Affero General Public License that 26 | # accompanied this program. 27 | # 28 | # If not, please request a copy through one of the means of contact 29 | # listed here: . 30 | # 31 | # Any modifications to this file must keep this entire header intact. 32 | 33 | """ 34 | Addon-wide constants 35 | """ 36 | 37 | from ._version import __version__ 38 | 39 | try: 40 | from .data.patrons import MEMBERS_CREDITED, MEMBERS_TOP 41 | except ImportError: 42 | MEMBERS_CREDITED = MEMBERS_TOP = () 43 | 44 | __all__ = [ 45 | "ADDON" 46 | ] 47 | 48 | # PROPERTIES DESCRIBING ADDON 49 | 50 | 51 | class ADDON(object): 52 | """Class storing general add-on properties 53 | Property names need to be all-uppercase with no leading underscores 54 | """ 55 | NAME = "Pop-up Dictionary" 56 | MODULE = "popup_dictionary" 57 | ID = "153625306" 58 | VERSION = __version__ 59 | LICENSE = "GNU AGPLv3" 60 | AUTHORS = ( 61 | {"name": "Aristotelis P. (Glutanimate)", "years": "2018-2019", 62 | "contact": "https://glutanimate.com"}, 63 | ) 64 | AUTHOR_MAIL = "ankiglutanimate@gmail.com" 65 | LIBRARIES = ( 66 | {"name": "qTip2", "version": "v3.0.3", 67 | "author": "Craig Michael Thompson", "license": "MIT license", 68 | "url": "http://qtip2.com/"}, 69 | {"name": "jquery-migrate", "version": "3.0.0", 70 | "author": "jquery", "license": "MIT license", 71 | "url": "https://github.com/jquery/jquery-migrate"}, 72 | {"name": "jQuery.highlight", "version": "5", 73 | "author": "Johann Burkard", "license": "MIT license", 74 | "url": "https://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html"}, 75 | ) 76 | CONTRIBUTORS = () 77 | SPONSORS = () 78 | MEMBERS_CREDITED = MEMBERS_CREDITED 79 | MEMBERS_TOP = MEMBERS_TOP 80 | LINKS = { 81 | "patreon": "https://www.patreon.com/glutanimate", 82 | "bepatron": "https://www.patreon.com/bePatron?u=7522179", 83 | "coffee": "http://ko-fi.com/glutanimate", 84 | "description": "https://ankiweb.net/shared/info/{}".format(ID), 85 | "rate": "https://ankiweb.net/shared/review/{}".format(ID), 86 | "twitter": "https://twitter.com/glutanimate", 87 | "youtube": "https://www.youtube.com/c/glutanimate", 88 | "help": "" 89 | } 90 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Libaddon: A helper library for Anki add-on development 34 | 35 | Provides access to a number of commonly used modules shared across 36 | many of my add-ons. 37 | 38 | Please note that this package is not fit for general use yet, as it is 39 | still is too specific to my own add-ons and implementations. 40 | 41 | This module is the package entry-point. 42 | """ 43 | 44 | from ._version import __version__ # noqa: F401 45 | 46 | 47 | def maybeVendorTyping(): 48 | try: 49 | import typing # noqa: F401 50 | import types # noqa: F401 51 | except ImportError: 52 | registerLegacyVendorDir() 53 | 54 | 55 | def registerLegacyVendorDir(): 56 | """Some modules like "typing" cannot be properly vendorized, so fall back 57 | to hacky sys.path modifications if necessary 58 | NOTE: make sure not to use vendored legacy dependencies before running this 59 | """ 60 | import sys 61 | import os 62 | 63 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_vendor_legacy")) 64 | 65 | 66 | def checkFor2114ImportError(name: str) -> bool: 67 | try: 68 | # litmus test for Anki import bug 69 | from .platform import anki_version # noqa: F401 70 | 71 | return True 72 | except ImportError: 73 | # Disable add-on and inform user of the bug 74 | from aqt.utils import showWarning 75 | from aqt import mw 76 | from anki import version as anki_version 77 | 78 | if mw is None: 79 | return False 80 | 81 | mw.addonManager.toggleEnabled(__name__, enable=False) 82 | 83 | bug = "https://anki.tenderapp.com/discussions/ankidesktop/34836" 84 | downloads = "https://apps.ankiweb.net#download" 85 | vers = "2.1.15" 86 | title = "Warning: {name} disabled".format(name=name) 87 | msg = ( 88 | "WARNING: {name} had to be disabled because the " 89 | "version of Anki that is currently installed on your system " 90 | "({anki_version}) is incompatible with the add-on.

" 91 | "Earlier releases of Anki like this one " 92 | "suffer from a bug that breaks " 93 | "{name} and many other add-ons on your system. " 94 | "In order to fix this you will have to update Anki " 95 | "to version {vers} or higher.

" 96 | "After updating Anki, please re-enable " 97 | "{name} by heading to Tools → Add-ons, selecting the " 98 | "add-on, and clicking Toggle Enabled.".format( 99 | name=name, 100 | anki_version=anki_version, 101 | bug=bug, 102 | vers=vers, 103 | downloads=downloads, 104 | ) 105 | ) 106 | 107 | showWarning(msg, title=title, textFormat="rich") 108 | 109 | return False 110 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Vendorized third-party packages 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the licenses 2 | found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made 3 | under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) Donald Stufft and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/__about__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | __all__ = [ 7 | "__title__", 8 | "__summary__", 9 | "__uri__", 10 | "__version__", 11 | "__author__", 12 | "__email__", 13 | "__license__", 14 | "__copyright__", 15 | ] 16 | 17 | __title__ = "packaging" 18 | __summary__ = "Core utilities for Python packages" 19 | __uri__ = "https://github.com/pypa/packaging" 20 | 21 | __version__ = "20.0" 22 | 23 | __author__ = "Donald Stufft and individual contributors" 24 | __email__ = "donald@stufft.io" 25 | 26 | __license__ = "BSD or Apache License, Version 2.0" 27 | __copyright__ = "Copyright 2014-2019 %s" % __author__ 28 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | from .__about__ import ( 7 | __author__, 8 | __copyright__, 9 | __email__, 10 | __license__, 11 | __summary__, 12 | __title__, 13 | __uri__, 14 | __version__, 15 | ) 16 | 17 | __all__ = [ 18 | "__title__", 19 | "__summary__", 20 | "__uri__", 21 | "__version__", 22 | "__author__", 23 | "__email__", 24 | "__license__", 25 | "__copyright__", 26 | ] 27 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/_compat.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | # NOTE: This module has been modified to be packaged with Anki add-ons 5 | # The changes are Copyright (c) 2020 Aristotelis P. 6 | # and licensed under the same license as the original module 7 | 8 | from __future__ import absolute_import, division, print_function 9 | 10 | import sys 11 | 12 | from ._typing import MYPY_CHECK_RUNNING 13 | 14 | if MYPY_CHECK_RUNNING: # pragma: no cover 15 | from ..typing import Any, Dict, Tuple, Type 16 | 17 | 18 | PY2 = sys.version_info[0] == 2 19 | PY3 = sys.version_info[0] == 3 20 | 21 | # flake8: noqa 22 | 23 | if PY3: 24 | string_types = (str,) 25 | else: 26 | string_types = (basestring,) 27 | 28 | 29 | def with_metaclass(meta, *bases): 30 | # type: (Type[Any], Tuple[Type[Any], ...]) -> Any 31 | """ 32 | Create a base class with a metaclass. 33 | """ 34 | # This requires a bit of explanation: the basic idea is to make a dummy 35 | # metaclass for one level of class instantiation that replaces itself with 36 | # the actual metaclass. 37 | class metaclass(meta): # type: ignore 38 | def __new__(cls, name, this_bases, d): 39 | # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any 40 | return meta(name, bases, d) 41 | 42 | return type.__new__(metaclass, "temporary_class", (), {}) -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/_structures.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | from __future__ import absolute_import, division, print_function 5 | 6 | 7 | class InfinityType(object): 8 | def __repr__(self): 9 | # type: () -> str 10 | return "Infinity" 11 | 12 | def __hash__(self): 13 | # type: () -> int 14 | return hash(repr(self)) 15 | 16 | def __lt__(self, other): 17 | # type: (object) -> bool 18 | return False 19 | 20 | def __le__(self, other): 21 | # type: (object) -> bool 22 | return False 23 | 24 | def __eq__(self, other): 25 | # type: (object) -> bool 26 | return isinstance(other, self.__class__) 27 | 28 | def __ne__(self, other): 29 | # type: (object) -> bool 30 | return not isinstance(other, self.__class__) 31 | 32 | def __gt__(self, other): 33 | # type: (object) -> bool 34 | return True 35 | 36 | def __ge__(self, other): 37 | # type: (object) -> bool 38 | return True 39 | 40 | def __neg__(self): 41 | # type: (object) -> NegativeInfinityType 42 | return NegativeInfinity 43 | 44 | 45 | Infinity = InfinityType() 46 | 47 | 48 | class NegativeInfinityType(object): 49 | def __repr__(self): 50 | # type: () -> str 51 | return "-Infinity" 52 | 53 | def __hash__(self): 54 | # type: () -> int 55 | return hash(repr(self)) 56 | 57 | def __lt__(self, other): 58 | # type: (object) -> bool 59 | return True 60 | 61 | def __le__(self, other): 62 | # type: (object) -> bool 63 | return True 64 | 65 | def __eq__(self, other): 66 | # type: (object) -> bool 67 | return isinstance(other, self.__class__) 68 | 69 | def __ne__(self, other): 70 | # type: (object) -> bool 71 | return not isinstance(other, self.__class__) 72 | 73 | def __gt__(self, other): 74 | # type: (object) -> bool 75 | return False 76 | 77 | def __ge__(self, other): 78 | # type: (object) -> bool 79 | return False 80 | 81 | def __neg__(self): 82 | # type: (object) -> InfinityType 83 | return Infinity 84 | 85 | 86 | NegativeInfinity = NegativeInfinityType() 87 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/_typing.py: -------------------------------------------------------------------------------- 1 | # NOTE: This module has been modified to be packaged with Anki add-ons 2 | # The changes are Copyright (c) 2020 Aristotelis P. 3 | # and licensed under the same license as the original module 4 | 5 | """For neatly implementing static typing in packaging. 6 | 7 | `mypy` - the static type analysis tool we use - uses the `typing` module, which 8 | provides core functionality fundamental to mypy's functioning. 9 | 10 | Generally, `typing` would be imported at runtime and used in that fashion - 11 | it acts as a no-op at runtime and does not have any run-time overhead by 12 | design. 13 | 14 | As it turns out, `typing` is not vendorable - it uses separate sources for 15 | Python 2/Python 3. Thus, this codebase can not expect it to be present. 16 | To work around this, mypy allows the typing import to be behind a False-y 17 | optional to prevent it from running at runtime and type-comments can be used 18 | to remove the need for the types to be accessible directly during runtime. 19 | 20 | This module provides the False-y guard in a nicely named fashion so that a 21 | curious maintainer can reach here to read this. 22 | 23 | In packaging, all static-typing related imports should be guarded as follows: 24 | 25 | from packaging._typing import MYPY_CHECK_RUNNING 26 | 27 | if MYPY_CHECK_RUNNING: 28 | from typing import ... 29 | 30 | Ref: https://github.com/python/mypy/issues/3216 31 | """ 32 | 33 | MYPY_CHECK_RUNNING = False 34 | 35 | if MYPY_CHECK_RUNNING: # pragma: no cover 36 | from .. import typing 37 | 38 | cast = typing.cast 39 | else: 40 | # typing's cast() is needed at runtime, but we don't want to import typing. 41 | # Thus, we use a dummy no-op version, which we tell mypy to ignore. 42 | def cast(type_, value): # type: ignore 43 | return value -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glutanimate/popup-dictionary/43127f1b7490880392c43dc096d0edf694e00d5f/src/popup_dictionary/libaddon/_vendor/packaging/py.typed -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/requirements.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | # NOTE: This module has been modified to be packaged with Anki add-ons 5 | # The changes are Copyright (c) 2020 Aristotelis P. 6 | # and licensed under the same license as the original module 7 | 8 | from __future__ import absolute_import, division, print_function 9 | 10 | import string 11 | import re 12 | 13 | from pyparsing import stringStart, stringEnd, originalTextFor, ParseException 14 | from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine 15 | from pyparsing import Literal as L # noqa 16 | from six.moves.urllib import parse as urlparse 17 | 18 | from ._typing import MYPY_CHECK_RUNNING 19 | from .markers import MARKER_EXPR, Marker 20 | from .specifiers import LegacySpecifier, Specifier, SpecifierSet 21 | 22 | if MYPY_CHECK_RUNNING: # pragma: no cover 23 | from ..typing import List 24 | 25 | 26 | class InvalidRequirement(ValueError): 27 | """ 28 | An invalid requirement was found, users should refer to PEP 508. 29 | """ 30 | 31 | 32 | ALPHANUM = Word(string.ascii_letters + string.digits) 33 | 34 | LBRACKET = L("[").suppress() 35 | RBRACKET = L("]").suppress() 36 | LPAREN = L("(").suppress() 37 | RPAREN = L(")").suppress() 38 | COMMA = L(",").suppress() 39 | SEMICOLON = L(";").suppress() 40 | AT = L("@").suppress() 41 | 42 | PUNCTUATION = Word("-_.") 43 | IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM) 44 | IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) 45 | 46 | NAME = IDENTIFIER("name") 47 | EXTRA = IDENTIFIER 48 | 49 | URI = Regex(r"[^ ]+")("url") 50 | URL = AT + URI 51 | 52 | EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) 53 | EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") 54 | 55 | VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) 56 | VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) 57 | 58 | VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY 59 | VERSION_MANY = Combine( 60 | VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False 61 | )("_raw_spec") 62 | _VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) 63 | _VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "") 64 | 65 | VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") 66 | VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) 67 | 68 | MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") 69 | MARKER_EXPR.setParseAction( 70 | lambda s, l, t: Marker(s[t._original_start : t._original_end]) 71 | ) 72 | MARKER_SEPARATOR = SEMICOLON 73 | MARKER = MARKER_SEPARATOR + MARKER_EXPR 74 | 75 | VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) 76 | URL_AND_MARKER = URL + Optional(MARKER) 77 | 78 | NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) 79 | 80 | REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd 81 | # pyparsing isn't thread safe during initialization, so we do it eagerly, see 82 | # issue #104 83 | REQUIREMENT.parseString("x[]") 84 | 85 | 86 | class Requirement(object): 87 | """Parse a requirement. 88 | 89 | Parse a given requirement string into its parts, such as name, specifier, 90 | URL, and extras. Raises InvalidRequirement on a badly-formed requirement 91 | string. 92 | """ 93 | 94 | # TODO: Can we test whether something is contained within a requirement? 95 | # If so how do we do that? Do we need to test against the _name_ of 96 | # the thing as well as the version? What about the markers? 97 | # TODO: Can we normalize the name and extra name? 98 | 99 | def __init__(self, requirement_string): 100 | # type: (str) -> None 101 | try: 102 | req = REQUIREMENT.parseString(requirement_string) 103 | except ParseException as e: 104 | raise InvalidRequirement( 105 | 'Parse error at "{0!r}": {1}'.format( 106 | requirement_string[e.loc : e.loc + 8], e.msg 107 | ) 108 | ) 109 | 110 | self.name = req.name 111 | if req.url: 112 | parsed_url = urlparse.urlparse(req.url) 113 | if parsed_url.scheme == "file": 114 | if urlparse.urlunparse(parsed_url) != req.url: 115 | raise InvalidRequirement("Invalid URL given") 116 | elif not (parsed_url.scheme and parsed_url.netloc) or ( 117 | not parsed_url.scheme and not parsed_url.netloc 118 | ): 119 | raise InvalidRequirement("Invalid URL: {0}".format(req.url)) 120 | self.url = req.url 121 | else: 122 | self.url = None 123 | self.extras = set(req.extras.asList() if req.extras else []) 124 | self.specifier = SpecifierSet(req.specifier) 125 | self.marker = req.marker if req.marker else None 126 | 127 | def __str__(self): 128 | # type: () -> str 129 | parts = [self.name] # type: List[str] 130 | 131 | if self.extras: 132 | parts.append("[{0}]".format(",".join(sorted(self.extras)))) 133 | 134 | if self.specifier: 135 | parts.append(str(self.specifier)) 136 | 137 | if self.url: 138 | parts.append("@ {0}".format(self.url)) 139 | if self.marker: 140 | parts.append(" ") 141 | 142 | if self.marker: 143 | parts.append("; {0}".format(self.marker)) 144 | 145 | return "".join(parts) 146 | 147 | def __repr__(self): 148 | # type: () -> str 149 | return "".format(str(self)) -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_vendor/packaging/utils.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | # NOTE: This module has been modified to be packaged with Anki add-ons 5 | # The changes are Copyright (c) 2020 Aristotelis P. 6 | # and licensed under the same license as the original module 7 | 8 | from __future__ import absolute_import, division, print_function 9 | 10 | import re 11 | 12 | from ._typing import MYPY_CHECK_RUNNING 13 | from .version import InvalidVersion, Version 14 | 15 | if MYPY_CHECK_RUNNING: # pragma: no cover 16 | from ..typing import Union 17 | 18 | _canonicalize_regex = re.compile(r"[-_.]+") 19 | 20 | 21 | def canonicalize_name(name): 22 | # type: (str) -> str 23 | # This is taken from PEP 503. 24 | return _canonicalize_regex.sub("-", name).lower() 25 | 26 | 27 | def canonicalize_version(_version): 28 | # type: (str) -> Union[Version, str] 29 | """ 30 | This is very similar to Version.__str__, but has one subtle difference 31 | with the way it handles the release segment. 32 | """ 33 | 34 | try: 35 | version = Version(_version) 36 | except InvalidVersion: 37 | # Legacy versions cannot be normalized 38 | return _version 39 | 40 | parts = [] 41 | 42 | # Epoch 43 | if version.epoch != 0: 44 | parts.append("{0}!".format(version.epoch)) 45 | 46 | # Release segment 47 | # NB: This strips trailing '.0's to normalize 48 | parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) 49 | 50 | # Pre-release 51 | if version.pre is not None: 52 | parts.append("".join(str(x) for x in version.pre)) 53 | 54 | # Post-release 55 | if version.post is not None: 56 | parts.append(".post{0}".format(version.post)) 57 | 58 | # Development release 59 | if version.dev is not None: 60 | parts.append(".dev{0}".format(version.dev)) 61 | 62 | # Local version segment 63 | if version.local is not None: 64 | parts.append("+{0}".format(version.local)) 65 | 66 | return "".join(parts) -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Version information 34 | """ 35 | 36 | __version__ = "0.1.0-dev.0" 37 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/anki/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Package that bundles together commonly used modules that mediate interaction 34 | between add-ons and Anki 35 | """ 36 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/anki/configeditor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # Copyright (C) 2016-2019 Ankitects Pty Ltd and contributors 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License as 10 | # published by the Free Software Foundation, either version 3 of the 11 | # License, or (at your option) any later version, with the additions 12 | # listed at the end of the license file that accompanied this program. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with this program. If not, see . 21 | # 22 | # NOTE: This program is subject to certain additional terms pursuant to 23 | # Section 7 of the GNU Affero General Public License. You should have 24 | # received a copy of these additional terms immediately following the 25 | # terms and conditions of the GNU Affero General Public License that 26 | # accompanied this program. 27 | # 28 | # If not, please request a copy through one of the means of contact 29 | # listed here: . 30 | # 31 | # Any modifications to this file must keep this entire header intact. 32 | 33 | """ 34 | anki20 compat: Add-on configuration editor 35 | """ 36 | 37 | import aqt 38 | from aqt.qt import * 39 | from aqt.utils import tooltip 40 | 41 | from anki.utils import json 42 | 43 | from ..consts import ADDON 44 | from .._vendor import markdown2 45 | from ..platform import PATH_THIS_ADDON 46 | 47 | from .dialog_htmlview import HTMLViewer 48 | 49 | class ConfigEditor(QDialog): 50 | 51 | def __init__(self, config_manager, parent): 52 | super(ConfigEditor, self).__init__(parent=parent) 53 | self.mgr = config_manager 54 | self.form = aqt.forms.editaddon.Ui_Dialog() 55 | self.form.setupUi(self) 56 | self.setWindowTitle("{} Configuration".format(ADDON.NAME)) 57 | self.setupWidgets() 58 | self.updateText(self.mgr["local"]) 59 | self.exec_() 60 | 61 | def setupWidgets(self): 62 | button_box = self.form.buttonBox 63 | restore_btn = button_box.addButton(QDialogButtonBox.RestoreDefaults) 64 | help_btn = button_box.addButton(QDialogButtonBox.Help) 65 | help_btn.clicked.connect(self.onHelpRequested) 66 | restore_btn.clicked.connect(self.onRestoreDefaults) 67 | 68 | def updateText(self, conf): 69 | self.form.text.setPlainText( 70 | json.dumps(conf, ensure_ascii=False, sort_keys=True, 71 | indent=4, separators=(',', ': '))) 72 | 73 | def onRestoreDefaults(self): 74 | default_conf = self.mgr.defaults["local"] 75 | self.updateText(default_conf) 76 | tooltip("Restored defaults", parent=self) 77 | 78 | def onHelpRequested(self): 79 | docs_path = os.path.join(PATH_THIS_ADDON, "config.md") 80 | if not os.path.exists(docs_path): 81 | return False 82 | with open(docs_path, "r") as f: 83 | html = markdown2.markdown(f.read()) 84 | dialog = HTMLViewer(html, title="{} Configuration Help".format( 85 | ADDON.NAME), parent=self) 86 | dialog.show() 87 | 88 | def accept(self): 89 | txt = self.form.text.toPlainText() 90 | try: 91 | new_conf = json.loads(txt) 92 | except ValueError as e: 93 | showInfo("Invalid configuration, restoring previous config: " + 94 | repr(e)) 95 | return 96 | if not isinstance(new_conf, dict): 97 | showInfo("Invalid configuration, restoring previous config: " 98 | "top level object must be a map") 99 | return 100 | 101 | self.mgr["local"] = new_conf 102 | self.mgr.save(storage_name="local") 103 | super(ConfigEditor, self).accept() 104 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/anki/editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Helpers for interacting with Anki's editor instances 34 | """ 35 | 36 | # Handling async JS execution when saving editor content 37 | 38 | def editorSaveThen(callback): 39 | def onSaved(editor, *args, **kwargs): 40 | # uses evalWithCallback internally: 41 | editor.saveNow(lambda: callback(editor, *args, **kwargs)) 42 | return onSaved 43 | 44 | 45 | def widgetEditorSaveThen(callback): 46 | def onSaved(widget, *args, **kwargs): 47 | """[summary] 48 | 49 | Arguments: 50 | callback {[type]} -- [description] 51 | widget {Qt widget or widget} -- Qt object the editor is a member of 52 | (e.g. Browser, AddCards, EditCurrent) 53 | """ 54 | # uses evalWithCallback internally: 55 | widget.editor.saveNow(lambda: callback(widget, *args, **kwargs)) 56 | return onSaved 57 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/anki/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Utility functions for interacting with Anki 34 | """ 35 | 36 | import os 37 | 38 | from aqt import mw 39 | 40 | from ..platform import ANKI20, PATH_ADDONS 41 | from ..consts import ADDON 42 | 43 | 44 | def debugInfo(): 45 | """Return verbose info on add-ons and Anki installation""" 46 | info = ["{name} version {version}".format(name=ADDON.NAME, 47 | version=ADDON.VERSION)] 48 | if ANKI20: 49 | from aqt.qt import QT_VERSION_STR, PYQT_VERSION_STR 50 | from aqt import appVersion 51 | from anki.utils import platDesc 52 | info.append("Anki {version} (Qt {qt} PyQt {pyqt})".format( 53 | version=appVersion, qt=QT_VERSION_STR, pyqt=PYQT_VERSION_STR)) 54 | info.append(platDesc()) 55 | files = [f for f in os.listdir(PATH_ADDONS) 56 | if f.endswith(".py")] 57 | info.append("Add-ons:\n\n" + repr(files)) 58 | else: 59 | from aqt.utils import supportText 60 | info.append(supportText()) 61 | 62 | addmgr = mw.addonManager 63 | info.append("Add-ons:\n\n" + "\n".join( 64 | addmgr.annotatedName(d) for d in addmgr.allAddons())) 65 | 66 | return "\n\n".join(info) 67 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/abstract/anki.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | 36 | import copy 37 | from abc import ABC, abstractmethod 38 | 39 | from anki.hooks import addHook, remHook 40 | from aqt.main import AnkiQt 41 | 42 | from ..._vendor.packaging import version 43 | from ...anki.additions.hooks import HOOKS 44 | from ...util.nesting import deepMergeDicts 45 | from ..errors import ( 46 | ConfigError, 47 | ConfigNotLoadedError, 48 | ConfigNotReadyError, 49 | FutureConfigError, 50 | ) 51 | from .base import ConfigStorage 52 | 53 | from typing import Optional 54 | 55 | __all__ = [ 56 | "AnkiConfigStorage", 57 | ] 58 | 59 | 60 | # TODO: SUBCLASS DOCSTRINGS 61 | 62 | 63 | class AnkiConfigStorage(ConfigStorage, ABC): 64 | 65 | name = "" 66 | root_namespace = None 67 | 68 | def __init__( 69 | self, 70 | mw: AnkiQt, 71 | namespace: str, 72 | defaults: dict, 73 | atomic: bool = False, 74 | ): 75 | try: 76 | _ = defaults["version"] 77 | except KeyError: 78 | raise ConfigError("Defaults need to include a 'version' key/value pair") 79 | 80 | super().__init__( 81 | mw, namespace, defaults=defaults, atomic=atomic 82 | ) 83 | 84 | self._deferred: bool = False 85 | 86 | def initialize(self) -> bool: 87 | if self._loaded: 88 | return True 89 | self._ready = True 90 | try: 91 | self.load() 92 | except ConfigNotReadyError: 93 | self._deferInitialization() 94 | return False 95 | return super().initialize() 96 | 97 | def load(self) -> bool: 98 | """[summary] 99 | 100 | Returns: 101 | bool -- Whether existing config was found 102 | """ 103 | if not self._ready: 104 | raise ConfigNotReadyError("Attempted to load before initializing config") 105 | config_object = self._configObject 106 | user_data = config_object.get(self._namespace, None) 107 | if user_data: 108 | user_data = self._getUpdatedConfig(user_data, self.defaults) 109 | self.data = user_data or copy.deepcopy(self._defaults) 110 | super().load() 111 | return bool(user_data) 112 | 113 | def save(self) -> None: 114 | if not self._loaded: 115 | raise ConfigNotLoadedError("Attempted to save before loading config") 116 | # Ensure that we pass values instead of a reference to our data: 117 | self._configObject[self._namespace] = copy.deepcopy(self.data) 118 | self._flush() 119 | return super().save() 120 | 121 | def delete(self) -> None: 122 | self.data = {} 123 | self.save() 124 | return super().delete() 125 | 126 | def purge(self) -> None: 127 | """Completely remove modifications from base storage object""" 128 | try: 129 | del self._configObject[self._namespace] 130 | except KeyError: 131 | raise ConfigError("Attempted to purge non-existing config") 132 | self._flush() 133 | 134 | def unload(self) -> None: 135 | if self._deferred: 136 | remHook("profileLoaded", self.initialize) 137 | self._deferred = False 138 | super().unload() 139 | 140 | def _deferInitialization(self): 141 | if self._deferred: 142 | raise ConfigError("Initialization already deferred") 143 | self._deferred = True 144 | addHook(HOOKS.PROFILE_LOADED, self.initialize) 145 | 146 | @property 147 | def _configObject(self) -> dict: 148 | try: 149 | config_object = self._ankiConfigObject() 150 | except AttributeError: 151 | config_object = None 152 | if config_object is None: 153 | raise ConfigNotReadyError("Anki base storage object is not ready") 154 | 155 | if self.root_namespace: 156 | try: 157 | config_object = config_object[self.root_namespace] 158 | except KeyError: 159 | config_object[self.root_namespace] = {} 160 | 161 | return config_object 162 | 163 | @staticmethod 164 | def _getUpdatedConfig(data: dict, defaults: dict) -> Optional[dict]: 165 | try: 166 | defaults_version = defaults["version"] 167 | except KeyError: 168 | raise ConfigError("Defaults need to include a 'version' key/value pair") 169 | 170 | # legacy support: non-str version or no version 171 | data_version = str(data.get("version", "0.0.0")) 172 | 173 | parsed_version_data = version.parse(data_version) 174 | parsed_version_defaults = version.parse(defaults_version) 175 | 176 | # Upgrade config version if necessary 177 | if parsed_version_data < parsed_version_defaults: 178 | data = deepMergeDicts( 179 | defaults, data, new=True 180 | ) # returns deepcopied defaults, updated with data 181 | data["version"] = defaults_version 182 | elif parsed_version_data > parsed_version_defaults: 183 | # TODO: Figure out where to handle 184 | raise FutureConfigError("Config is newer than add-on release") 185 | else: 186 | # ensure that we never operate on base config object directly 187 | data = copy.deepcopy(data) 188 | 189 | return data 190 | 191 | @abstractmethod 192 | def _ankiConfigObject(self) -> dict: 193 | pass 194 | 195 | @abstractmethod 196 | def _flush(self) -> None: 197 | pass 198 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/abstract/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | 36 | from abc import ABC 37 | import copy 38 | 39 | from anki.hooks import addHook, remHook 40 | from aqt.main import AnkiQt 41 | 42 | from ..errors import ConfigNotLoadedError, ConfigError 43 | from ..signals import ConfigSignals 44 | from .interface import ConfigInterface 45 | 46 | from typing import Any, Optional, Hashable 47 | 48 | 49 | __all__ = ["ConfigStorage"] 50 | 51 | 52 | class ConfigStorage(ConfigInterface, ABC): 53 | 54 | name: str = "" 55 | 56 | def __init__( 57 | self, 58 | mw: AnkiQt, 59 | namespace: Optional[str] = None, 60 | defaults: Optional[dict] = None, 61 | atomic: bool = False, 62 | ): 63 | super().__init__() 64 | 65 | self._mw = mw 66 | self._namespace = namespace 67 | self._defaults = defaults or {} 68 | self._atomic = False 69 | 70 | self._ready: bool = False 71 | self._loaded: bool = False 72 | self._dirty: bool = False 73 | 74 | self.data = {} 75 | self.signals = ConfigSignals() 76 | 77 | # Overwrite some ConfigInterface implementations 78 | 79 | def __getitem__(self, key: Hashable) -> Any: 80 | if not self._loaded: 81 | raise ConfigNotLoadedError() 82 | return super().__getitem__(key) 83 | 84 | def __setitem__(self, key: Hashable, value: Any) -> None: 85 | if not self._loaded: 86 | raise ConfigNotLoadedError() 87 | super().__setitem__(key, value) 88 | if self._atomic: 89 | self.save() 90 | else: 91 | self._dirty = True 92 | 93 | # Fill out ConfigInterface abstract methods and properties 94 | 95 | @property 96 | def ready(self) -> bool: 97 | return self._ready 98 | 99 | @property 100 | def loaded(self) -> bool: 101 | return self._loaded 102 | 103 | @property 104 | def dirty(self) -> bool: 105 | return self._dirty 106 | 107 | # TODO: CRUCIAL – perform config validation 108 | # if invalid: 109 | # config.reset() 110 | # and perhaps notify user 111 | # CONSIDER: perform these only at load/save time or with every access? 112 | # (expensive!) 113 | 114 | def initialize(self) -> bool: 115 | """Performs one-shot setup steps. Should only be fired once. 116 | Separated out of __init__ in order to provide more granular control 117 | of initialization steps, and enable deferring some initialization 118 | steps if necessary 119 | """ 120 | addHook("unloadProfile", self.unload) 121 | self._ready = True 122 | self.signals.initialized.emit() 123 | return True 124 | 125 | def load(self) -> bool: 126 | # should set self.data from base storage 127 | self._loaded = True 128 | self.signals.loaded.emit() 129 | return True 130 | 131 | def save(self) -> None: 132 | # should set base storage from self.data 133 | self._dirty = False 134 | self.signals.saved.emit() 135 | 136 | @property 137 | def defaults(self) -> dict: 138 | return self._defaults 139 | 140 | @defaults.setter 141 | def defaults(self, data: dict) -> None: 142 | self._defaults = copy.deepcopy(data) 143 | 144 | def reset(self) -> None: 145 | self.data = self.defaults 146 | self.save() 147 | self.signals.reset.emit() 148 | 149 | def delete(self) -> None: 150 | # data representation and base storage object are emptied, but persist 151 | # (e.g. don't completely purge storage key out of storage object) 152 | self._dirty = False 153 | self.signals.deleted.emit() 154 | 155 | def unload(self): 156 | self.signals.unloaded.emit() 157 | # TODO: is this necessary? throws errors for now ↓ 158 | # self.signals.disconnect() 159 | if not self._loaded: 160 | return 161 | try: 162 | self.save() 163 | except (FileNotFoundError, ConfigError) as e: 164 | # Corner case: Closing Anki after add-on uninstall 165 | print(e) 166 | self._loaded = self._dirty = self._ready = False 167 | remHook("unloadProfile", self.unload) 168 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/abstract/interface.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, abstractproperty 2 | from collections.abc import MutableMapping 3 | 4 | from ..signals import ConfigSignals 5 | 6 | from typing import Any, Hashable 7 | 8 | 9 | class ConfigInterface(MutableMapping): 10 | 11 | signals: ConfigSignals 12 | data: dict 13 | 14 | # Fill out MutableMapping interface abstract methods 15 | 16 | def __getitem__(self, key: Hashable) -> Any: 17 | if key in self.data: 18 | return self.data[key] 19 | if hasattr(self.__class__, "__missing__"): 20 | return self.__class__.__missing__(self, key) # type: ignore 21 | raise KeyError(key) 22 | 23 | def __setitem__(self, key: Hashable, value: Any) -> None: 24 | self.data[key] = value 25 | 26 | def __delitem__(self, key: Hashable) -> None: 27 | del self.data[key] 28 | 29 | def __iter__(self): 30 | return iter(self.data) 31 | 32 | def __len__(self): 33 | return len(self.data) 34 | 35 | def __contains__(self, key: Hashable): 36 | return key in self.data 37 | 38 | def __repr__(self) -> str: 39 | return repr(self.data) 40 | 41 | # Define additional abstract methods and properties 42 | 43 | @abstractproperty 44 | def defaults(self) -> dict: 45 | return {} 46 | 47 | @abstractproperty 48 | def ready(self) -> bool: 49 | """Base storage object ready for I/O 50 | 51 | Returns: 52 | bool -- whether base storage object is ready 53 | """ 54 | return False 55 | 56 | @abstractproperty 57 | def loaded(self) -> bool: 58 | """Config loaded from base storage object 59 | 60 | Returns: 61 | bool -- whether config is loaded in 62 | """ 63 | return False 64 | 65 | @abstractproperty 66 | def dirty(self) -> bool: 67 | """Config representation diverges from base storage object 68 | 69 | Returns: 70 | bool -- whether config diverges from base storage object 71 | """ 72 | return False 73 | 74 | @abstractmethod 75 | def initialize(self) -> bool: 76 | """Performs one-shot setup steps. Should only be fired once. 77 | Separated out of __init__ in order to provide more granular control 78 | of initialization steps, and enable deferring some initialization 79 | steps if necessary 80 | """ 81 | # should emit signals.initialized 82 | return 83 | 84 | @abstractmethod 85 | def load(self) -> bool: 86 | # should emit signals.loaded 87 | return 88 | 89 | @abstractmethod 90 | def save(self): 91 | pass 92 | 93 | @abstractmethod 94 | def reset(self): 95 | pass 96 | 97 | @abstractmethod 98 | def delete(self): 99 | pass 100 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | 33 | class ConfigError(Exception): 34 | """ 35 | Thrown whenever a config-specific exception occurs 36 | """ 37 | 38 | pass 39 | 40 | class FutureConfigError(ConfigError): 41 | pass 42 | 43 | class ConfigNotReadyError(ConfigError): 44 | pass 45 | 46 | 47 | class ConfigNotLoadedError(ConfigError): 48 | pass 49 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | from .abstract.base import ConfigStorage 33 | from .abstract.interface import ConfigInterface 34 | from .errors import ConfigError 35 | from .signals import ConfigSignals 36 | 37 | from typing import List, Dict 38 | 39 | 40 | class ConfigManager(ConfigInterface): 41 | def __init__(self, storages: List[ConfigStorage]) -> None: 42 | self.data: Dict[str, ConfigStorage] = { 43 | storage.name: storage for storage in storages 44 | } 45 | self.signals = ConfigSignals() 46 | self._unloaded: set = set() 47 | 48 | # Overwrite some ConfigInterface implementations 49 | 50 | def __getitem__(self, key: str) -> ConfigStorage: 51 | return super().__getitem__(key) 52 | 53 | def __setitem__(self, key: str, value: ConfigStorage): 54 | try: 55 | assert isinstance(value, ConfigStorage) 56 | except AssertionError: 57 | raise ConfigError("Value to be set needs to be a valid ConfigStorage") 58 | return super().__setitem__(key, value) 59 | 60 | # Fill out ConfigInterface abstract methods and properties 61 | 62 | @property 63 | def ready(self) -> bool: 64 | return all(storage.ready for storage in self.data.values()) 65 | 66 | @property 67 | def loaded(self) -> bool: 68 | return all(storage.loaded for storage in self.data.values()) 69 | 70 | @property 71 | def dirty(self) -> bool: 72 | return any(storage.dirty for storage in self.data.values()) 73 | 74 | def initialize(self) -> bool: 75 | for storage in self.data.values(): 76 | storage.initialize() 77 | storage.signals.unloaded.connect(lambda: self._markUnloaded(storage.name)) 78 | self.signals.initialized.emit() 79 | return True 80 | 81 | def load(self) -> bool: 82 | for storage in self.data.values(): 83 | storage.load() 84 | self.signals.loaded.emit() 85 | return True 86 | 87 | def save(self) -> None: 88 | for storage in self.data.values(): 89 | storage.save() 90 | self.signals.saved.emit() 91 | 92 | @property 93 | def defaults(self) -> dict: 94 | return {storage.name: storage.defaults for storage in self.data.values()} 95 | 96 | @defaults.setter 97 | def defaults(self, data: Dict[str, dict]) -> None: 98 | for storage_name in data: 99 | try: 100 | storage = self.data[storage_name] 101 | except KeyError: 102 | raise ConfigError(f"Unsupported storage {storage_name}") 103 | storage.defaults = data[storage_name] 104 | 105 | def reset(self) -> None: 106 | for storage in self.data.values(): 107 | storage.reset() 108 | self.signals.reset.emit() 109 | 110 | def delete(self) -> None: 111 | for storage in self.data.values(): 112 | storage.delete() 113 | self.signals.deleted.emit() 114 | 115 | def unload(self): 116 | for storage in self.data.values(): 117 | storage.unload() 118 | self.signals.unloaded.emit() 119 | 120 | def _markUnloaded(self, storage_name: str): 121 | self._unloaded.add(storage_name) 122 | if all(k in self._unloaded for k in self.data.keys()): 123 | self.signals.unloaded.emit() 124 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | from PyQt5.QtCore import pyqtSignal, QObject 33 | 34 | 35 | class ConfigSignals(QObject): 36 | initialized = pyqtSignal() 37 | saved = pyqtSignal() 38 | loaded = pyqtSignal() 39 | reset = pyqtSignal() 40 | deleted = pyqtSignal() 41 | unloaded = pyqtSignal() 42 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/storages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/storages/anki.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | 36 | from ..abstract.anki import AnkiConfigStorage 37 | from ..errors import ConfigNotReadyError 38 | 39 | __all__ = [ 40 | "ProfileConfigStorage", 41 | "SyncedConfigStorage", 42 | "MetaConfigStorage", 43 | "LibaddonMetaConfigStorage", 44 | ] 45 | 46 | 47 | class ProfileConfigStorage(AnkiConfigStorage): 48 | # NOTE: Profile is available at add-on init time when Anki is launched 49 | # with a specific profile as a parameter. This is usually not the case, 50 | # but might arise when testing an add-on and lead you astray. 51 | 52 | name = "profile" 53 | 54 | def _ankiConfigObject(self) -> dict: 55 | return self._mw.pm.profile 56 | 57 | def _flush(self) -> None: 58 | # no flushing required 59 | pass 60 | 61 | 62 | class SyncedConfigStorage(AnkiConfigStorage): 63 | 64 | name = "synced" 65 | 66 | def _ankiConfigObject(self) -> dict: 67 | return self._mw.col.conf 68 | 69 | def _flush(self) -> None: 70 | try: 71 | self._mw.col.setMod() 72 | except AttributeError: 73 | raise ConfigNotReadyError("Anki base storage object is not ready") 74 | 75 | 76 | class MetaConfigStorage(AnkiConfigStorage): 77 | 78 | name = "meta" 79 | 80 | def _ankiConfigObject(self) -> dict: 81 | return self._mw.pm.meta 82 | 83 | def _flush(self) -> None: 84 | # no flushing required 85 | pass 86 | 87 | 88 | class LibaddonMetaConfigStorage(MetaConfigStorage): 89 | 90 | root_namespace = "libaddon" 91 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/storages/json.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | 36 | import json 37 | from pathlib import Path 38 | 39 | from aqt.main import AnkiQt 40 | 41 | from ...util.types import PathOrString 42 | from ..abstract.base import ConfigStorage 43 | from ..errors import ConfigError, ConfigNotReadyError, ConfigNotLoadedError 44 | 45 | from typing import Optional 46 | 47 | __all__ = ["JSONConfigStorage"] 48 | 49 | 50 | class JSONConfigStorage(ConfigStorage): 51 | """e.g. JSON file in user_data folder""" 52 | 53 | name = "json" 54 | 55 | def __init__( 56 | self, 57 | mw: AnkiQt, 58 | path: PathOrString, 59 | defaults: Optional[dict] = None, 60 | atomic: bool = False, 61 | ): 62 | self._path: Path = Path(path) 63 | super().__init__(mw, defaults=defaults, atomic=atomic) 64 | 65 | def initialize(self) -> bool: 66 | if self._loaded: 67 | return True 68 | self._ready = True 69 | self.load() 70 | return super().initialize() 71 | 72 | def load(self) -> bool: 73 | if not self._ready: 74 | raise ConfigNotReadyError("Attempted to load before initializing config") 75 | path = self._safePath(self._path) 76 | data = self._readData(path) 77 | self.data = data if data is not None else self.defaults 78 | super().load() 79 | return data is not None 80 | 81 | def save(self) -> None: 82 | if not self._loaded: 83 | raise ConfigNotLoadedError("Attempted to save before loading config") 84 | path = self._safePath(self._path) 85 | self._writeData(path, self.data) 86 | super().save() 87 | 88 | def delete(self): 89 | self.data = {} 90 | self.save() 91 | super().delete() 92 | 93 | def purge(self) -> None: 94 | """Completely remove modifications from base storage object""" 95 | self._removeFile() 96 | 97 | def _safePath(self, path: Path) -> Path: 98 | if not path.is_file(): 99 | path.parent.mkdir(parents=True, exist_ok=True) 100 | with path.open("w", encoding="utf-8") as f: 101 | json.dump(None, f) 102 | return path 103 | 104 | def _readData(self, path: Path) -> Optional[dict]: 105 | try: 106 | with path.open(encoding="utf-8") as f: 107 | return json.load(f) 108 | except (IOError, OSError, ValueError) as e: 109 | # log 110 | raise ConfigError( 111 | f"Could not read {self.name} storage at {path}:\n{str(e)}" 112 | ) 113 | 114 | def _writeData(self, path: Path, data: dict) -> None: 115 | try: 116 | with path.open("w", encoding="utf-8") as f: 117 | json.dump(data, f) 118 | except (IOError, OSError, ValueError) as e: 119 | # log 120 | raise ConfigError( 121 | f"Could not write to {self.name} storage at {path}:\n{str(e)}" 122 | ) 123 | 124 | def _removeFile(self) -> None: 125 | path = self._safePath(self._path) 126 | path.unlink() 127 | 128 | def unload(self) -> None: 129 | # FIXME: overwrites ConfigStorage.unload to prevent 130 | # unloading on profile switch. not necessary for JSONConfigStorage 131 | # since config shared across profiles. Instead we just perform a 132 | # (more safe) save on config unload 133 | if not self._loaded: 134 | return 135 | try: 136 | self.save() 137 | except (FileNotFoundError, ConfigError) as e: 138 | # Corner case: Closing Anki after add-on uninstall 139 | print(e) 140 | 141 | 142 | class UserFilesConfigStorage(JSONConfigStorage): 143 | def __init__( 144 | self, 145 | mw: AnkiQt, 146 | file_stem: str, 147 | defaults: Optional[dict] = None, 148 | atomic: bool = False, 149 | ): 150 | from ...platform import pathUserFiles 151 | 152 | path = Path(pathUserFiles()) / f"{file_stem}.json" 153 | 154 | super().__init__( 155 | mw, path, defaults=defaults, atomic=atomic 156 | ) 157 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/config/storages/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Add-on configuration storages 34 | """ 35 | 36 | from aqt.main import AnkiQt 37 | 38 | from ...addon import ADDON 39 | from ...anki import ANKI 40 | from ...util.version import checkVersion 41 | from ..abstract.base import ConfigStorage 42 | from ..errors import ConfigError, ConfigNotReadyError, ConfigNotLoadedError 43 | 44 | __all__ = ["LocalConfigStorage"] 45 | 46 | 47 | class LocalConfigStorage(ConfigStorage): 48 | 49 | name = "local" 50 | 51 | def __init__( 52 | self, 53 | mw: AnkiQt, 54 | atomic: bool = False, 55 | namespace=ADDON.MODULE, 56 | native_gui: bool = True, 57 | ): 58 | self._native_gui = native_gui 59 | 60 | # Anki handles defaults: 61 | defaults = mw.addonManager.addonConfigDefaults(namespace) 62 | if defaults is None: 63 | raise ConfigError("No default config file provided") 64 | 65 | super().__init__( 66 | mw, namespace, defaults=defaults, atomic=atomic 67 | ) 68 | 69 | def initialize(self) -> bool: 70 | if self._loaded: 71 | return True 72 | self._ready = True 73 | self.load() 74 | if self._native_gui: 75 | self._ensureSaveBeforeConfigGUILoaded() 76 | self._ensureLoadAfterConfigGUIFinished() 77 | return super().initialize() 78 | 79 | def delete(self) -> None: 80 | self.data = {} 81 | self.save() 82 | return super().delete() 83 | 84 | def load(self) -> bool: 85 | if not self._ready: 86 | raise ConfigNotReadyError("Attempted to load before initializing config") 87 | data = self._mw.addonManager.getConfig(self._namespace) 88 | if data is None: # should never happen 89 | raise ConfigError("No default config file provided") 90 | self.data = data 91 | return super().load() 92 | 93 | def save(self) -> None: 94 | if not self._loaded: 95 | raise ConfigNotLoadedError("Attempted to save before loading config") 96 | self._mw.addonManager.writeConfig(self._namespace, self.data) 97 | return super().save() 98 | 99 | @property 100 | def defaults(self) -> dict: 101 | return self._defaults 102 | 103 | @defaults.setter 104 | def defaults(self, data: dict) -> None: 105 | raise NotImplementedError( 106 | f"{self.name} storage does not support setting defaults" 107 | ) 108 | 109 | def _ensureLoadAfterConfigGUIFinished(self) -> None: 110 | self._mw.addonManager.setConfigUpdatedAction(self._namespace, self.load) 111 | 112 | def _ensureSaveBeforeConfigGUILoaded(self) -> None: 113 | """ugly workaround, drop as soon as possible""" 114 | 115 | if checkVersion(ANKI.VERSION, "2.1.17"): 116 | self._mw.addonManager.setConfigAction( 117 | self._namespace, self._saveBeforeConfigLoaded 118 | ) 119 | return 120 | 121 | from anki.hooks import wrap 122 | from aqt.addons import AddonsDialog 123 | 124 | def wrappedOnConfig(addonsDialog: AddonsDialog, *args, **kwargs): 125 | """Save before config editor is invoked""" 126 | addon = addonsDialog.onlyOneSelected() 127 | if not addon or addon != self._namespace: 128 | return 129 | self.save() 130 | 131 | AddonsDialog.onConfig = wrap(AddonsDialog.onConfig, wrappedOnConfig, "before") 132 | 133 | def _saveBeforeConfigLoaded(self) -> bool: 134 | self.save() 135 | # instructs Anki to continue with config dialog: 136 | return False 137 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/consts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Package-wide constants 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | 40 | def setAddonProperties(addon): 41 | """Update ADDON class properties from another ADDON class 42 | 43 | Arguments: 44 | addon {object} -- an ADDON class with properties stored as class 45 | attributes 46 | """ 47 | for key, value in addon.__dict__.items(): 48 | if key.startswith("__") and key.endswith("__"): 49 | # ignore special attributes 50 | continue 51 | setattr(ADDON, key, value) 52 | 53 | class ADDON(object): 54 | """Class storing general add-on properties 55 | Property names need to be all-uppercase with no leading underscores. 56 | Should be updated by add-on on initialization. 57 | """ 58 | NAME = "" 59 | MODULE = "" 60 | REPO = "" 61 | ID = "" 62 | VERSION = "" 63 | LICENSE = "" 64 | AUTHORS = () 65 | AUTHOR_MAIL = "" 66 | LIBRARIES = () 67 | CONTRIBUTORS = () 68 | SPONSORS = () 69 | MEMBERS_CREDITED = () 70 | MEMBERS_TOP = () 71 | LINKS = {} 72 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/debug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Log add-on events 34 | """ 35 | 36 | import os 37 | import sys 38 | from datetime import datetime 39 | 40 | # need to vendorize 'logging' as Anki's 'logging' does not contain handlers 41 | from ._vendor import logging 42 | from ._vendor.logging import handlers 43 | 44 | from .consts import ADDON 45 | from .anki.utils import debugInfo 46 | from .platform import PATH_THIS_ADDON 47 | 48 | __all__ = [ 49 | "logger", "enableDebugging", "disableDebugging", "maybeStartDebugging", 50 | "startDebugging", "PATH_LOG" 51 | ] 52 | 53 | 54 | PATH_LOG = os.path.join(PATH_THIS_ADDON, "log.txt") 55 | 56 | logger = logging.getLogger(ADDON.MODULE) 57 | 58 | _cli_handler = logging.StreamHandler(sys.stdout) 59 | _file_handler = handlers.RotatingFileHandler( 60 | PATH_LOG, maxBytes=2000000, backupCount=1, delay=True) 61 | 62 | _fmt = ("%(asctime)s %(filename)s:%(funcName)s:%(lineno)-8s " 63 | "%(levelname)-8s: %(message)s") 64 | _fmt_date = '%Y-%m-%dT%H:%M:%S%z' 65 | 66 | _formatter = logging.Formatter(_fmt, _fmt_date) 67 | _file_handler.setFormatter(_formatter) 68 | _cli_handler.setFormatter(_formatter) 69 | 70 | logger.addHandler(_file_handler) 71 | logger.addHandler(_cli_handler) 72 | 73 | logger.setLevel(logging.ERROR) 74 | 75 | PATH_DEBUG_ENABLER = os.path.join(PATH_THIS_ADDON, "debug") 76 | 77 | 78 | def isDebuggingOn(): 79 | return logger.level == logging.DEBUG 80 | 81 | 82 | def debugFileSet(): 83 | return os.path.exists(PATH_DEBUG_ENABLER) 84 | 85 | 86 | def toggleDebugging(): 87 | if debugFileSet(): 88 | disableDebugging() 89 | return False 90 | else: 91 | enableDebugging() 92 | return True 93 | 94 | 95 | def enableDebugging(): 96 | if debugFileSet(): 97 | return 98 | with open(PATH_DEBUG_ENABLER, "w"): 99 | pass 100 | if not isDebuggingOn(): 101 | startDebugging() 102 | 103 | 104 | def disableDebugging(): 105 | if not debugFileSet(): 106 | return 107 | os.remove(PATH_DEBUG_ENABLER) 108 | if isDebuggingOn(): 109 | stopDebugging() 110 | 111 | 112 | def maybeStartDebugging(): 113 | if not debugFileSet(): 114 | return 115 | startDebugging() 116 | 117 | 118 | def startDebugging(): 119 | logger.setLevel(logging.DEBUG) 120 | time = datetime.today().strftime(_fmt_date) 121 | logger.info("="*79) 122 | logger.info(22 * " " + "START {name} log {time}".format( 123 | name=ADDON.NAME, time=time) + 22 * " ") 124 | logger.info("="*79) 125 | logger.info(debugInfo()) 126 | logger.info("="*79) 127 | 128 | 129 | def stopDebugging(): 130 | logger.setLevel(logging.ERROR) 131 | 132 | 133 | def getLatestLog(): 134 | if not os.path.exists(PATH_LOG): 135 | return False 136 | with open(PATH_LOG, "r") as f: 137 | log = f.read() 138 | return log 139 | 140 | 141 | def openLog(): 142 | if not os.path.exists(PATH_LOG): 143 | return False 144 | from .utils import openFile 145 | openFile(PATH_LOG) 146 | 147 | def clearLog(): 148 | if not os.path.exists(PATH_LOG): 149 | return False 150 | with open(PATH_LOG, "w") as f: 151 | f.write("") 152 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Package for custom reusable Qt dialogs (Anki-specific) 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/about.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Generate 'about' info, including credits, copyright, etc. 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from ..consts import ADDON 40 | from ..platform import ANKI20 41 | 42 | if not ANKI20: 43 | string = str 44 | else: 45 | import string 46 | 47 | libs_header = ( 48 | "

{} ships with the following third-party code:

".format(ADDON.NAME)) 49 | 50 | html_template = """\ 51 | 52 | 53 | 54 | 65 | 66 | 67 | {title} 68 |

Credits

69 | {authors_string} 70 | {libs_string} 71 | 72 |

Thank you!

73 |

My heartfelt thanks go out to everyone who has supported this add-on through their tips, 74 | contributions, or any other means. You guys rock!

75 |

In particular I would like to thank all of the awesome people who support me 76 | on Patreon, including:

77 |
{members_string}
78 |

Want to be listed here? 79 | Pledge your support on Patreon now 80 | to receive all kinds of exclusive goodies! 81 |

82 | 83 |

License

84 |

{display_name} is free and open-source software. The add-on code that runs within 85 | Anki is released under the {license} license, extended by a number of additional terms. 86 | For more information please see the license file that accompanied this program.

87 |

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. 88 | Please see the license file for more details.

89 | 90 | {debugging} 91 | 92 | \ 93 | """ 94 | 95 | debugging_template = """\ 96 |

Debugging

97 |

Please don't use any of the following features unless instructed to:

98 | \ 104 | """ 105 | 106 | authors_template = """\ 107 |

© {years} {name}

\ 108 | """ 109 | 110 | libs_item_template = """\ 111 |
  • {name} ({version}), © {author}, {license}
  • \ 112 | """ 113 | 114 | title_template = """\ 115 |

    About {display_name} (v{version})

    \ 116 | """ 117 | 118 | def getAboutString(title=False, showDebug=False): 119 | authors_string = "\n".join(authors_template.format(**dct) 120 | for dct in ADDON.AUTHORS) 121 | libs_entries = "\n".join(libs_item_template.format(**dct) 122 | for dct in ADDON.LIBRARIES) 123 | if libs_entries: 124 | libs_string = "\n".join((libs_header, "
      ", libs_entries, "
    ")) 125 | else: 126 | libs_string = "" 127 | contributors_string = "

    With patches from: {}

    ".format( 128 | ", ".join(sorted(ADDON.CONTRIBUTORS, key=string.lower)) 129 | ) 130 | 131 | members_top_string = "{}".format(", ".join(ADDON.MEMBERS_TOP)) 132 | members_credited_string = ", ".join(ADDON.MEMBERS_CREDITED) 133 | members_string = "

    {t},

    {r}

    ".format( 134 | t=members_top_string, r=members_credited_string) 135 | 136 | if title: 137 | title_string = title_template.format(display_name=ADDON.NAME, 138 | version=ADDON.VERSION) 139 | else: 140 | title_string = "" 141 | 142 | if showDebug: 143 | debugging = debugging_template 144 | else: 145 | debugging = "" 146 | 147 | return html_template.format(display_name=ADDON.NAME, 148 | license=ADDON.LICENSE, 149 | title=title_string, 150 | authors_string=authors_string, 151 | libs_string=libs_string, 152 | contributors_string=contributors_string, 153 | members_string=members_string, 154 | qrc_prefix=ADDON.MODULE, 155 | debugging=debugging) 156 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Package for custom reusable Qt dialogs (not Anki-specific) 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/dialog_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Basic QDialog, extended with some quality-of-life improvements 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from .widgets.qt import * 40 | from .interface import CommonWidgetInterface 41 | 42 | __all__ = ["BasicDialog"] 43 | 44 | 45 | class BasicDialog(QDialog): 46 | 47 | def __init__(self, form_module=None, parent=None, **kwargs): 48 | super(BasicDialog, self).__init__(parent=parent, **kwargs) 49 | self.parent = parent 50 | self.interface = CommonWidgetInterface(self) 51 | # Set up UI from pre-generated UI form: 52 | if form_module: 53 | self.form = form_module.Ui_Dialog() 54 | self.form.setupUi(self) 55 | self._setupUI() 56 | self._setupEvents() 57 | self._setupShortcuts() 58 | 59 | # WIDGET SET-UP 60 | 61 | def _setupUI(self): 62 | """ 63 | Set up any type of subsequent UI modifications 64 | (e.g. adding custom widgets on top of form) 65 | """ 66 | pass 67 | 68 | def _setupEvents(self): 69 | """Set up any type of event bindings""" 70 | pass 71 | 72 | def _setupShortcuts(self): 73 | """Set up any type of keyboard shortcuts""" 74 | pass 75 | 76 | # DIALOG OPEN/CLOSE 77 | 78 | def _onClose(self): 79 | """Executed whenever dialog closed""" 80 | pass 81 | 82 | def _onAccept(self): 83 | """Executed only if dialog confirmed""" 84 | pass 85 | 86 | def _onReject(self): 87 | """Executed only if dialog dismissed""" 88 | pass 89 | 90 | def accept(self): 91 | """Overwrites default accept() to control close actions""" 92 | self._onClose() 93 | self._onAccept() 94 | super(BasicDialog, self).accept() 95 | 96 | def reject(self): 97 | """Overwrites default reject() to control close actions""" 98 | self._onClose() 99 | self._onReject() 100 | super(BasicDialog, self).reject() 101 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Package for custom reusable Qt widgets 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/widgets/qcolorbutton.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Custom color-chooser 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from .qt import QPushButton, QColorDialog, QPixmap, QColor, QIcon, QSize 40 | 41 | class QColorButton(QPushButton): 42 | def __init__(self, parent=None, color="#000000"): 43 | super(QColorButton, self).__init__(parent=parent) 44 | self._updateButtonColor(color) 45 | self.clicked.connect(self._chooseColor) 46 | 47 | def _chooseColor(self): 48 | qcolour = QColor(self.color) 49 | dialog = QColorDialog(qcolour, parent=self) 50 | color = dialog.getColor() 51 | if not color.isValid(): 52 | return False 53 | color = color.name() 54 | self._updateButtonColor(color) 55 | 56 | def _updateButtonColor(self, color): 57 | """Generate color preview pixmap and place it on button""" 58 | pixmap = QPixmap(128, 18) 59 | qcolour = QColor(0, 0, 0) 60 | qcolour.setNamedColor(color) 61 | pixmap.fill(qcolour) 62 | self.setIcon(QIcon(pixmap)) 63 | self.setIconSize(QSize(128, 18)) 64 | self.color = color 65 | 66 | def color(self): 67 | return self.color 68 | 69 | def setColor(self, color): 70 | self._updateButtonColor(color) 71 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/widgets/qkeygrabber.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Custom hotkey selector 34 | 35 | NOTE: obsolete on PyQt5 36 | """ 37 | 38 | from __future__ import (absolute_import, division, 39 | print_function, unicode_literals) 40 | 41 | from ....platform import PLATFORM 42 | 43 | from .qt import QDialog, QPushButton, QVBoxLayout, QLabel, Qt, QKeySequence 44 | 45 | PLATFORM_MODKEY_NAMES = { 46 | "lin": {"meta": "Meta", "ctrl": "Ctrl", 47 | "alt": "Alt", "shift": "Shift"}, 48 | "win": {"meta": "Win", "ctrl": "Ctrl", "alt": 49 | "Alt", "shift": "Shift"}, 50 | "mac": {"meta": "Control", "ctrl": "Command", 51 | "alt": "Option", "shift": "Shift"} 52 | } 53 | 54 | class QKeyGrabButton(QPushButton): 55 | def __init__(self, parent=None, key_string=""): 56 | super(QKeyGrabButton, self).__init__("", parent=parent) 57 | self.setKey(key_string) 58 | self.clicked.connect(self.grabKey) 59 | 60 | def setKey(self, key_string): 61 | self.key_string = key_string 62 | qkeyseq = QKeySequence(key_string, QKeySequence.PortableText) 63 | native_key_string = qkeyseq.toString(format=QKeySequence.NativeText) 64 | self.setText(native_key_string) 65 | 66 | def key(self): 67 | return self.key_string 68 | 69 | def grabKey(self): 70 | """Invoke key grabber""" 71 | grabber = QKeyGrab(self.parent()) 72 | ret = grabber.exec_() 73 | if ret != 1: 74 | return 75 | key_string = grabber.key_string 76 | if not key_string: # or not ret 77 | return 78 | self.setKey(key_string) 79 | 80 | 81 | class QKeyGrab(QDialog): 82 | """ 83 | Simple key combination grabber for hotkey assignments 84 | 85 | Based in part on ImageResizer by searene 86 | (https://github.com/searene/Anki-Addons) 87 | """ 88 | 89 | modkey_names = PLATFORM_MODKEY_NAMES[PLATFORM] 90 | 91 | def __init__(self, parent): 92 | """ 93 | Initialize dialog 94 | 95 | Arguments: 96 | parent {QWidget} -- Parent Qt widget 97 | """ 98 | QDialog.__init__(self, parent=parent) 99 | self.parent = parent 100 | # self.active is used to trace whether there's any key held now: 101 | self.active = 0 102 | self._resetDialog() 103 | self._setupUI() 104 | 105 | def _setupUI(self): 106 | """Basic UI setup""" 107 | mainLayout = QVBoxLayout() 108 | self.label = QLabel("Please press the key combination\n" 109 | "you would like to assign") 110 | self.label.setAlignment(Qt.AlignCenter) 111 | mainLayout.addWidget(self.label) 112 | self.setLayout(mainLayout) 113 | self.setWindowTitle("Grab key combination") 114 | 115 | def _resetDialog(self): 116 | self.extra = self.key_string = None 117 | self.meta = self.ctrl = self.alt = self.shift = False 118 | 119 | def keyPressEvent(self, evt): 120 | """ 121 | Intercept key presses and save current key plus 122 | active modifiers. 123 | 124 | Arguments: 125 | evt {QKeyEvent} -- Intercepted key press event 126 | """ 127 | self.active += 1 128 | 129 | key = evt.key() 130 | if key > 0 and key < 127: 131 | self.extra = chr(key) 132 | elif key == Qt.Key_Control: 133 | self.ctrl = True 134 | elif key == Qt.Key_Alt: 135 | self.alt = True 136 | elif key == Qt.Key_Shift: 137 | self.shift = True 138 | elif key == Qt.Key_Meta: 139 | self.meta = True 140 | else: 141 | self.extra = QKeySequence(key).toString() 142 | self.other = True 143 | 144 | def keyReleaseEvent(self, evt): 145 | """ 146 | Intercept key release event, checking and then saving key combo 147 | and exiting dialog. 148 | 149 | Arguments: 150 | evt {QKeyEvent} -- Intercepted key release event 151 | """ 152 | self.active -= 1 153 | 154 | if self.active != 0: 155 | # at least 1 key still held 156 | return 157 | 158 | # TODO: platform-specific messages 159 | msg = None 160 | if not (self.shift or self.ctrl or self.alt or self.meta or self.other): 161 | msg = ("Please use at least one keyboard modifier\n" 162 | "({meta}, {ctrl}, {alt}, {shift})".format( 163 | **self.modkey_names)) 164 | if (self.shift and not (self.ctrl or self.alt or self.meta or self.other)): 165 | msg = ("Shift needs to be combined with at least one\n" 166 | "other modifier ({meta}, {ctrl}, {alt})".format( 167 | **self.modkey_names)) 168 | if not self.extra: 169 | msg = ("Please press at least one key that is \n" 170 | "not a modifier (not {meta}, {ctrl}, " 171 | "{alt}, or {shift})".format( 172 | **self.modkey_names)) 173 | 174 | if msg: 175 | self.label.setText(msg) 176 | self._resetDialog() 177 | return 178 | 179 | combo = [] 180 | if self.meta: 181 | combo.append("Meta") 182 | if self.ctrl: 183 | combo.append("Ctrl") 184 | if self.shift: 185 | combo.append("Shift") 186 | if self.alt: 187 | combo.append("Alt") 188 | combo.append(self.extra) 189 | 190 | self.key_string = "+".join(combo) 191 | 192 | self.accept() 193 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/widgets/qt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018 Aristotelis P. 6 | # Copyright (C) 2013-2018 Damien Elmes 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License as 10 | # published by the Free Software Foundation, either version 3 of the 11 | # License, or (at your option) any later version, with the additions 12 | # listed at the end of the license file that accompanied this program. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU Affero General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Affero General Public License 20 | # along with this program. If not, see . 21 | # 22 | # NOTE: This program is subject to certain additional terms pursuant to 23 | # Section 7 of the GNU Affero General Public License. You should have 24 | # received a copy of these additional terms immediately following the 25 | # terms and conditions of the GNU Affero General Public License that 26 | # accompanied this program. 27 | # 28 | # If not, please request a copy through one of the means of contact 29 | # listed here: . 30 | # 31 | # Any modifications to this file must keep this entire header intact. 32 | 33 | """ 34 | Qt imports 35 | """ 36 | 37 | from __future__ import (absolute_import, division, 38 | print_function, unicode_literals) 39 | 40 | # extracted from aqt.qt: 41 | import sip 42 | 43 | try: 44 | from PyQt5.Qt import * # noqa: F401 45 | except ImportError: 46 | sip.setapi('QString', 2) 47 | sip.setapi('QVariant', 2) 48 | sip.setapi('QUrl', 2) 49 | try: 50 | sip.setdestroyonexit(False) 51 | except: # noqa: E722 52 | # missing in older versions 53 | pass 54 | from PyQt4.QtCore import * # noqa: F401 55 | from PyQt4.QtGui import * # noqa: F401 56 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/basic/widgets/qutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Miscellaneous Qt utilities 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from .qt import QMessageBox 40 | 41 | def showInfo(message, parent=None, mode="info", title="Anki"): 42 | if mode == "info": 43 | icon = QMessageBox.Information 44 | elif mode == "warning": 45 | icon = QMessageBox.Warning 46 | elif mode == "critical": 47 | icon = QMessageBox.Critical 48 | 49 | return QMessageBox(icon, title, message, parent=parent) 50 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/dialog_configeditor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | anki20 compat: Add-on configuration editor 34 | """ 35 | 36 | import aqt 37 | from aqt.qt import * 38 | from aqt.utils import tooltip, showInfo 39 | 40 | from anki.utils import json 41 | 42 | from .._vendor import markdown2 43 | 44 | from ..consts import ADDON 45 | from ..platform import PATH_THIS_ADDON 46 | 47 | from .dialog_htmlview import HTMLViewer 48 | 49 | 50 | class ConfigEditor(QDialog): 51 | 52 | def __init__(self, config_manager, parent): 53 | super(ConfigEditor, self).__init__(parent=parent) 54 | self.mgr = config_manager 55 | self.form = aqt.forms.editaddon.Ui_Dialog() 56 | self.form.setupUi(self) 57 | self.setWindowTitle("{} Configuration".format(ADDON.NAME)) 58 | self.setupWidgets() 59 | self.updateText(self.mgr["local"]) 60 | self.exec_() 61 | 62 | def setupWidgets(self): 63 | button_box = self.form.buttonBox 64 | restore_btn = button_box.addButton(QDialogButtonBox.RestoreDefaults) 65 | help_btn = button_box.addButton(QDialogButtonBox.Help) 66 | help_btn.clicked.connect(self.onHelpRequested) 67 | restore_btn.clicked.connect(self.onRestoreDefaults) 68 | 69 | def updateText(self, conf): 70 | self.form.text.setPlainText( 71 | json.dumps(conf, ensure_ascii=False, sort_keys=True, 72 | indent=4, separators=(',', ': '))) 73 | 74 | def onRestoreDefaults(self): 75 | default_conf = self.mgr.defaults["local"] 76 | self.updateText(default_conf) 77 | tooltip("Restored defaults", parent=self) 78 | 79 | def onHelpRequested(self): 80 | docs_path = os.path.join(PATH_THIS_ADDON, "config.md") 81 | if not os.path.exists(docs_path): 82 | return False 83 | with open(docs_path, "r") as f: 84 | html = markdown2.markdown(f.read()) 85 | dialog = HTMLViewer(html, title="{} Configuration Help".format( 86 | ADDON.NAME), parent=self) 87 | dialog.show() 88 | 89 | def accept(self): 90 | txt = self.form.text.toPlainText() 91 | error = None 92 | try: 93 | new_conf = json.loads(txt) 94 | except ValueError as e: 95 | new_conf = None 96 | error = repr(e) 97 | 98 | if new_conf and not isinstance(new_conf, dict): 99 | error = "Top level object must be a dictionary." 100 | 101 | if error: 102 | showInfo("The configuration seems to be invalid. Please make " 103 | "sure you haven't made a typo or forgot a control " 104 | "character (e.g. commas, brackets, etc.). " 105 | "Original error message follows below:\n\n{}" 106 | "\n\nIf you're not sure what's wrong you can start " 107 | "from scratch by clicking on 'Restore Defaults' " 108 | "in the config window.".format(error)) 109 | return 110 | 111 | act = self.mgr.conf_updated_action 112 | if act: 113 | act(new_conf) 114 | 115 | super(ConfigEditor, self).accept() 116 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/dialog_contrib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Contributions diaog 34 | 35 | Uses the following addon-level constants, if defined: 36 | 37 | ADDON.NAME, ADDON.AUTHOR_MAIL, ADDON.LINKS 38 | """ 39 | 40 | from __future__ import (absolute_import, division, 41 | print_function, unicode_literals) 42 | 43 | from aqt.utils import openLink 44 | 45 | from ..consts import ADDON 46 | 47 | from .basic.dialog_basic import BasicDialog 48 | from .labelformatter import formatLabels 49 | 50 | from .dialog_htmlview import HTMLViewer 51 | from .about import getAboutString 52 | 53 | 54 | class ContribDialog(BasicDialog): 55 | """ 56 | Add-on agnostic dialog that presents user with a number 57 | of options to support the development of the add-on. 58 | """ 59 | 60 | def __init__(self, form_module, parent=None): 61 | """ 62 | Initialize contrib dialog with provided form 63 | 64 | Arguments: 65 | form_module {PyQt form module} -- PyQt dialog form outlining the UI 66 | 67 | Provided Qt form should contain the following widgets: 68 | QPushButton: btnMail, btnCoffee, btnPatreon, btnCredits 69 | 70 | Keyword Arguments: 71 | parent {QWidget} -- Parent Qt widget (default: {None}) 72 | """ 73 | 74 | super(ContribDialog, self).__init__(form_module=form_module, 75 | parent=parent) 76 | 77 | def _setupUI(self): 78 | formatLabels(self, self._linkHandler) 79 | 80 | def _setupEvents(self): 81 | """ 82 | Connect button presses to actions 83 | """ 84 | mail_string = "mailto:{}".format(ADDON.AUTHOR_MAIL) 85 | self.form.btnMail.clicked.connect( 86 | lambda: openLink(mail_string)) 87 | self.form.btnCoffee.clicked.connect( 88 | lambda: openLink(ADDON.LINKS["coffee"])) 89 | self.form.btnPatreon.clicked.connect( 90 | lambda: openLink(ADDON.LINKS["patreon"])) 91 | self.form.btnCredits.clicked.connect( 92 | self._showCredits) 93 | 94 | def _showCredits(self): 95 | viewer = HTMLViewer(getAboutString(title=True), 96 | title=ADDON.NAME, parent=self) 97 | viewer.exec_() 98 | 99 | def _linkHandler(self, url): 100 | """Support for binding custom actions to text links""" 101 | if not url.startswith("action://"): 102 | return openLink(url) 103 | protocol, cmd = url.split("://") 104 | if cmd == "installed-addons": 105 | print("invoking installed addons dialog") 106 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/dialog_htmlview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Simple dialog for viewing HTML 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from aqt.qt import * 40 | 41 | from ..platform import PLATFORM 42 | 43 | from .basic.dialog_basic import BasicDialog 44 | 45 | 46 | class HTMLViewer(BasicDialog): 47 | 48 | def __init__(self, html, title=None, parent=None): 49 | super(HTMLViewer, self).__init__(parent=parent) 50 | if PLATFORM == "win": 51 | self.setMinimumWidth(400) 52 | self.setMinimumHeight(500) 53 | else: 54 | self.setMinimumWidth(500) 55 | self.setMinimumHeight(600) 56 | if title: 57 | self.setWindowTitle(title) 58 | self.setHtml(html) 59 | 60 | def _setupUI(self): 61 | layout = QVBoxLayout(self) 62 | self.setLayout(layout) 63 | self._browser = QTextBrowser(self) 64 | self._browser.setOpenExternalLinks(True) 65 | layout.addWidget(self._browser) 66 | 67 | def setHtml(self, html): 68 | self._browser.setHtml(html) 69 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/dialog_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Main options dialog 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from aqt.qt import Qt, QUrl, QApplication 40 | 41 | from aqt.utils import openLink, tooltip 42 | 43 | from ..consts import ADDON 44 | from ..platform import PLATFORM 45 | from ..debug import (toggleDebugging, isDebuggingOn, 46 | getLatestLog, openLog, clearLog) 47 | 48 | from .basic.dialog_mapped import MappedDialog 49 | from .about import getAboutString 50 | from .labelformatter import formatLabels 51 | 52 | 53 | class OptionsDialog(MappedDialog): 54 | 55 | def __init__(self, mapped_widgets, config, form_module=None, 56 | parent=None, **kwargs): 57 | """ 58 | Creates an options dialog with the provided Qt form and populates its 59 | widgets from a ConfigManager config object. 60 | 61 | Arguments: 62 | mapped_widgets {sequence} -- A list or tuple of mappings between 63 | widget names, config value names, and 64 | special methods to act as mediators 65 | (see MappedDialog docstring for specs) 66 | config {ConfigManager} -- ConfigManager object providing access to 67 | add-on config values 68 | 69 | Keyword Arguments: 70 | form_module {PyQt form module} -- Dialog form module generated 71 | through pyuic (default: {None}) 72 | parent {QWidget} -- Parent Qt widget (default: {None}) 73 | 74 | 75 | """ 76 | # Mediator methods defined in mapped_widgets might need access to 77 | # certain instance attributes. As super().__init__ instantiates 78 | # all widget values it is important that we set these attributes 79 | # beforehand: 80 | self.config = config 81 | super(OptionsDialog, self).__init__( 82 | mapped_widgets, self.config.all, self.config.defaults, 83 | form_module=form_module, parent=parent) 84 | # Instance methods that modify the initialized UI should either be 85 | # called from self._setupUI or from here 86 | 87 | # Static widget setup 88 | 89 | def _setupUI(self): 90 | formatLabels(self, self._linkHandler) 91 | self._setupAbout() 92 | self._setupLabDebug() 93 | 94 | if PLATFORM == "mac": 95 | # Decrease tab margins on macOS 96 | tab_widget = getattr(self.form, "tabWidget", None) 97 | if not tab_widget: 98 | return 99 | for idx in range(tab_widget.count()): 100 | tab = tab_widget.widget(idx) 101 | if not tab: 102 | continue 103 | layout = tab.layout() 104 | if not layout: 105 | continue 106 | layout.setContentsMargins(3, 3, 3, 3) 107 | 108 | def _setupAbout(self): 109 | """ 110 | Fill out 'about' widget 111 | """ 112 | if hasattr(self.form, "htmlAbout"): 113 | about_string = getAboutString(showDebug=True) 114 | self.form.htmlAbout.setHtml(about_string) 115 | self.form.htmlAbout.setOpenLinks(False) 116 | self.form.htmlAbout.anchorClicked.connect(self._linkHandler) 117 | 118 | def _setupLabDebug(self): 119 | label = getattr(self.form, "labDebug", None) 120 | if not label: 121 | return 122 | if isDebuggingOn(): 123 | label.setText( 124 | "DEBUG ACTIVE") 125 | else: 126 | label.setText("") 127 | 128 | # Events 129 | 130 | def keyPressEvent(self, evt): 131 | """ 132 | Prevent accidentally closing dialog when editing complex widgets 133 | by ignoring Return and Escape 134 | """ 135 | if evt.key() == Qt.Key_Enter or evt.key() == Qt.Key_Return: 136 | return evt.accept() 137 | super(OptionsDialog, self).keyPressEvent(evt) 138 | 139 | def _setupEvents(self): 140 | super(OptionsDialog, self)._setupEvents() 141 | for name, link in ADDON.LINKS.items(): 142 | btn_widget = getattr(self.form, "btn" + name.capitalize(), None) 143 | if not btn_widget: 144 | continue 145 | btn_widget.clicked.connect(lambda _, link=link: openLink(link)) 146 | 147 | # Link actions 148 | 149 | def _linkHandler(self, url): 150 | """Support for binding custom actions to text links""" 151 | if isinstance(url, QUrl): 152 | url = url.toString() 153 | if not url.startswith("action://"): 154 | return openLink(url) 155 | protocol, cmd = url.split("://") 156 | if cmd == "debug-toggle": 157 | self._toggleDebugging() 158 | elif cmd == "debug-open": 159 | self._openDebuglog() 160 | elif cmd == "debug-copy": 161 | self._copyDebuglog() 162 | elif cmd == "debug-clear": 163 | self._clearDebuglog() 164 | elif cmd == "changelog": 165 | self._openChangelog() 166 | 167 | def _toggleDebugging(self): 168 | if toggleDebugging(): 169 | msg = "enabled" 170 | else: 171 | msg = "disabled" 172 | tooltip("Debugging {msg}".format(msg=msg)) 173 | self._setupLabDebug() 174 | 175 | def _copyDebuglog(self): 176 | log = getLatestLog() 177 | if log is False: 178 | tooltip("No debug log has been recorded, yet") 179 | return False 180 | QApplication.clipboard().setText(log) 181 | tooltip("Copied to clipboard") 182 | 183 | def _openDebuglog(self): 184 | ret = openLog() 185 | if ret is False: 186 | tooltip("No debug log has been recorded, yet") 187 | return False 188 | 189 | def _openChangelog(self): 190 | changelog = ADDON.LINKS.get("changelog") 191 | if not changelog: 192 | return 193 | openLink(changelog) 194 | 195 | def _clearDebuglog(self): 196 | ret = clearLog() 197 | if ret is False: 198 | tooltip("No debug log has been recorded, yet") 199 | return False 200 | tooltip("Debug log cleared") 201 | 202 | # Exit handling 203 | 204 | def _onAccept(self): 205 | """Executed only if dialog confirmed""" 206 | self.getData() # updates self.config in place 207 | self.config.save() 208 | super(OptionsDialog, self)._onAccept() 209 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/gui/labelformatter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Utilities to fill out predefined data in dialog text labels 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | from aqt.qt import * 40 | 41 | from ..consts import ADDON 42 | from ..platform import ANKI20 43 | 44 | format_dict = { 45 | "ADDON_NAME": ADDON.NAME, 46 | "ADDON_VERSION": ADDON.VERSION, 47 | } 48 | 49 | if not ANKI20: 50 | fmt_find_params = ((QLabel, QPushButton), QRegExp(".*"), 51 | Qt.FindChildrenRecursively) 52 | else: 53 | # Qt4: recursive by default. No third param. 54 | fmt_find_params = ((QLabel, QPushButton), QRegExp(".*")) 55 | 56 | 57 | def formatLabels(dialog, linkhandler=None): 58 | for widget in dialog.findChildren(*fmt_find_params): 59 | if widget.objectName().startswith("fmt"): 60 | widget.setText(widget.text().format(**format_dict)) 61 | if linkhandler and isinstance(widget, QLabel): 62 | widget.linkActivated.connect(linkhandler) 63 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/platform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2019 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Provides information on Anki version and platform 34 | """ 35 | 36 | from __future__ import (absolute_import, division, 37 | print_function, unicode_literals) 38 | 39 | import sys 40 | import os 41 | 42 | from aqt import mw 43 | 44 | from anki import version as anki_version 45 | from anki.utils import isMac, isWin 46 | 47 | from .utils import ensureExists 48 | 49 | if isMac: 50 | PLATFORM = "mac" 51 | elif isWin: 52 | PLATFORM = "win" 53 | else: 54 | PLATFORM = "lin" 55 | 56 | SYS_ENCODING = sys.getfilesystemencoding() 57 | PYTHON3 = sys.version_info[0] == 3 58 | ANKI20 = anki_version.startswith("2.0.") 59 | 60 | name_components = __name__.split(".") 61 | 62 | MODULE_ADDON = name_components[0] 63 | MODULE_LIBADDON = name_components[1] 64 | 65 | PATH_ADDONS = mw.pm.addonFolder() 66 | 67 | if ANKI20: 68 | JSPY_BRIDGE = "py.link" 69 | else: 70 | JSPY_BRIDGE = "pycmd" 71 | 72 | PATH_THIS_ADDON = os.path.join(PATH_ADDONS, MODULE_ADDON) 73 | 74 | 75 | def schedVer(): 76 | if ANKI20: 77 | return 1 78 | if not mw.col: # collection not loaded 79 | return None 80 | return mw.col.schedVer() 81 | 82 | 83 | def pathUserFiles(): 84 | user_files = os.path.join(PATH_THIS_ADDON, "user_files") 85 | return ensureExists(user_files) 86 | 87 | 88 | def pathMediaFiles(): 89 | return mw.col.media.dir() 90 | 91 | 92 | def checkAnkiVersion(lower, upper=None): 93 | """Check whether anki version is in specified range 94 | 95 | By default the upper boundary is set to infinite 96 | 97 | Arguments: 98 | lower {str} -- minimum version (inclusive) 99 | 100 | Keyword Arguments: 101 | upper {str} -- maximum version (exclusive) (default: {None}) 102 | 103 | Returns: 104 | bool -- Whether anki version is in specified range 105 | """ 106 | return checkVersion(anki_version, lower, upper=upper) 107 | 108 | 109 | def checkQtVersion(lower, upper=None): 110 | """Check whether Qt version is in specified range 111 | 112 | By default the upper boundary is set to infinite 113 | 114 | Arguments: 115 | lower {str} -- minimum version (inclusive) 116 | 117 | Keyword Arguments: 118 | upper {str} -- maximum version (exclusive) (default: {None}) 119 | 120 | Returns: 121 | bool -- Whether Qt version is in specified range 122 | """ 123 | from aqt.qt import QT_VERSION_STR 124 | return checkVersion(QT_VERSION_STR, lower, upper=upper) 125 | 126 | 127 | def checkVersion(current, lower, upper=None): 128 | """Generic version checker 129 | 130 | Checks whether specified version is in specified range 131 | 132 | Arguments: 133 | current {str} -- current version 134 | lower {str} -- minimum version (inclusive) 135 | 136 | Keyword Arguments: 137 | upper {str} -- maximum version (exclusive) (default: {None}) 138 | 139 | Returns: 140 | bool -- Whether current version is in specified range 141 | """ 142 | from ._vendor.packaging import version 143 | 144 | if upper is not None: 145 | ankiv_parsed = version.parse(current) 146 | return (ankiv_parsed >= version.parse(lower) and 147 | ankiv_parsed < version.parse(upper)) 148 | 149 | return version.parse(current) >= version.parse(lower) 150 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Utility modules not specific to Anki 34 | """ 35 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/filesystem.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | File system manipulation utilities 34 | """ 35 | 36 | import os 37 | import sys 38 | 39 | from .types import PathOrString 40 | 41 | 42 | def ensureExists(path: PathOrString) -> str: 43 | path = str(path) 44 | if not os.path.exists(path): 45 | os.makedirs(path) 46 | return path 47 | 48 | 49 | def openFile(path: PathOrString) -> None: 50 | """Open file in default viewer""" 51 | import subprocess 52 | 53 | path = str(path) 54 | 55 | if sys.platform.startswith("win32"): 56 | try: 57 | os.startfile(path) # type: ignore 58 | except (OSError, UnicodeDecodeError): 59 | pass 60 | elif sys.platform.startswith("darwin"): 61 | subprocess.call(("open", path)) 62 | else: 63 | subprocess.call(("xdg-open", path)) 64 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/nesting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Manipulation of nested data structures 34 | """ 35 | 36 | from functools import reduce 37 | from copy import deepcopy 38 | 39 | from typing import Union, Any 40 | 41 | # Utility functions for operating with nested objects 42 | 43 | 44 | def getNestedValue(obj: Any, keys: Union[list, tuple]): 45 | """ 46 | Get value out of nested collection by supplying tuple of 47 | nested keys/indices 48 | 49 | Arguments: 50 | obj {Collection} -- Nested collection 51 | keys {list/tuple} -- Key/index path leading to config val 52 | 53 | Returns: 54 | Any -- Config value 55 | """ 56 | cur = obj 57 | for nr, key in enumerate(keys): 58 | cur = cur[key] 59 | return cur 60 | 61 | 62 | def setNestedValue(obj: Any, keys: Union[list, tuple], value) -> None: 63 | """ 64 | Set value in nested collection by supplying Sequence of 65 | nested keys / indices, and value to set 66 | 67 | Arguments: 68 | obj {Collection} -- Nested collection 69 | keys {list/tuple} -- Key/index path leading to config val 70 | value {Any} -- value 71 | """ 72 | depth = len(keys) - 1 73 | cur = obj 74 | for nr, key in enumerate(keys): 75 | if nr == depth: 76 | cur[key] = value 77 | return 78 | cur = cur[key] 79 | 80 | 81 | def getNestedAttribute(obj: Any, attr: str, *args) -> Any: 82 | """ 83 | Gets nested attribute from "."-separated string 84 | 85 | Arguments: 86 | obj {object} -- object to parse 87 | attr {string} -- attribute name, optionally including 88 | "."-characters to denote different levels 89 | of nesting 90 | 91 | Returns: 92 | Any -- object corresponding to attribute name 93 | 94 | Credits: 95 | https://gist.github.com/wonderbeyond/d293e7a2af1de4873f2d757edd580288 96 | """ 97 | 98 | def _getattr(obj: Any, attr: str): 99 | return getattr(obj, attr, *args) 100 | 101 | return reduce(_getattr, [obj] + attr.split(".")) 102 | 103 | 104 | def deepMergeLists(original: list, incoming: list, new: bool = False) -> list: 105 | """ 106 | Deep merge two lists. Optionally leaves original intact. 107 | 108 | Procedure: 109 | Reursively call deep merge on each correlated element of list. 110 | If item type in both elements are 111 | a. dict: Call deepMergeDicts on both values. 112 | b. list: Call deepMergeLists on both values. 113 | c. any other type: Value is overridden. 114 | d. conflicting types: Value is overridden. 115 | 116 | If incoming list longer than original then extra values are appended. 117 | 118 | Arguments: 119 | original {list} -- original list 120 | incoming {list} -- list with updated values 121 | new {bool} -- whether or not to create a new list instead of 122 | updating original 123 | 124 | Returns: 125 | list -- Merged list 126 | 127 | Credits: 128 | https://stackoverflow.com/a/50773244/1708932 129 | """ 130 | result = original if not new else deepcopy(original) 131 | 132 | common_length = min(len(original), len(incoming)) 133 | for idx in range(common_length): 134 | if isinstance(result[idx], dict) and isinstance(incoming[idx], dict): 135 | deepMergeDicts(result[idx], incoming[idx]) 136 | elif isinstance(result[idx], list) and isinstance(incoming[idx], list): 137 | deepMergeLists(result[idx], incoming[idx]) 138 | else: 139 | result[idx] = incoming[idx] 140 | 141 | for idx in range(common_length, len(incoming)): 142 | result.append(incoming[idx]) 143 | 144 | return result 145 | 146 | 147 | def deepMergeDicts(original: dict, incoming: dict, new: bool = False) -> dict: 148 | """ 149 | Deep merge two dictionaries. Optionally leaves original intact. 150 | 151 | Procedure: 152 | For key conflicts if both values are: 153 | a. dict: Recursively call deepMergeDicts on both values. 154 | b. list: Call deepMergeLists on both values. 155 | c. any other type: Original value is overridden. 156 | d. conflicting types: Original value is preserved. 157 | 158 | In the context of Anki config objects: 159 | - original should correspond to default config, i.e. the "scheme" 160 | of the expected config values 161 | - incoming should correspond to the user-specific values 162 | - incoming values takes precedence over original values with the 163 | exception of: 164 | - new values added to the configuration 165 | - existing values whose data types have changed (e.g. list → dict) 166 | 167 | Arguments: 168 | original {dict} -- original dictionary 169 | incoming {dict} -- dictionary with updated values 170 | new {bool} -- whether or not to create a new dictionary instead of 171 | updating original 172 | 173 | Returns: 174 | dict -- Merged dictionaries 175 | 176 | Credits: 177 | https://stackoverflow.com/a/50773244/1708932 178 | 179 | """ 180 | result = original if not new else deepcopy(original) 181 | 182 | for key in incoming: 183 | if key in result: 184 | if isinstance(result[key], dict) and isinstance(incoming[key], dict): 185 | deepMergeDicts(result[key], incoming[key]) 186 | elif isinstance(result[key], list) and isinstance(incoming[key], list): 187 | deepMergeLists(result[key], incoming[key]) 188 | elif result[key] is not None and (type(result[key]) != type(incoming[key])): 189 | # switched to different data type, original takes precedence 190 | # with the exception of None value in original being replaced 191 | pass 192 | else: 193 | # type preserved. incoming takes precedence. 194 | result[key] = incoming[key] 195 | else: 196 | result[key] = incoming[key] 197 | 198 | return result 199 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/packaging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # Copyright (C) 2016 Jason R Coombs and other PyPA contributors 7 | # 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License as 11 | # published by the Free Software Foundation, either version 3 of the 12 | # License, or (at your option) any later version, with the additions 13 | # listed at the end of the license file that accompanied this program. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU Affero General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU Affero General Public License 21 | # along with this program. If not, see . 22 | # 23 | # NOTE: This program is subject to certain additional terms pursuant to 24 | # Section 7 of the GNU Affero General Public License. You should have 25 | # received a copy of these additional terms immediately following the 26 | # terms and conditions of the GNU Affero General Public License that 27 | # accompanied this program. 28 | # 29 | # If not, please request a copy through one of the means of contact 30 | # listed here: . 31 | # 32 | # Any modifications to this file must keep this entire header intact. 33 | 34 | """ 35 | Components related to packaging third-party code and libraries 36 | with Anki add-ons 37 | """ 38 | 39 | import os 40 | import sys 41 | 42 | from types import ModuleType 43 | 44 | from typing import Optional 45 | 46 | 47 | __all__ = ["importAny", "addPathToModuleLookup"] 48 | 49 | # Third-party add-on imports 50 | ###################################################################### 51 | 52 | 53 | def importAny(*modules: str) -> Optional[ModuleType]: 54 | """ 55 | Import by name, providing multiple alternative names 56 | 57 | Common use case: Support all the different package names found 58 | between 2.0 add-ons, 2.1 AnkiWeb releases, and 2.1 dev releases 59 | 60 | Raises: 61 | ImportError -- Module not found 62 | 63 | Returns: 64 | module -- Imported python module 65 | """ 66 | for mod in modules: 67 | try: 68 | return __import__(mod) 69 | except ImportError: 70 | pass 71 | raise ImportError("Requires one of " + ", ".join(modules)) 72 | 73 | 74 | # Registering external libraries & modules 75 | ###################################################################### 76 | 77 | # NOTE: Use of these is discouraged and should be reserved for cases where 78 | # traditional vendoring fails or is not feasible to implement. 79 | 80 | # NOTE: I have yet to find a reliable way to add modules to packages 81 | # that *do* ship with Anki, but are missing specific sub-modules 82 | # (e.g. 'version' module of 'distutils') 83 | # 84 | # The issue mainly arises when Anki has already loaded the corresponding 85 | # module at add-on init time. At that point it becomes non-trivial to 86 | # update the module cache with our own version of the module. 87 | # 88 | # It is easy to make due with explicitly importing our own version of 89 | # the module if we have control over the code-base, but in case of 90 | # dependencies of other modules this becomes a major problem 91 | # (e.g. third-party packages depending on stdlib modules missing 92 | # in Anki's Python distribution). 93 | 94 | 95 | def addPathToModuleLookup(path: str) -> None: 96 | """ 97 | Add modules shipped with the add-on to Python module search path 98 | 99 | Arguments: 100 | path {str,unicode} -- Fully qualified path to module directory 101 | """ 102 | assert os.path.isdir(path) 103 | # Insert at idx 0 in order to supersede system-wide packages 104 | sys.path.insert(0, path) 105 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | from pathlib import Path 33 | 34 | from typing import Union 35 | 36 | 37 | ListOrTuple = Union[list, tuple] 38 | PathOrString = Union[str, Path] 39 | -------------------------------------------------------------------------------- /src/popup_dictionary/libaddon/util/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Libaddon for Anki 4 | # 5 | # Copyright (C) 2018-2020 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Utilities for semantic version comparisons 34 | """ 35 | 36 | from .._vendor.packaging import version 37 | 38 | from typing import Optional 39 | 40 | 41 | def checkVersion(current: str, lower: str, upper: Optional[str] = None) -> bool: 42 | """Generic version checker 43 | 44 | Checks whether specified version is in specified range 45 | 46 | Arguments: 47 | current {str} -- current version 48 | lower {str} -- minimum version (inclusive) 49 | 50 | Keyword Arguments: 51 | upper {str} -- maximum version (exclusive) (default: {None}) 52 | 53 | Returns: 54 | bool -- Whether current version is in specified range 55 | """ 56 | 57 | if upper is not None: 58 | current_parsed = version.parse(current) 59 | return current_parsed >= version.parse( 60 | lower 61 | ) and current_parsed < version.parse(upper) 62 | 63 | return version.parse(current) >= version.parse(lower) 64 | -------------------------------------------------------------------------------- /src/popup_dictionary/migrate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | import copy 33 | from typing import Any, Dict, List 34 | 35 | from .config import config 36 | from .libaddon.anki.configmanager import ConfigManager 37 | from .libaddon.platform import checkAnkiVersion 38 | 39 | _KEY_GENERAL_HOTKEY = "generalHotkey" 40 | 41 | 42 | def reset_config_defaults( 43 | config_dict: Dict[str, Any], 44 | default_config_dict: Dict[str, Any], 45 | keys_to_reset: List[str], 46 | ) -> Dict[str, Any]: 47 | for key in keys_to_reset: 48 | config_dict[key] = default_config_dict[key] 49 | return config_dict 50 | 51 | 52 | def migrate_config(config_manager: ConfigManager): 53 | local_config = copy.deepcopy(config_manager["local"]) 54 | default_config = config_manager.defaults["local"] 55 | keys_to_reset = [] 56 | 57 | if ( 58 | checkAnkiVersion("2.1.41") 59 | and local_config[_KEY_GENERAL_HOTKEY] == "Ctrl+Shift+D" 60 | ): 61 | # Anki 2.1.41 and up conflict with the old default key binding 62 | keys_to_reset.append(_KEY_GENERAL_HOTKEY) 63 | 64 | if not keys_to_reset: 65 | return 66 | 67 | config_manager["local"] = reset_config_defaults( 68 | config_dict=local_config, 69 | default_config_dict=default_config, 70 | keys_to_reset=keys_to_reset, 71 | ) 72 | config_manager.save(storage_name="local") 73 | 74 | 75 | def migrate_addon(): 76 | migrate_config(config_manager=config) 77 | -------------------------------------------------------------------------------- /src/popup_dictionary/results.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Parses collection for pertinent notes and generates result list 34 | """ 35 | 36 | import re 37 | from typing import TYPE_CHECKING, List, Optional, Sequence, Union 38 | 39 | from aqt import mw 40 | from aqt.utils import askUser 41 | 42 | from .config import config 43 | from .libaddon.debug import logger 44 | 45 | if TYPE_CHECKING: 46 | from anki.collection import Collection 47 | from anki.notes import Note, NoteId 48 | 49 | PYCMD_IDENTIFIER: str = "popupDictionary" 50 | 51 | # UI messages 52 | 53 | WRN_RESCOUNT: str = ( 54 | "{} relevant notes found.
    " 55 | "The tooltip could take a lot of time to render and
    " 56 | "temporarily slow down Anki.

    " 57 | "Are you sure you want to proceed?" 58 | ) 59 | 60 | # HTML format strings for results 61 | 62 | html_reslist: str = """
    {}
    """ 63 | 64 | html_res_normal: str = f"""\ 65 |
    {{}}
    \ 67 | """ 68 | 69 | html_res_dict: str = f"""\ 70 |
    71 |
    Definition:
    72 | {{}} 73 |
    74 |
    """ 75 | 76 | html_field: str = """
    {}
    """ 77 | 78 | # RegExes for cloze marker removal 79 | 80 | cloze_re_str = r"\{\{c(\d+)::(.*?)(::(.*?))?\}\}" 81 | cloze_re = re.compile(cloze_re_str) 82 | 83 | # Anki API shims 84 | 85 | 86 | def get_note(collection: "Collection", note_id: "NoteId") -> "Note": 87 | try: 88 | return collection.get_note(note_id) 89 | except AttributeError: 90 | return collection.getNote(note_id) 91 | 92 | 93 | def find_notes(collection: "Collection", query: str, **kwargs) -> Sequence["NoteId"]: 94 | try: 95 | return collection.find_notes(query, **kwargs) 96 | except AttributeError: 97 | return collection.findNotes(query, **kwargs) # type:ignore[attr-defined] 98 | 99 | 100 | # Functions that compose tooltip content 101 | 102 | 103 | def get_content_for(term: str, ignore_nid: str) -> str: 104 | """Compose tooltip content for search term. 105 | Returns HTML string.""" 106 | conf = config["local"] 107 | 108 | dict_entry = None 109 | note_content = None 110 | content = [] 111 | 112 | if conf["dictionaryEnabled"]: 113 | dict_entry = search_definition_for(term) 114 | if dict_entry: 115 | content.append(dict_entry) 116 | 117 | if conf["snippetsEnabled"]: 118 | note_content = get_note_snippets_for(term, ignore_nid) 119 | 120 | if note_content: 121 | content.extend(note_content) # type: ignore 122 | 123 | if content: 124 | return html_reslist.format("".join(content)) 125 | elif note_content is False: 126 | return "" 127 | elif note_content is None and conf["generalConfirmEmpty"]: 128 | return "No other results found." 129 | 130 | return "" 131 | 132 | 133 | def get_note_snippets_for(term: str, ignore_nid: str) -> Union[List[str], bool, None]: 134 | """Find relevant note snippets for search term. 135 | Returns list of HTML strings.""" 136 | 137 | conf = config["local"] 138 | 139 | logger.debug("getNoteSnippetsFor called") 140 | # exclude current note 141 | current_nid = mw.reviewer.card.note().id 142 | exclusion_tokens = ["-nid:{}".format(current_nid)] 143 | 144 | if ignore_nid: 145 | exclusion_tokens.append("-nid:{}".format(ignore_nid)) 146 | 147 | if conf["snippetsLimitToCurrentDeck"]: 148 | exclusion_tokens.append("deck:current") 149 | 150 | if conf["snippetsExcludeNewNotes"]: 151 | exclusion_tokens.append("-is:new") 152 | 153 | # construct query string 154 | query = """"{}" {}""".format(term, " ".join(exclusion_tokens)) 155 | 156 | # NOTE: performing the SQL query directly might be faster 157 | res = sorted(find_notes(collection=mw.col, query=query)) 158 | logger.debug("getNoteSnippetsFor query finished.") 159 | 160 | if not res: 161 | return None 162 | 163 | # Prevent slowdowns when search term is too common 164 | res_len = len(res) 165 | warn_limit = conf["snippetsResultsWarnLimit"] 166 | if warn_limit > 0 and res_len > warn_limit: 167 | if not askUser(WRN_RESCOUNT.format(res_len), title="Popup Dictionary"): 168 | return False 169 | 170 | note_content: List[str] = [] 171 | excluded_flds = conf["snippetsExcludedFields"] 172 | 173 | for nid in res: 174 | note = get_note(collection=mw.col, note_id=nid) 175 | valid_flds = [ 176 | html_field.format(i[1]) for i in note.items() if i[0] not in excluded_flds 177 | ] 178 | joined_flds = "".join(valid_flds) 179 | # remove cloze markers 180 | filtered_flds = cloze_re.sub(r"\2", joined_flds) 181 | note_content.append(html_res_normal.format(nid, filtered_flds)) 182 | 183 | return note_content 184 | 185 | 186 | def search_definition_for(term: str) -> Optional[str]: 187 | """Look up search term in dictionary deck. 188 | Returns HTML string.""" 189 | conf = config["local"] 190 | query = """note:"{}" {}:"{}" """.format( 191 | conf["dictionaryNoteTypeName"], conf["dictionaryTermFieldName"], term 192 | ) 193 | res = find_notes(collection=mw.col, query=query) 194 | if res: 195 | nid = res[0] 196 | note = get_note(collection=mw.col, note_id=nid) 197 | try: 198 | result = note[conf["dictionaryDefinitionFieldName"]] 199 | except KeyError: 200 | return None 201 | return html_res_dict.format(nid, result) 202 | 203 | return None 204 | -------------------------------------------------------------------------------- /src/popup_dictionary/reviewer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Modifications to Anki's Reviewer 34 | """ 35 | 36 | import json 37 | from typing import TYPE_CHECKING, Any, Optional, Tuple, Union 38 | 39 | from PyQt5.QtGui import QKeySequence 40 | from PyQt5.QtWidgets import QMenu, QShortcut 41 | 42 | from aqt import mw 43 | from aqt.reviewer import Reviewer 44 | 45 | from .browser import browse_to_nid 46 | from .config import config 47 | from .results import PYCMD_IDENTIFIER, get_content_for 48 | from .web import popup_integrator 49 | 50 | if TYPE_CHECKING: # 2.1.22+ 51 | from aqt.webview import AnkiWebView, WebContent 52 | 53 | 54 | # UI interaction #### 55 | 56 | 57 | def setup_shortcuts(): 58 | QShortcut( # type: ignore 59 | QKeySequence(config["local"]["generalHotkey"]), 60 | mw, 61 | activated=on_lookup_triggered, 62 | ) 63 | 64 | 65 | def on_lookup_triggered(*args): 66 | if mw.state != "review": 67 | return 68 | mw.reviewer.web.eval("invokeTooltipAtSelectedElm();") 69 | 70 | 71 | def on_webview_will_show_context_menu(webview: "AnkiWebView", menu: QMenu): 72 | # shaky heuristic to determine which web view we are in 73 | if hasattr(webview, "title"): 74 | if webview.title != "main webview": 75 | return 76 | 77 | if mw.state != "review" or not webview.selectedText(): 78 | return 79 | 80 | action = menu.addAction("Look up in Pop-up Dictionary...") 81 | action.setShortcut(config["local"]["generalHotkey"]) 82 | action.triggered.connect(on_lookup_triggered) 83 | 84 | 85 | # JS <-> PY communication #### 86 | 87 | 88 | def webview_message_handler(message: str) -> Optional[str]: 89 | cmd, arg = message.split(":", 1) 90 | subcmd = cmd.replace(PYCMD_IDENTIFIER, "") 91 | 92 | if subcmd == "Browse": 93 | (cmd, arg) = message.split(":", 1) 94 | if not arg: 95 | return None 96 | browse_to_nid(int(arg)) 97 | elif subcmd == "Lookup": 98 | (cmd, payload) = message.split(":", 1) 99 | term, ignore_nid = json.loads(payload) 100 | term = term.strip() 101 | return get_content_for(term, ignore_nid) 102 | else: 103 | print(f"Unrecognized pop-up dictionary pycmd identifier {subcmd}") 104 | 105 | return None 106 | 107 | 108 | def on_webview_will_set_content( 109 | web_content: "WebContent", context: Union[Reviewer, Any] 110 | ): 111 | if not isinstance(context, Reviewer): 112 | return 113 | 114 | # Appending to body rather than using header. Not best practice, but let's stay 115 | # on the safe side 116 | web_content.body += popup_integrator 117 | 118 | 119 | def on_webview_did_receive_js_message( 120 | handled: Tuple[bool, Any], message: str, context: Union[Reviewer, Any] 121 | ): 122 | if not isinstance(context, Reviewer): 123 | return handled 124 | 125 | if not message.startswith(PYCMD_IDENTIFIER): 126 | return handled 127 | 128 | callback_value = webview_message_handler(message) 129 | 130 | return (True, callback_value) 131 | 132 | 133 | # Hook into Anki #### 134 | 135 | # ensure that we only patch once on first profile load 136 | _reviewer_patched: bool = False 137 | 138 | 139 | def patch_reviewer(): 140 | global _reviewer_patched 141 | 142 | if _reviewer_patched: 143 | return 144 | 145 | from aqt.gui_hooks import ( 146 | webview_will_set_content, 147 | webview_did_receive_js_message, 148 | ) 149 | 150 | webview_will_set_content.append(on_webview_will_set_content) 151 | webview_did_receive_js_message.append(on_webview_did_receive_js_message) 152 | 153 | _reviewer_patched = True 154 | 155 | 156 | def initialize_reviewer(): 157 | """Delay patching reviewer to counteract bad practices in other add-ons that 158 | overwrite revHtml and _linkHandler in their entirety""" 159 | 160 | from aqt.gui_hooks import profile_did_open, webview_will_show_context_menu 161 | 162 | profile_did_open.append(patch_reviewer) 163 | webview_will_show_context_menu.append(on_webview_will_show_context_menu) 164 | 165 | setup_shortcuts() 166 | -------------------------------------------------------------------------------- /src/popup_dictionary/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | Note type and card templates. 34 | """ 35 | 36 | from typing import TYPE_CHECKING, NamedTuple, Optional, Tuple 37 | 38 | from aqt import mw 39 | from aqt.main import AnkiQt 40 | 41 | from .config import config 42 | 43 | if TYPE_CHECKING: 44 | from anki.models import FieldDict, ModelManager, NotetypeDict, TemplateDict 45 | 46 | 47 | class CardTemplate(NamedTuple): 48 | name: str 49 | qfmt: str 50 | afmt: str 51 | 52 | 53 | class NoteType(NamedTuple): 54 | name: str 55 | fields: Tuple[str, ...] 56 | templates: Tuple[CardTemplate, ...] 57 | css: str 58 | 59 | 60 | _dictionary_card_template: CardTemplate = CardTemplate( 61 | name="Definition", 62 | qfmt="""\ 63 | Define: {{%s}} 64 | """ 65 | % config["local"]["dictionaryTermFieldName"], 66 | afmt="""\ 67 | {{FrontSide}} 68 | 69 |
    70 | 71 | {{%s}} 72 | """ 73 | % config["local"]["dictionaryDefinitionFieldName"], 74 | ) 75 | 76 | _dictionary_note_type = NoteType( 77 | name=config["local"]["dictionaryNoteTypeName"], 78 | fields=( 79 | config["local"]["dictionaryTermFieldName"], 80 | config["local"]["dictionaryDefinitionFieldName"], 81 | ), 82 | templates=(_dictionary_card_template,), 83 | css="""\ 84 | .card { 85 | font-family: arial; 86 | font-size: 20px; 87 | text-align: center; 88 | color: black; 89 | background-color: white; 90 | } 91 | """, 92 | ) 93 | 94 | # Anki API shims 95 | 96 | 97 | def models_new_field(model_manager: "ModelManager", name: str) -> "FieldDict": 98 | try: 99 | return model_manager.new_field(name) 100 | except AttributeError: 101 | return model_manager.newField(name) # type:ignore[attr-defined] 102 | 103 | 104 | def models_new_template(model_manager: "ModelManager", name: str) -> "TemplateDict": 105 | try: 106 | return model_manager.new_template(name) 107 | except AttributeError: 108 | return model_manager.newTemplate(name) # type:ignore[attr-defined] 109 | 110 | 111 | def models_by_name( 112 | model_manager: "ModelManager", name: str 113 | ) -> Optional["NotetypeDict"]: 114 | try: 115 | return model_manager.by_name(name) 116 | except AttributeError: 117 | return model_manager.byName(name) # type:ignore[attr-defined] 118 | 119 | 120 | # Note type creation 121 | 122 | 123 | def maybe_add_note_type(mw: AnkiQt, note_type: NoteType) -> bool: 124 | if mw.col is None: 125 | print("Collection not ready") 126 | return False 127 | 128 | model_manager = mw.col.models 129 | 130 | if models_by_name(model_manager=model_manager, name=note_type.name): 131 | # note type already exists (by name) 132 | return False 133 | 134 | anki_model = model_manager.new(note_type.name) 135 | 136 | # Add fields: 137 | for field_name in note_type.fields: 138 | field = models_new_field(model_manager=model_manager, name=field_name) 139 | model_manager.addField(anki_model, field) 140 | 141 | # Add card templates: 142 | for card_template in note_type.templates: 143 | template = models_new_template( 144 | model_manager=model_manager, name=card_template.name 145 | ) 146 | template["qfmt"] = card_template.qfmt 147 | template["afmt"] = card_template.afmt 148 | model_manager.addTemplate(anki_model, template) 149 | 150 | anki_model["css"] = note_type.css 151 | 152 | model_manager.add(anki_model) 153 | 154 | return True 155 | 156 | 157 | def maybe_create_template(): 158 | if mw is None: 159 | return 160 | 161 | if not config["local"]["dictionaryEnabled"]: 162 | return 163 | 164 | if maybe_add_note_type(mw, _dictionary_note_type): 165 | mw.reset() 166 | 167 | 168 | def initialize_template(): 169 | from aqt.gui_hooks import profile_did_open 170 | 171 | profile_did_open.append(maybe_create_template) 172 | -------------------------------------------------------------------------------- /src/popup_dictionary/web.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Pop-up Dictionary Add-on for Anki 4 | # 5 | # Copyright (C) 2018-2021 Aristotelis P. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License as 9 | # published by the Free Software Foundation, either version 3 of the 10 | # License, or (at your option) any later version, with the additions 11 | # listed at the end of the license file that accompanied this program. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # NOTE: This program is subject to certain additional terms pursuant to 22 | # Section 7 of the GNU Affero General Public License. You should have 23 | # received a copy of these additional terms immediately following the 24 | # terms and conditions of the GNU Affero General Public License that 25 | # accompanied this program. 26 | # 27 | # If not, please request a copy through one of the means of contact 28 | # listed here: . 29 | # 30 | # Any modifications to this file must keep this entire header intact. 31 | 32 | """ 33 | JS libs 34 | """ 35 | 36 | from aqt import mw 37 | 38 | from .consts import ADDON 39 | from .libaddon.platform import MODULE_ADDON 40 | 41 | 42 | # jQuery-migrate is required on 2.1.40+ to fix compatibility conflcits between 43 | # qtip2 and jQuery 3.x 44 | # cf. https://github.com/qTip2/qTip2/issues/797 45 | popup_integrator = f""" 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | """ 57 | 58 | 59 | def initialize_web(): 60 | # TODO: either fix on Anki#s end or use re.escape(os.path.sep) 61 | mw.addonManager.setWebExports(__name__, r"web.*") 62 | -------------------------------------------------------------------------------- /src/popup_dictionary/web/LICENSE_JQUERY_MIGRATE.txt: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery-migrate 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /src/popup_dictionary/web/LICENSE_QTIP2.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Craig Michael Thompson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/popup_dictionary/web/jquery.highlight.min.js: -------------------------------------------------------------------------------- 1 | // jQuery.highlight by Johann Burkard - MIT license (http://johannburkard.de) 2 | // Modifications are Copyright (C) 2018-2021 Aristotelis P. 3 | // Published under the same license 4 | jQuery.fn.highlight=function(c){function e(b,c){var d=0;if(3==b.nodeType){var a=b.data.toUpperCase().indexOf(c),a=a-(b.data.substr(0,a).toUpperCase().length-b.data.substr(0,a).length);if(0<=a){d=document.createElement("span");d.className="pdict-highlight";a=b.splitText(a);a.splitText(c.length);var f=a.cloneNode(!0);d.appendChild(f);a.parentNode.replaceChild(d,a);d=1}}else if(1==b.nodeType&&b.childNodes&&!/(script|style)/i.test(b.tagName))for(a=0;a { 7 | otherQtipElm.classList.add("pdict-disabled"); 8 | }); 9 | focusedElement.classList.remove("pdict-disabled"); 10 | } 11 | 12 | 13 | function createTooltip(element) { 14 | // Creates tooltip on specified DOM element, sets up mouse click events 15 | // and child tooltips, returns tooltip API object 16 | 17 | // create qtip on Anki qa div and assign its api object to 'tooltip' 18 | var tooltip = $(element) 19 | .qtip({ 20 | content: { 21 | text: "Loading...", 22 | }, 23 | prerender: true, // need to prerender for child tooltips to work properly 24 | // draw on mouse position, but don't update position on mousemove 25 | position: { 26 | target: "mouse", 27 | viewport: $(document), // constrain to window 28 | adjust: { 29 | mouse: false, // don't follow mouse 30 | method: "flip", // adjust to viewport by flipping tip if necessary 31 | scroll: false, // buggy, disable 32 | }, 33 | }, 34 | // apply predefined style 35 | style: { 36 | classes: "qtip-bootstrap pdict-popup", 37 | }, 38 | // don't set up any hide event triggers, do it manually instead 39 | hide: false, 40 | // wait until called upon 41 | show: false, 42 | events: { 43 | hide: function (event, api) { 44 | // hide next nested tooltip on hide 45 | var ttID = api.get("id"); 46 | var ttIDnext = "#qtip-" + (ttID + 1); 47 | $(ttIDnext).qtip("hide"); 48 | }, 49 | }, 50 | }) 51 | .qtip("api"); 52 | 53 | const qtipElm = tooltip.tooltip[0]; 54 | const footerElm = document.createElement("div"); 55 | const leftFooterHTML = `Pop-up Dictionary v${_pDictVersion} by Glutanimate`; 56 | const rightFooterHTML = `♥ Support my work`; 57 | footerElm.innerHTML = ``; 58 | footerElm.classList.add("pdict-footer"); 59 | qtipElm.appendChild(footerElm); 60 | 61 | $(qtipElm).mouseenter(function() { 62 | focusTooltip(qtipElm); 63 | }) 64 | 65 | $(qtipElm).click( function () { 66 | qtipElm.classList.remove("pdict-disabled"); 67 | }); 68 | 69 | // Custom double click event handler that works across 70 | // element boundaries → support for dblclick-holding and 71 | // then releasing over different DOM element (e.g. boldened text) 72 | 73 | var clicks = 0, 74 | delay = 500; 75 | 76 | $(element).on("mousedown", function (event) { 77 | clicks++; 78 | 79 | setTimeout(function () { 80 | clicks = 0; 81 | }, delay); 82 | 83 | if (clicks === 2) { 84 | event.stopImmediatePropagation(); 85 | $(document).one("mouseup", function (event) { 86 | showTooltip(event, tooltip, element); 87 | clicks = 0; 88 | return; 89 | }); 90 | } else { 91 | tooltip.hide(); 92 | } 93 | }); 94 | 95 | return tooltip; 96 | } 97 | 98 | getSelected = function () { 99 | return ( 100 | (window.getSelection && window.getSelection()) || 101 | (document.selection && document.selection.createRange()) 102 | ); 103 | }; 104 | 105 | invokeTooltipAtSelectedElm = function () { 106 | var selection = getSelected(); 107 | var selElm = selection.getRangeAt(0).startContainer.parentNode; 108 | var ttBoundElm = $(selElm).closest(".qtip-content"); 109 | if (typeof ttBoundElm[0] === "undefined") { 110 | ttBoundElm = document.getElementById("qa"); 111 | var tooltip = qaTooltip; 112 | } else { 113 | var tooltip = ttBoundElm.qtip("api"); 114 | } 115 | showTooltip(event, tooltip, ttBoundElm); 116 | }; 117 | 118 | // Look up selected text and show result in provided tooltip 119 | showTooltip = function (event, tooltip, element) { 120 | /* event: event that triggered function call 121 | tooltip: qtip api object of tooltip to use for showing results 122 | element: element that tooltip is bound to */ 123 | 124 | // Prevent immediately hiding invoked tooltip 125 | if (typeof event !== "undefined") { 126 | event.stopPropagation(); 127 | } 128 | 129 | // Hide existing tooltip at current nesting level, 130 | // this propagates to all child tooltips through the qtip 131 | // hide event 132 | tooltip.hide(); 133 | 134 | // Get selection 135 | var selection = getSelected(); 136 | term = selection.toString().trim(); 137 | 138 | // Return if selection empty or too short 139 | if (term.length < 3) { 140 | return; 141 | } 142 | 143 | // Exclude NID of clicked-on result entry 144 | if (element.id != "#qa") { 145 | var selElm = selection.getRangeAt(0).startContainer.parentNode; 146 | var resElm = $(selElm).closest(".pdict-res")[0]; 147 | if (resElm && "nid" in resElm.dataset) { 148 | var selNID = resElm.dataset.nid; 149 | console.log("Ignore current NID: " + selNID); 150 | } else { 151 | var selNID = ""; 152 | } 153 | } 154 | 155 | // Set tooltip contents through pyrun bridge. Need to use a callback 156 | // due to async execution of JS and Python in Anki 2.1 157 | pycmd( 158 | "popupDictionaryLookup:" + JSON.stringify([term, selNID]), 159 | function (text) { 160 | return onCallback(text); 161 | } 162 | ); 163 | 164 | function onCallback(text) { 165 | // Silent exit if no results returned and ALWAYS_SHOW in Python False 166 | if (!text) { 167 | return; 168 | } 169 | 170 | // Determine current qtip ID and ID of potential child tooltip 171 | var ttID = tooltip.get("id"); 172 | var domID = "#qtip-" + ttID; 173 | var newttID = ttID + 1; 174 | var newdomID = "#qtip-" + newttID; 175 | console.log("Current tt domID: " + domID); 176 | console.log("New tt domID: " + newdomID); 177 | 178 | // Set tooltip content and show it 179 | tooltip.set("content.text", text); 180 | console.log("Set text"); 181 | focusTooltip(tooltip.tooltip[0]); 182 | tooltip.show(); 183 | console.log("Showed tooltip"); 184 | // Need to scroll to top if tooltip has been drawn before 185 | $(domID + "-content").scrollTop(0); 186 | 187 | // Highlight search term 188 | $(domID).highlight(term); 189 | $(".pdict-dict").removeHighlight(); // don't highlight term in dictionary elm 190 | 191 | // Nested tooltips 192 | // create child tooltip for content on current tooltip 193 | if ($(newdomID).length == 0) { 194 | // Bind new qtip instance to content of current tooltip 195 | console.log( 196 | "Create new tooltip on ID: " + 197 | domID + 198 | ". Tooltip will have ID: " + 199 | newdomID 200 | ); 201 | createTooltip(domID + "-content"); 202 | } else { 203 | // Reuse existing qtip instance 204 | console.log("Found existing tooltip with ID: " + newdomID); 205 | } 206 | } 207 | }; 208 | 209 | // set up bindings to close qtip, unless mouseup is registered on qtip itself 210 | $(document).on("mouseup", function (event) { 211 | if ($(event.target).closest("#qa, .qtip").length > 0) { 212 | return; 213 | } 214 | event.stopImmediatePropagation(); 215 | qaTooltip.hide(); 216 | }); 217 | 218 | qaTooltip = createTooltip("#qa"); 219 | }); 220 | --------------------------------------------------------------------------------