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 |
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 | "
{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.
".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 = """
"""
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 |
--------------------------------------------------------------------------------