├── .gitignore ├── docs ├── demo.gif ├── options_1.png ├── options_2.png └── Symbol_List.md ├── tests ├── import_bad_duplicates.csv ├── import_bad_missingvals.csv ├── import_good_data.csv ├── export_good_data_reference.csv └── Test_Cases.txt ├── src ├── __init__.py ├── get_version.py ├── browser_replacer.py ├── default_symbols.py ├── replacer_pre-2.1.41.js ├── insert_symbols.py ├── replacer.js ├── symbol_manager.py ├── Ui_SymbolWindow_5.py ├── Ui_SymbolWindow_6.py ├── Ui_SymbolWindow_4.py ├── Ui_SymbolWindow.ui └── symbol_window.py ├── manifest.json ├── __init__.py ├── bin ├── link.sh └── build_for_release.sh ├── README.md └── gen_sym_doc.py /.gitignore: -------------------------------------------------------------------------------- 1 | src/__pycache__/ 2 | src/.idea/ 3 | build/ 4 | 5 | *.swp 6 | *.pyc 7 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jefdongus/insert-symbols-anki-addon/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /docs/options_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jefdongus/insert-symbols-anki-addon/HEAD/docs/options_1.png -------------------------------------------------------------------------------- /docs/options_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jefdongus/insert-symbols-anki-addon/HEAD/docs/options_2.png -------------------------------------------------------------------------------- /tests/import_bad_duplicates.csv: -------------------------------------------------------------------------------- 1 | a,val_1 2 | b,val_2 3 | 4 | b,val_3 5 | 6 | bb,val_4 7 | c,val_5 8 | 9 | c,val_6 10 | d,val_7 11 | 12 | duplicates,should-be-b-and-c 13 | -------------------------------------------------------------------------------- /tests/import_bad_missingvals.csv: -------------------------------------------------------------------------------- 1 | :key1:,val_1 2 | :key2:,val_2 3 | 4 | val_3 5 | :key4:,val_4 6 | :key5:,val_5 7 | 8 | :key6:, 9 | :key7:,val_7 10 | :key8:, 11 | 12 | errors,should-be-lines-4-8-and-10 13 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is not part of the source code. For ease of development, this repo 3 | can be placed directly inside the Anki addons directory, and the latest source 4 | code will be loaded whenever Anki is opened. 5 | """ 6 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Insert Symbols As You Type", 3 | "package": "2040501954", 4 | "ankiweb_id": "2040501954", 5 | "author": "jefdongus", 6 | "version": "1.4.2", 7 | "homepage": "https://github.com/jefdongus/insert-symbols-anki-addon", 8 | "conflicts": [ 9 | "insert_symbols" 10 | ] 11 | } -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is not part of the source code. For ease of development, this repo 3 | can be placed directly inside the Anki addons directory, and the latest source 4 | code will be loaded whenever Anki is opened. 5 | 6 | The __init__.py included in the final .ankiaddon file is generated by 7 | build_for_release.sh. 8 | """ 9 | 10 | from .src import insert_symbols 11 | -------------------------------------------------------------------------------- /tests/import_good_data.csv: -------------------------------------------------------------------------------- 1 | """_""",*_* 2 | 'hi',aaaaaaa 3 | ::bold::,bold text 4 | ::macro::,
{{c1::
}}
5 | ::sup::,text 6 | :hiri:,ぁ 7 | :o:,Ω 8 | :star:,★ 9 | :tab:, 10 | HI,:aaa: 11 | _hi_,"hi, hi" 12 | d,multi word values 13 | fix,d 14 | hi,< > 15 | pre,c 16 | prefix,asdfjkl; 17 | s1,spacing-1 18 | s2,spacing-2 19 | same,text 20 | suffix,ddd 21 | value,text 22 | -------------------------------------------------------------------------------- /tests/export_good_data_reference.csv: -------------------------------------------------------------------------------- 1 | """_""",*_* 2 | 'hi',aaaaaaa 3 | ::bold::,bold text 4 | ::macro::,
{{c1::
}}
5 | ::sup::,text 6 | :hiri:,ぁ 7 | :o:,Ω 8 | :star:,★ 9 | :tab:, 10 | HI,:aaa: 11 | _hi_,"hi, hi" 12 | d,multi word values 13 | fix,d 14 | hi,< > 15 | pre,c 16 | prefix,asdfjkl; 17 | s1,spacing-1 18 | s2,spacing-2 19 | same,text 20 | suffix,ddd 21 | value,text 22 | -------------------------------------------------------------------------------- /bin/link.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DIR="$(cd "$(dirname "$0")/.." && pwd -P)" 3 | addon_name="SymbolsAsYouTypeDev" 4 | customdir='' 5 | 6 | if [[ -d "$customdir" ]]; then 7 | target="$customdir/$addon_name" 8 | 9 | elif [[ -d "$HOME/.local/share/AnkiDev/addons21" ]]; then 10 | target="$HOME/.local/share/AnkiDev/addons21/$addon_name" 11 | 12 | elif [[ $(uname) = 'Darwin' ]]; then 13 | target="$HOME/Library/Application Support/Anki2/addons21/$addon_name" 14 | 15 | elif [[ $(uname) = 'Linux' ]]; then 16 | target="$HOME/.local/share/Anki2/addons21/$addon_name" 17 | 18 | else 19 | echo 'Unknown platform' 20 | exit -1 21 | fi 22 | 23 | if [[ "$1" =~ ^-?d$ ]]; then 24 | if [[ ! -h "$target" ]]; then 25 | echo 'Directory was not linked' 26 | else 27 | rm "$target" 28 | fi 29 | 30 | elif [[ "$1" =~ ^-?c$ ]]; then 31 | if [[ ! -h "$target" ]]; then 32 | ln -s "$DIR" "$target" 33 | else 34 | echo 'Directory was already linked.' 35 | fi 36 | 37 | else 38 | echo 'Unknown command' 39 | exit -2 40 | fi 41 | -------------------------------------------------------------------------------- /bin/build_for_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ADDON_NAME=insert_symbols 4 | 5 | ROOT_FOLDER="$(cd "$(dirname "$0")/.." && pwd -P)" 6 | SRC_FOLDER="$ROOT_FOLDER/src" 7 | BUILD_FOLDER="$ROOT_FOLDER/build" 8 | 9 | EXCLUDES=".|..|*.ui|__pycache__" 10 | 11 | if [ -d $BUILD_FOLDER ]; then 12 | rm -rf $BUILD_FOLDER/* 13 | else 14 | mkdir $BUILD_FOLDER 15 | fi 16 | 17 | mkdir -p "$BUILD_FOLDER/Anki20/$ADDON_NAME" 18 | mkdir "$BUILD_FOLDER/Anki21" 19 | 20 | shopt -s nullglob 21 | shopt -s extglob 22 | 23 | cd "$SRC_FOLDER" 24 | 25 | for i in !($EXCLUDES); do 26 | cp $i "$BUILD_FOLDER/Anki20/$ADDON_NAME/$i" 27 | cp $i "$BUILD_FOLDER/Anki21/$i" 28 | done 29 | 30 | #cp Ui_SymbolWindow_4.py "$BUILD_FOLDER/Anki20/$ADDON_NAME/Ui_SymbolWindow.py" 31 | #cp Ui_SymbolWindow.py "$BUILD_FOLDER/Anki21/Ui_SymbolWindow.py" 32 | cp "$ROOT_FOLDER/manifest.json" "$BUILD_FOLDER/Anki21/manifest.json" 33 | 34 | py_fname=$(echo $ADDON_NAME | tr '_' ' ' | sed 's/\b[a-z]/\u&/g') 35 | 36 | echo "import $ADDON_NAME.$ADDON_NAME" > "$BUILD_FOLDER/Anki20/$py_fname.py" 37 | echo "from . import $ADDON_NAME" > "$BUILD_FOLDER/Anki21/__init__.py" 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symbols As You Type 2 | 3 | This Anki plugin lets you quickly insert Unicode symbols into cards by typing a predefined key sequence. 4 | 5 | ![demo](docs/demo.gif) 6 | 7 | ### Getting Started: 8 | ---- 9 | 1. Install the plugin from [AnkiWeb](https://ankiweb.net/shared/info/2040501954) (plugin ID: 2040501954). 10 | 2. For a list of available symbols, open the Options window (see below) or refer to the [list of default symbols](https://github.com/jefdongus/insert-symbols-anki-addon/wiki/List-of-Default-Symbols). 11 | 12 | 13 | ### Features: 14 | ---- 15 | - Large variety of symbols including Greek letters, mathematical characters, currency, and more. 16 | - Symbol list is fully customizable and is synced to AnkiWeb per profile. 17 | - Compatible with Anki 2.0 and 2.1 18 | 19 | 20 | ### Adding Your Own Symbols: 21 | ---- 22 | The symbol list is fully customizable, and you can add, import, and export your own list of symbols through the Options window. To open the Options window, click on the `"Insert Symbol Options..."` item in the `Tools` menu. If the plugin was just installed, you may need to restart Anki for the menu to show up: 23 | 24 | ![symbol-options-menuitem](docs/options_1.png) 25 | 26 | This should bring up the following window: 27 | 28 | ![symbol-options-popup](docs/options_2.png) 29 | 30 | ### License: 31 | ---- 32 | This plugin is licensed under the GNU Affero General Public License version 3, same as Anki. 33 | -------------------------------------------------------------------------------- /gen_sym_doc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This script generates a Markdown file for the Github wiki symbol list. 5 | """ 6 | 7 | from src.default_symbols import _SYMBOL_DICT 8 | 9 | output_fname = 'docs/Symbol_List.md' 10 | 11 | def make_TOC(): 12 | output = '### Sections\n' 13 | counter = 1 14 | 15 | for title in _SYMBOL_DICT: 16 | link_name = title.replace(' ', '-') \ 17 | .replace('(', '') \ 18 | .replace(')', '') \ 19 | .lower() 20 | output += '%d. [%s](#%s)\n' % (counter, title, link_name) 21 | counter += 1 22 | 23 | output += ('\nFor instructions on how to add your own symbols, ' 24 | 'please see the [[FAQ|FAQ]].\n\n') 25 | return output 26 | 27 | def make_section(title, symbol_list): 28 | output = ( 29 | '### %s\n\n' 30 | '| Symbol | Key |\n' 31 | '| --- | --- |\n' 32 | ) % title 33 | 34 | for key, val in symbol_list: 35 | output += ('| %s | `%s` |\n' % (val, key)) 36 | 37 | output += '\n[Back to Top](#sections)\n' 38 | return output 39 | 40 | # Write output 41 | with open(output_fname, 'w', 42 | encoding='ascii', 43 | errors='xmlcharrefreplace') as out_file: 44 | 45 | out_file.write('\n\n') 46 | out_file.write(make_TOC()) 47 | for title, symbol_list in _SYMBOL_DICT.items(): 48 | out_file.write(make_section(title, symbol_list)) 49 | -------------------------------------------------------------------------------- /tests/Test_Cases.txt: -------------------------------------------------------------------------------- 1 | Below are a checklist of tests to perform: 2 | 3 | Text Areas: 4 | ------------------------------ 5 | 1) Test that symbols can be added to the end of a block of text. 6 | 2) Test that symbols can be added to the middle of a block of text. 7 | 3) Test that replacement occurs for arrows and colon-delimited keys as soon as the last character is typed. 8 | 4) Test that for other characters, replacement only occurs if the character before the key is a whitespace AND that a whitespace character is pressed. 9 | 10 | 11 | Options Window UI: 12 | ------------------------------ 13 | 1) Test that typing keys auto-scrolls the ScrollView. 14 | 2) Test that keys with spaces are NOT valid. 15 | 3) Test that values with spaces are valid. 16 | 4) Test that valid K-V pairs can be added, and that changes are seen in the textarea. 17 | 5) Test that leading/trailing whitespace are trimmed. 18 | 6) Test that clicking on a ScrollView row updates the key/value fields. 19 | 7) Test that editing the value of an existing K-V pair changes the button to "Replace". 20 | 8) Test that editing the key of an existing K-V pair changes the button to "Add". 21 | 9) Test that if a key is invalid or if a value does not exist, the Add/Replace button is grayed out. 22 | 10) Test that an existing K-V pair can be updated, and that changes are seen in the textarea. 23 | 11) Test that an existing K-V pair can be deleted, and that changes are seen in the textarea. 24 | 25 | 26 | Import / Export: 27 | ------------------------------ 28 | 1) Test that importing "import_good_data.txt" works. 29 | 2) Test that importing "import_bad_missingvals.txt" and "import_bad_duplicates.txt" fails. 30 | 3) Load "import_good_data.txt" and check that the exported file matches "export_good_data_reference.txt". 31 | 4) Test that resetting the symbol list works. 32 | 5) Test that symbols are saved to database after closing Anki. 33 | -------------------------------------------------------------------------------- /src/get_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parses Anki's version. Uncommment and run this directly to evaluate test cases. 3 | """ 4 | 5 | import re 6 | import pkgutil 7 | 8 | import anki 9 | 10 | """ Constants """ 11 | 12 | ANKI_VER_PRE_2_1_0 = 0 13 | ANKI_VER_PRE_2_1_41 = 1 14 | ANKI_VER_PRE_23_10 = 2 15 | ANKI_VER_LATEST = 3 16 | 17 | PYQT_VER_4 = 4 18 | PYQT_VER_5 = 5 19 | PYQT_VER_LATEST = 6 20 | 21 | """ Parse Anki Version """ 22 | 23 | def _parse_anki_version_new(): 24 | """ 25 | pointVersion() is added in Anki 2.1.20 so if this gets called, by default 26 | Anki is 2.1.20 or higher. 27 | """ 28 | point_version = anki.utils.pointVersion() 29 | if point_version < 41: 30 | return ANKI_VER_PRE_2_1_41 31 | elif point_version < 231000: 32 | return ANKI_VER_PRE_23_10 33 | else: 34 | return ANKI_VER_LATEST 35 | 36 | def _parse_anki_version_old(): 37 | """ 38 | This will only get called if Anki is between 2.0 and 2.1.19 39 | """ 40 | try: 41 | version = anki.version 42 | v = tuple(map(int, re.match("(\d+)\.(\d+)\.(\d+)", version).groups())) 43 | 44 | if v[0] < 2: 45 | return ANKI_VER_PRE_2_1_0 46 | elif v[0] == 2 and v[1] < 1: 47 | return ANKI_VER_PRE_2_1_0 48 | else: 49 | return ANKI_VER_PRE_2_1_41 50 | except: 51 | return ANKI_VER_PRE_2_1_41 52 | 53 | def get_anki_version(): 54 | has_point_version = getattr(anki.utils, 'pointVersion', None) 55 | if has_point_version: 56 | return _parse_anki_version_new() 57 | else: 58 | return _parse_anki_version(version) 59 | 60 | 61 | """ Obtain PyQt Version """ 62 | 63 | def get_pyqt_version(): 64 | if pkgutil.find_loader('PyQt4'): 65 | return PYQT_VER_4 66 | elif pkgutil.find_loader('PyQt5'): 67 | return PYQT_VER_5 68 | else: 69 | return PYQT_VER_LATEST 70 | 71 | 72 | """ Test Cases """ 73 | 74 | # def _test_version_parsing(version, expected): 75 | # if _parse_anki_version(version) == expected: 76 | # result = 'Passed' 77 | # else: 78 | # result = 'FAILED' 79 | # print("%s '%s'" % (result, version)) 80 | 81 | # _test_version_parsing('1.9.9', ANKI_VER_PRE_2_1_0) 82 | # _test_version_parsing('2.0.31', ANKI_VER_PRE_2_1_0) 83 | # _test_version_parsing('2.1.0', ANKI_VER_PRE_2_1_41) 84 | # _test_version_parsing('2.1.4', ANKI_VER_PRE_2_1_41) 85 | # _test_version_parsing('2.1.40', ANKI_VER_PRE_2_1_41) 86 | # _test_version_parsing('2.1.40-beta', ANKI_VER_PRE_2_1_41) 87 | # _test_version_parsing('2.1.41', ANKI_VER_LATEST) 88 | # _test_version_parsing('2.1.41-beta', ANKI_VER_LATEST) 89 | # _test_version_parsing('2.2.0', ANKI_VER_LATEST) 90 | # _test_version_parsing('3.0.0', ANKI_VER_LATEST) 91 | # _test_version_parsing('1.0', ANKI_VER_LATEST) 92 | # _test_version_parsing('12345', ANKI_VER_LATEST) 93 | # _test_version_parsing('abcde', ANKI_VER_LATEST) 94 | -------------------------------------------------------------------------------- /src/browser_replacer.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains BrowserReplacer, which replicates the function of 3 | replacer.js for the Browser search bar. 4 | """ 5 | 6 | 7 | from aqt.qt import * 8 | 9 | class BrowserReplacer(object): 10 | 11 | def __init__(self, match_list): 12 | self._match_list = match_list 13 | 14 | def on_browser_init(self, browser): 15 | """ Set up hooks to the search box. """ 16 | self._browser = browser 17 | 18 | search_box = self.get_search_box() 19 | if search_box: 20 | search_box.textEdited.connect(self.on_text_edited) 21 | search_box.returnPressed.connect(self.on_return_pressed) 22 | 23 | def update_list(self, match_list): 24 | self._match_list = match_list 25 | 26 | def get_search_box(self): 27 | """ Underlying QLineEdit object can get deleted. """ 28 | searchEdit = self._browser.form.searchEdit 29 | if not searchEdit: 30 | return None 31 | else: 32 | return searchEdit.lineEdit() 33 | 34 | 35 | """ Event Handling """ 36 | 37 | def on_return_pressed(self): 38 | search_box = self.get_search_box() 39 | if search_box: 40 | current_text = search_box.text() 41 | self._check_for_replacement(current_text, True) 42 | 43 | def on_text_edited(self, current_text): 44 | self._check_for_replacement(current_text, False) 45 | 46 | 47 | """ Matching Functions """ 48 | 49 | def _is_whitespace(self, string, i): 50 | """ Checks whether string[i] is whitespace. """ 51 | if i < 0 or i >= len(string): 52 | return False 53 | return string[i].isspace() 54 | 55 | def _check_for_replacement(self, text, is_enter_pressed): 56 | """ 57 | Port of code in replacer.js. The major difference is that this function 58 | is triggered after a whitespace character is inserted, whereas it isn't 59 | in the Javascript code. 60 | """ 61 | search_box = self.get_search_box() 62 | if not text or not search_box: 63 | return 64 | 65 | cursor_pos = search_box.cursorPosition() 66 | if is_enter_pressed: 67 | is_whitespace_pressed = False 68 | else: 69 | is_whitespace_pressed = self._is_whitespace(text, cursor_pos-1) 70 | 71 | match = self._matches_keyword(text, cursor_pos, 72 | is_whitespace_pressed, is_enter_pressed) 73 | if match: 74 | self._perform_replacement(text, match[0], match[1], match[2]) 75 | 76 | def _matches_keyword(self, text, cursor_pos, is_whitespace_pressed, 77 | is_enter_pressed): 78 | """ Port of code in replacer.js. """ 79 | for item in self._match_list: 80 | key = item['key'] 81 | val = item['val'] 82 | trigger_on_space = (item['f'] == 0) 83 | 84 | if trigger_on_space: 85 | # Make sure to 1) only trigger after whitespace and 2) ignore 86 | # any trailing spaces: 87 | if is_enter_pressed: 88 | end_index = cursor_pos 89 | elif is_whitespace_pressed: 90 | end_index = cursor_pos - 1 91 | else: 92 | continue 93 | else: 94 | end_index = cursor_pos 95 | 96 | start_index = max(end_index - len(key), 0) 97 | if text[start_index : end_index] == key: 98 | if trigger_on_space: 99 | # Check that character preceding key is whitespace: 100 | prior_idx = start_index - 1 101 | if prior_idx >= 0 and not self._is_whitespace(text, prior_idx): 102 | continue 103 | return (val, start_index, end_index) 104 | return None 105 | 106 | def _perform_replacement(self, old_text, value, start_idx, end_idx): 107 | """ Port of code in replacer.js. """ 108 | new_text = old_text[:start_idx] + value + old_text[end_idx:] 109 | self.get_search_box().setText(new_text) 110 | self.get_search_box().setCursorPosition(start_idx + len(value)) 111 | 112 | -------------------------------------------------------------------------------- /docs/Symbol_List.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Sections 4 | 1. [Arrows](#arrows) 5 | 2. [Typography](#typography) 6 | 3. [Math (General)](#math-general) 7 | 4. [Math (Binary Operators)](#math-binary-operators) 8 | 5. [Math (Relational)](#math-relational) 9 | 6. [Math (Sets)](#math-sets) 10 | 7. [Math (Logical)](#math-logical) 11 | 8. [Math (Calculus)](#math-calculus) 12 | 9. [Fractions](#fractions) 13 | 10. [Greek Symbols (Lowercase)](#greek-symbols-lowercase) 14 | 11. [Greek Symbols (Uppercase)](#greek-symbols-uppercase) 15 | 12. [Currency](#currency) 16 | 17 | For instructions on how to add your own symbols, please see the [[FAQ|FAQ]]. 18 | 19 | ### Arrows 20 | 21 | | Symbol | Key | 22 | | --- | --- | 23 | | → | `->` | 24 | | ⇒ | `=>` | 25 | | ← | `<-` | 26 | | ⇐ | `<=` | 27 | | ↑ | `:N:` | 28 | | ⇑ | `:N2:` | 29 | | ↓ | `:S:` | 30 | | ⇓ | `:S2:` | 31 | | → | `:E:` | 32 | | ⇒ | `:E2:` | 33 | | ← | `:W:` | 34 | | ⇐ | `:W2:` | 35 | 36 | [Back to Top](#sections) 37 | ### Typography 38 | 39 | | Symbol | Key | 40 | | --- | --- | 41 | | ‒ | `--` | 42 | | — | `---` | 43 | | † | `:dagger:` | 44 | | ‡ | `:ddagger:` | 45 | | § | `:section:` | 46 | | ¶ | `:paragraph:` | 47 | 48 | [Back to Top](#sections) 49 | ### Math (General) 50 | 51 | | Symbol | Key | 52 | | --- | --- | 53 | | ∞ | `:infty:` | 54 | | ° | `:deg:` | 55 | | ‰ | `:permil:` | 56 | | √ | `:sqrt:` | 57 | | ∛ | `:cubert:` | 58 | | ∜ | `:4thrt:` | 59 | | ∠ | `:angle:` | 60 | | ℏ | `:hbar:` | 61 | 62 | [Back to Top](#sections) 63 | ### Math (Binary Operators) 64 | 65 | | Symbol | Key | 66 | | --- | --- | 67 | | ± | `:pm:` | 68 | | ∓ | `:mp:` | 69 | | · | `:dot:` | 70 | | × | `:times:` | 71 | | ÷ | `:div:` | 72 | 73 | [Back to Top](#sections) 74 | ### Math (Relational) 75 | 76 | | Symbol | Key | 77 | | --- | --- | 78 | | ≈ | `:approx:` | 79 | | ≡ | `:equiv:` | 80 | | ∝ | `:propto:` | 81 | | ≠ | `:neq:` | 82 | | ≥ | `:geq:` | 83 | | ≤ | `:leq:` | 84 | | ≫ | `:>>:` | 85 | | ≪ | `:<<:` | 86 | 87 | [Back to Top](#sections) 88 | ### Math (Sets) 89 | 90 | | Symbol | Key | 91 | | --- | --- | 92 | | ⊂ | `:subset:` | 93 | | ⊆ | `:subseteq:` | 94 | | ⊃ | `:supset:` | 95 | | ⊇ | `:supseteq:` | 96 | | ∈ | `:in:` | 97 | | ∋ | `:ni:` | 98 | | ∩ | `:cap:` | 99 | | ∪ | `:cup:` | 100 | | ∅ | `:emptyset:` | 101 | 102 | [Back to Top](#sections) 103 | ### Math (Logical) 104 | 105 | | Symbol | Key | 106 | | --- | --- | 107 | | ¬ | `:neg:` | 108 | | ∨ | `:vee:` | 109 | | ∧ | `:wedge:` | 110 | | ∀ | `:forall:` | 111 | | ∃ | `:exists:` | 112 | | ∴ | `:therefore:` | 113 | 114 | [Back to Top](#sections) 115 | ### Math (Calculus) 116 | 117 | | Symbol | Key | 118 | | --- | --- | 119 | | ∇ | `:nabla:` | 120 | | ∂ | `:partial:` | 121 | | ∫ | `:integral:` | 122 | 123 | [Back to Top](#sections) 124 | ### Fractions 125 | 126 | | Symbol | Key | 127 | | --- | --- | 128 | | ½ | `:1/2:` | 129 | | ⅓ | `:1/3:` | 130 | | ⅔ | `:2/3:` | 131 | | ¼ | `:1/4:` | 132 | | ¾ | `:3/4:` | 133 | | ⅕ | `:1/5:` | 134 | | ⅖ | `:2/5:` | 135 | | ⅗ | `:3/5:` | 136 | | ⅘ | `:4/5:` | 137 | | ⅙ | `:1/6:` | 138 | | ⅚ | `:5/6:` | 139 | | ⅐ | `:1/7:` | 140 | | ⅛ | `:1/8:` | 141 | | ⅜ | `:3/8:` | 142 | | ⅝ | `:5/8:` | 143 | | ⅞ | `:7/8:` | 144 | | ⅑ | `:1/9:` | 145 | | ⅒ | `:1/10:` | 146 | 147 | [Back to Top](#sections) 148 | ### Greek Symbols (Lowercase) 149 | 150 | | Symbol | Key | 151 | | --- | --- | 152 | | α | `:alpha:` | 153 | | β | `:beta:` | 154 | | γ | `:gamma:` | 155 | | δ | `:delta:` | 156 | | ε | `:epsilon:` | 157 | | ζ | `:zeta:` | 158 | | η | `:eta:` | 159 | | θ | `:theta:` | 160 | | ι | `:iota:` | 161 | | κ | `:kappa:` | 162 | | λ | `:lambda:` | 163 | | μ | `:mu:` | 164 | | ν | `:nu:` | 165 | | ξ | `:xi:` | 166 | | ο | `:omicron:` | 167 | | π | `:pi:` | 168 | | ρ | `:rho:` | 169 | | σ | `:sigma:` | 170 | | τ | `:tau:` | 171 | | υ | `:upsilon:` | 172 | | φ | `:phi:` | 173 | | χ | `:chi:` | 174 | | ψ | `:psi:` | 175 | | ω | `:omega:` | 176 | 177 | [Back to Top](#sections) 178 | ### Greek Symbols (Uppercase) 179 | 180 | | Symbol | Key | 181 | | --- | --- | 182 | | Α | `:Alpha:` | 183 | | Β | `:Beta:` | 184 | | Γ | `:Gamma:` | 185 | | Δ | `:Delta:` | 186 | | Ε | `:Epsilon:` | 187 | | Ζ | `:Zeta:` | 188 | | Η | `:Eta:` | 189 | | Θ | `:Theta:` | 190 | | Ι | `:Iota:` | 191 | | Κ | `:Kappa:` | 192 | | Λ | `:Lambda:` | 193 | | Μ | `:Mu:` | 194 | | Ν | `:Nu:` | 195 | | Ξ | `:Xi:` | 196 | | Ο | `:Omicron:` | 197 | | Π | `:Pi:` | 198 | | Ρ | `:Rho:` | 199 | | Σ | `:Sigma:` | 200 | | Τ | `:Tau:` | 201 | | Υ | `:Upsilon:` | 202 | | Φ | `:Phi:` | 203 | | Χ | `:Chi:` | 204 | | Ψ | `:Psi:` | 205 | | Ω | `:Omega:` | 206 | 207 | [Back to Top](#sections) 208 | ### Currency 209 | 210 | | Symbol | Key | 211 | | --- | --- | 212 | | ¢ | `:cent:` | 213 | | £ | `:pound:` | 214 | | € | `:euro:` | 215 | | ₤ | `:lira:` | 216 | | ₱ | `:peso:` | 217 | | ₽ | `:ruble:` | 218 | | ₹ | `:rupee:` | 219 | | ₩ | `:won:` | 220 | | ¥ | `:yen:` | 221 | | ¥ | `:yuan:` | 222 | 223 | [Back to Top](#sections) 224 | -------------------------------------------------------------------------------- /src/default_symbols.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains the default symbol list as well as a list of special symbols 3 | which should behave like colon-delimited symbols. 4 | """ 5 | import itertools 6 | from collections import OrderedDict 7 | 8 | SPECIAL_KEYS = [ 9 | '->', 10 | '<-', 11 | '=>', 12 | '<=', 13 | ] 14 | 15 | """ Symbols Key-Value Pair Definitions """ 16 | 17 | _ARROWS = [ 18 | ('->', u'\u2192'), 19 | ('=>', u'\u21D2'), 20 | ('<-', u'\u2190'), 21 | ('<=', u'\u21D0'), 22 | (':N:', u'\u2191'), 23 | (':N2:', u'\u21D1'), 24 | (':S:', u'\u2193'), 25 | (':S2:', u'\u21D3'), 26 | (':E:', u'\u2192'), 27 | (':E2:', u'\u21D2'), 28 | (':W:', u'\u2190'), 29 | (':W2:', u'\u21D0'), 30 | ] 31 | 32 | _TYPOGRAPHY = [ 33 | ('--', u'\u2012'), 34 | ('---', u'\u2014'), 35 | (':dagger:', u'\u2020'), 36 | (':ddagger:', u'\u2021'), 37 | (':section:', u'\u00A7'), 38 | (':paragraph:', u'\u00B6'), 39 | ] 40 | 41 | _MATH_GEN = [ 42 | (':infty:', u'\u221E'), 43 | (':deg:', u'\u00B0'), 44 | (':permil:', u'\u2030'), 45 | (':sqrt:', u'\u221A'), 46 | (':cubert:', u'\u221B'), 47 | (':4thrt:', u'\u221C'), 48 | (':angle:', u'\u2220'), 49 | (':hbar:', u'\u210F'), 50 | ] 51 | 52 | _MATH_BINARY = [ 53 | (':pm:', u'\u00B1'), 54 | (':mp:', u'\u2213'), 55 | (':dot:', u'\u00B7'), 56 | (':times:', u'\u00D7'), 57 | (':div:', u'\u00F7'), 58 | ] 59 | 60 | _MATH_RELATION = [ 61 | (':approx:', u'\u2248'), 62 | (':equiv:', u'\u2261'), 63 | (':propto:', u'\u221D'), 64 | (':neq:', u'\u2260'), 65 | (':geq:', u'\u2265'), 66 | (':leq:', u'\u2264'), 67 | (':>>:', u'\u226B'), 68 | (':<<:', u'\u226A'), 69 | ] 70 | 71 | _MATH_SETS = [ 72 | (':subset:', u'\u2282'), 73 | (':subseteq:', u'\u2286'), 74 | (':supset:', u'\u2283'), 75 | (':supseteq:', u'\u2287'), 76 | (':in:', u'\u2208'), 77 | (':ni:', u'\u220B'), 78 | (':cap:', u'\u2229'), 79 | (':cup:', u'\u222A'), 80 | (':emptyset:', u'\u2205'), 81 | ] 82 | 83 | _MATH_LOGIC = [ 84 | (':neg:', u'\u00AC'), 85 | (':vee:', u'\u2228'), 86 | (':wedge:', u'\u2227'), 87 | (':forall:', u'\u2200'), 88 | (':exists:', u'\u2203'), 89 | (':therefore:', u'\u2234'), 90 | ] 91 | 92 | _MATH_CALCULUS = [ 93 | (':nabla:', u'\u2207'), 94 | (':partial:', u'\u2202'), 95 | (':integral:', u'\u222B'), 96 | ] 97 | 98 | _FRACTIONS = [ 99 | (':1/2:', u'\u00BD'), 100 | (':1/3:', u'\u2153'), 101 | (':2/3:', u'\u2154'), 102 | (':1/4:', u'\u00BC'), 103 | (':3/4:', u'\u00BE'), 104 | (':1/5:', u'\u2155'), 105 | (':2/5:', u'\u2156'), 106 | (':3/5:', u'\u2157'), 107 | (':4/5:', u'\u2158'), 108 | (':1/6:', u'\u2159'), 109 | (':5/6:', u'\u215A'), 110 | (':1/7:', u'\u2150'), 111 | (':1/8:', u'\u215B'), 112 | (':3/8:', u'\u215C'), 113 | (':5/8:', u'\u215D'), 114 | (':7/8:', u'\u215E'), 115 | (':1/9:', u'\u2151'), 116 | (':1/10:', u'\u2152'), 117 | ] 118 | 119 | _GREEK_LOWER = [ 120 | (':alpha:', u'\u03B1'), 121 | (':beta:', u'\u03B2'), 122 | (':gamma:', u'\u03B3'), 123 | (':delta:', u'\u03B4'), 124 | (':epsilon:', u'\u03B5'), 125 | (':zeta:', u'\u03B6'), 126 | (':eta:', u'\u03B7'), 127 | (':theta:', u'\u03B8'), 128 | (':iota:', u'\u03B9'), 129 | (':kappa:', u'\u03BA'), 130 | (':lambda:', u'\u03BB'), 131 | (':mu:', u'\u03BC'), 132 | (':nu:', u'\u03BD'), 133 | (':xi:', u'\u03BE'), 134 | (':omicron:', u'\u03BF'), 135 | (':pi:', u'\u03C0'), 136 | (':rho:', u'\u03C1'), 137 | (':sigma:', u'\u03C3'), 138 | (':tau:', u'\u03C4'), 139 | (':upsilon:', u'\u03C5'), 140 | (':phi:', u'\u03C6'), 141 | (':chi:', u'\u03C7'), 142 | (':psi:', u'\u03C8'), 143 | (':omega:', u'\u03C9'), 144 | ] 145 | 146 | _GREEK_UPPER = [ 147 | (':Alpha:', u'\u0391'), 148 | (':Beta:', u'\u0392'), 149 | (':Gamma:', u'\u0393'), 150 | (':Delta:', u'\u0394'), 151 | (':Epsilon:', u'\u0395'), 152 | (':Zeta:', u'\u0396'), 153 | (':Eta:', u'\u0397'), 154 | (':Theta:', u'\u0398'), 155 | (':Iota:', u'\u0399'), 156 | (':Kappa:', u'\u039A'), 157 | (':Lambda:', u'\u039B'), 158 | (':Mu:', u'\u039C'), 159 | (':Nu:', u'\u039D'), 160 | (':Xi:', u'\u039E'), 161 | (':Omicron:', u'\u039F'), 162 | (':Pi:', u'\u03A0'), 163 | (':Rho:', u'\u03A1'), 164 | (':Sigma:', u'\u03A3'), 165 | (':Tau:', u'\u03A4'), 166 | (':Upsilon:', u'\u03A5'), 167 | (':Phi:', u'\u03A6'), 168 | (':Chi:', u'\u03A7'), 169 | (':Psi:', u'\u03A8'), 170 | (':Omega:', u'\u03A9'), 171 | ] 172 | 173 | _CURRENCY = [ 174 | (':cent:', u'\u00A2'), 175 | (':pound:', u'\u00A3'), 176 | (':euro:', u'\u20AC'), 177 | (':lira:', u'\u20A4'), 178 | (':peso:', u'\u20B1'), 179 | (':ruble:', u'\u20BD'), 180 | (':rupee:', u'\u20B9'), 181 | (':won:', u'\u20A9'), 182 | (':yen:', u'\u00A5'), 183 | (':yuan:', u'\u00A5'), 184 | ] 185 | 186 | 187 | """ Create Index of Keys and Default List """ 188 | 189 | _SYMBOL_DICT = OrderedDict([ 190 | ("Arrows", _ARROWS), 191 | ("Typography", _TYPOGRAPHY), 192 | ("Math (General)", _MATH_GEN), 193 | ("Math (Binary Operators)", _MATH_BINARY), 194 | ("Math (Relational)", _MATH_RELATION), 195 | ("Math (Sets)", _MATH_SETS), 196 | ("Math (Logical)", _MATH_LOGIC), 197 | ("Math (Calculus)", _MATH_CALCULUS), 198 | ("Fractions", _FRACTIONS), 199 | ("Greek Symbols (Lowercase)", _GREEK_LOWER), 200 | ("Greek Symbols (Uppercase)", _GREEK_UPPER), 201 | ("Currency", _CURRENCY), 202 | ]) 203 | 204 | DEFAULT_MATCHES = list(itertools.chain.from_iterable(_SYMBOL_DICT.values())) 205 | -------------------------------------------------------------------------------- /src/replacer_pre-2.1.41.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Javascript code that performs symbol replacement. insert_symbols.py adds 3 | * this script to each Anki editor's WebView after setting the symbol list. 4 | * 5 | * This version is compatible with Anki versions 2.1.40 and older. FYI Anki 6 | * 2.0 uses jQuery version 1.5. 7 | */ 8 | 9 | var insert_symbols = new function() { 10 | 11 | const KEY_SPACE = 32; 12 | const KEY_ENTER = 13; 13 | 14 | var matchList = undefined; 15 | var shouldCheckOnKeyup = false; 16 | 17 | this.setMatchList = function(str) { 18 | matchList = JSON.parse(str); 19 | } 20 | 21 | // Keypress Handling: 22 | //---------------------------------- 23 | 24 | /** 25 | * During a long press, keydown events are fired repeatedly but keyup is 26 | * not fired till the end. Thus, checkForReplacement() should be called 27 | * here to make symbol replacement be responsive. 28 | * 29 | * However, the newly entered character does not appear in 30 | * focusNode.textContent until later. The new character is accessible via 31 | * the event object, but the WebView in Anki 2.0 doesn't support evt.key or 32 | * evt.code, so figuring out char mappings is complicated for many keys. 33 | * 34 | * Thus, the compromise is to call checkForReplacement() after whitespace 35 | * during keydown since those char mappings are relatively constant, but 36 | * defer to keyup for everything else. It works for the most part. 37 | */ 38 | this.onKeyDown = function(evt) { 39 | // Disable CTRL commands from triggering replacement: 40 | if (evt.ctrlKey) { 41 | return; 42 | } 43 | 44 | if (evt.which == KEY_SPACE || evt.which == KEY_ENTER) { 45 | checkForReplacement(true); 46 | } else { 47 | shouldCheckOnKeyup = true; 48 | } 49 | } 50 | 51 | this.onKeyUp = function(evt) { 52 | if (shouldCheckOnKeyup) { 53 | shouldCheckOnKeyup = false; 54 | checkForReplacement(false); 55 | } 56 | } 57 | 58 | /** 59 | * Add event handlers to Editor key events. Setup only needs to be 60 | * performed when the editor is first created. 61 | */ 62 | $(".field").keydown(this.onKeyDown); 63 | $(".field").keyup(this.onKeyUp); 64 | 65 | /** 66 | * Add event handlers to Reviewer key events to extend functionality to 67 | * "Edit Field During Review" plugin. This needs to be called each time 68 | * a question/answer is shown since that is when fields are made editable. 69 | */ 70 | this.setupReviewerKeyEvents = function() { 71 | $("[contenteditable=true]").keydown(this.onKeyDown); 72 | $("[contenteditable=true]").keyup(this.onKeyUp); 73 | } 74 | 75 | // Pattern Matching: 76 | //---------------------------------- 77 | 78 | /** 79 | * Checks whether the current text should be replaced by a symbol from the 80 | * symbol list. For simplicity, this function only looks at text within the 81 | * current Node. 82 | */ 83 | function checkForReplacement(isWhitespacePressed) { 84 | var sel = window.getSelection(); 85 | if (sel.isCollapsed) { 86 | var text = sel.focusNode.textContent; 87 | var cursorPos = sel.focusOffset; 88 | //debugDiv(sel.focusNode.textContent); 89 | 90 | var result = matchesKeyword(text, cursorPos, isWhitespacePressed); 91 | if (result.val !== null) { 92 | performReplacement(sel.focusNode, cursorPos - result.keylen, 93 | cursorPos, result.val, result.html); 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Checks whether the substring of TEXT up to END_INDEX matches any keys 100 | * from the match list. For non-special characters (ie. not colon-delimited 101 | * and not certain arrows), matching should only occur if both: 102 | * - when a whitespace key (space or enter) was pressed. 103 | * - when the character before the match is a whitespace. 104 | * See the README for more information. 105 | * 106 | * @param text A string containing the substring to check. 107 | * @param endIndex The length of the substring. 108 | * @return An object where VAL is value of the matched key-value pair 109 | * (or null if no match), KEYLEN is the length of the key in the 110 | * matched key-value pair, and HTML is whether the value should be 111 | * treated as raw HTML. 112 | */ 113 | function matchesKeyword(text, endIndex, isWhitespacePressed) { 114 | for (var i = 0, k; i < matchList.length; i++) { 115 | triggerOnSpace = (matchList[i].f == 0); 116 | 117 | // Skip entries that trigger only when whitespace is inputted: 118 | if (triggerOnSpace && !isWhitespacePressed) { 119 | continue; 120 | } 121 | 122 | key = matchList[i].key; 123 | var startIndex = endIndex - key.length; 124 | 125 | // Check if there is a match: 126 | if (text.substring(startIndex, endIndex) === key) { 127 | 128 | // If indicated, check if char before match is whitespace: 129 | if (triggerOnSpace && startIndex > 0 130 | && !/\s/.test(text[startIndex - 1])) { 131 | continue; 132 | } 133 | 134 | return { 135 | "val":matchList[i].val, 136 | "keylen":key.length, 137 | "html": (matchList[i].f == 2) 138 | }; 139 | } 140 | } 141 | 142 | return {"val":null, "keylen":0, "html": false}; 143 | } 144 | 145 | /** 146 | * Replaces the text in the given node with new text. Assumes that the node 147 | * is of type TEXT_NODE. 148 | * 149 | * @param node The node to perform replacement on. 150 | * @param rangeStart The start index of the range. 151 | * @param rangeEnd The end index of the range (should be 1 + the index of 152 | * the last character to be deleted). 153 | * @param newText Replacement text. 154 | */ 155 | function performReplacement(node, rangeStart, rangeEnd, newText, isHTML) { 156 | // Delete key: 157 | for (var i = rangeStart; i < rangeEnd; i++) { 158 | document.execCommand("delete", false, null); 159 | } 160 | 161 | // Insert new symbol: 162 | command = isHTML ? "insertHTML" : "insertText"; 163 | document.execCommand(command, false, newText); 164 | } 165 | 166 | // Debugging: 167 | //---------------------------------- 168 | 169 | // $("body").append('
'); 170 | // $("body").append('
'); 171 | 172 | // $(".debug1").html("Debug #1"); 173 | // $(".debug2").html("Debug #2"); 174 | 175 | // function debugDiv1(str) { 176 | // $(".debug1").html(str); 177 | // } 178 | 179 | // function debugDiv2(str) { 180 | // $(".debug2").html(str); 181 | // } 182 | } 183 | -------------------------------------------------------------------------------- /src/insert_symbols.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is run when the plugin is first loaded. It contains functions to set 3 | up the plugin, open the symbol list editor, and broadcast symbol list updates 4 | to editor windows that are open. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import aqt 11 | from anki.hooks import addHook, wrap 12 | from aqt.editor import Editor, EditorWebView 13 | from aqt.reviewer import Reviewer 14 | from aqt.browser import Browser 15 | 16 | from .browser_replacer import BrowserReplacer 17 | from .get_version import * 18 | from .symbol_manager import SymbolManager 19 | from .symbol_window import SymbolWindow 20 | 21 | """ 22 | Anki Version-specific Code 23 | """ 24 | 25 | ANKI_VER = get_anki_version() 26 | 27 | # Add-on path changed between Anki 2.0 and Anki 2.1 28 | if ANKI_VER == ANKI_VER_PRE_2_1_0: 29 | sys_encoding = sys.getfilesystemencoding() 30 | ADDON_PATH = os.path.dirname(__file__).decode(sys_encoding) 31 | else: 32 | ADDON_PATH = os.path.dirname(__file__) 33 | 34 | # Load new hooks if supported 35 | if ANKI_VER > ANKI_VER_PRE_23_10: 36 | from aqt import gui_hooks 37 | 38 | # Webview requires different JS between Anki 2.1.40 and Anki 2.1.41 39 | if ANKI_VER <= ANKI_VER_PRE_2_1_41: 40 | JS_FILE = "replacer_pre-2.1.41.js" 41 | else: 42 | JS_FILE = "replacer.js" 43 | 44 | 45 | """ 46 | Variable declarations 47 | """ 48 | 49 | ins_sym_manager = None 50 | ins_sym_window = None 51 | ins_sym_replacer = None 52 | 53 | ins_sym_webview_owners = { 54 | 'editors': [], 55 | 'reviewer': None 56 | } 57 | 58 | 59 | """ 60 | Javascript Loading & Updating 61 | """ 62 | 63 | def _update_JS(webview: EditorWebView): 64 | """ Updates the symbol list in the Javascript file. """ 65 | json = ins_sym_manager.get_JSON() 66 | webview.eval("insert_symbols.setMatchList(%s)" % json) 67 | 68 | def _load_JS(webview: EditorWebView): 69 | """ 70 | Loads replacer.js, the Javascript file which performs symbol replacement, 71 | into the given WebView. 72 | """ 73 | js_path = os.path.join(ADDON_PATH, JS_FILE) 74 | with open(js_path, 'r') as js_file: 75 | js = js_file.read() 76 | webview.eval(js) 77 | _update_JS(webview) 78 | 79 | def update_symbols(): 80 | """ 81 | This function is called by SymbolManager whenever the symbol list is 82 | updated. It updates the symbolList for every editor that is open. 83 | """ 84 | 85 | for editor in ins_sym_webview_owners['editors']: 86 | _update_JS(editor.web) 87 | 88 | if ins_sym_webview_owners['reviewer']: 89 | _update_JS(ins_sym_webview_owners['reviewer'].web) 90 | 91 | ins_sym_replacer.update_list(ins_sym_manager.get_match_list()) 92 | 93 | # aqt.utils.showInfo("Number of editors: %d" % len(ins_sym_webview_owners['editors'])) 94 | 95 | """ 96 | Editor Actions 97 | 98 | These actions occur when an instance of the card editor defined in Anki's 99 | aqt/editor.py (ie. Add Card window, Card Browser, or Reviewer) is opened. 100 | """ 101 | 102 | def on_editor_load_note(editor: Editor, focusTo=None): 103 | """ 104 | Anki calls Editor.loadNote() to refresh the editor's WebView, which occurs 105 | after setNote(), onHtmlEdit(), and bridge() / onBridgeCmd() in Editor is 106 | called. Editor.loadNote() resets Javascript code, so we must re-add our JS 107 | after every loadNote(). 108 | 109 | FYI: In Anki 2.1, the focusTo=None argument is new. 110 | """ 111 | if editor not in ins_sym_webview_owners['editors']: 112 | ins_sym_webview_owners['editors'].append(editor) 113 | 114 | _load_JS(editor.web) 115 | 116 | def on_editor_cleanup(editor: Editor): 117 | """ 118 | If the editor did not show any notes, on_editor_load_note() would not have 119 | been called and thus the Editor will not be in ins_sym_webview_owners. 120 | """ 121 | if editor in ins_sym_webview_owners['editors']: 122 | ins_sym_webview_owners['editors'].remove(editor) 123 | 124 | def on_browser_init(browser: Browser, main_window = None, card = None, 125 | search = None): 126 | ins_sym_replacer.on_browser_init(browser) 127 | 128 | def on_reviewer_initweb(reviewer: Reviewer): 129 | """ 130 | Anki calls Reviewer._initWeb() to update the WebView, which occurs when the 131 | reviewer is first opened or after every 100 cards have been reviewed. 132 | """ 133 | ins_sym_webview_owners['reviewer'] = reviewer 134 | _load_JS(reviewer.web) 135 | # aqt.utils.showInfo("on_reviewer_start() called") 136 | 137 | def on_reviewer_show_qa(card=None): 138 | """ 139 | This event is triggered when the Reviewer shows either a question or an 140 | answer. Since the Editable Fields plugin makes the fields editable when 141 | each card is created, key listeners need to be set up after each time. 142 | 143 | Added null checks to prevent AttributeError when reviewer is not properly initialized. 144 | """ 145 | reviewer = ins_sym_webview_owners.get('reviewer') 146 | if reviewer is None: 147 | return 148 | 149 | webview = getattr(reviewer, 'web', None) 150 | if webview: 151 | webview.eval("insert_symbols.setupReviewerKeyEvents()") 152 | 153 | def on_reviewer_cleanup(): 154 | """ This event is triggered when the Reviewer is about to be closed. """ 155 | ins_sym_webview_owners['reviewer'] = None 156 | # aqt.utils.showInfo("on_reviewer_end() called") 157 | 158 | 159 | """ 160 | Add-on Initialization 161 | 162 | These actions occur when a new profile is opened since each profile can have 163 | its own symbol list. 164 | """ 165 | 166 | def _setup_modules(): 167 | global ins_sym_manager, ins_sym_window, ins_sym_replacer 168 | 169 | ins_sym_manager = SymbolManager(aqt.mw, update_symbols) 170 | ins_sym_manager.on_profile_loaded() 171 | 172 | ins_sym_window = SymbolWindow(aqt.mw, ins_sym_manager) 173 | ins_sym_replacer = BrowserReplacer(ins_sym_manager.get_match_list()) 174 | 175 | def _setup_hooks(): 176 | """ 177 | Migrate to new hooks when possible though currently no suitable hooks for 178 | editor cleanup, browser init (gui_hooks.browser_will_show runs before search 179 | bar is set up), or reviewer webview initiation (gui_hooks.reviewer_did_init 180 | doesn't work). 181 | """ 182 | 183 | gui_hooks.editor_did_load_note.append(on_editor_load_note) 184 | Editor.cleanup = wrap(Editor.cleanup, on_editor_cleanup, 'before') 185 | 186 | Browser.__init__ = wrap(Browser.__init__, on_browser_init, 'after') 187 | 188 | Reviewer._initWeb = wrap(Reviewer._initWeb, on_reviewer_initweb, 'after') 189 | gui_hooks.reviewer_did_show_question.append(on_reviewer_show_qa) 190 | gui_hooks.reviewer_did_show_answer.append(on_reviewer_show_qa) 191 | gui_hooks.reviewer_will_end.append(on_reviewer_cleanup) 192 | 193 | def _setup_hooks_legacy(): 194 | Editor.loadNote = wrap(Editor.loadNote, on_editor_load_note, 'after') 195 | Editor.cleanup = wrap(Editor.cleanup, on_editor_cleanup, 'before') 196 | 197 | Browser.__init__ = wrap(Browser.__init__, on_browser_init, 'after') 198 | 199 | Reviewer._initWeb = wrap(Reviewer._initWeb, on_reviewer_initweb, 'after') 200 | addHook("showQuestion", on_reviewer_show_qa) 201 | addHook("showAnswer", on_reviewer_show_qa) 202 | addHook("reviewCleanup", on_reviewer_cleanup) 203 | 204 | # Perform setup when a new profile is loaded 205 | 206 | def on_profile_loaded(): 207 | _setup_modules() 208 | _setup_hooks() 209 | 210 | def on_profile_loaded_legacy(): 211 | _setup_modules() 212 | _setup_hooks_legacy() 213 | 214 | if ANKI_VER <= ANKI_VER_PRE_23_10: 215 | addHook("profileLoaded", on_profile_loaded_legacy) 216 | else: 217 | gui_hooks.profile_did_open.append(on_profile_loaded) 218 | 219 | # Add menu button 220 | open_action = aqt.qt.QAction("Insert Symbol Options...", aqt.mw, 221 | triggered=lambda: ins_sym_window.open()) 222 | aqt.mw.form.menuTools.addAction(open_action) 223 | -------------------------------------------------------------------------------- /src/replacer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Javascript code that performs symbol replacement. insert_symbols.py adds 3 | * this script to each Anki editor's WebView after setting the symbol list. 4 | * 5 | * This version is compatible with Anki versions 2.1.41 and newer. 6 | */ 7 | 8 | var insert_symbols = new function () { 9 | 10 | const SETUP_TIMEOUT = 500; // To prevent our code trying to add listeners before HTML is ready 11 | 12 | const KEY_SPACE = 32; 13 | const KEY_ENTER = 13; 14 | 15 | var matchList = undefined; 16 | var shouldCheckOnKeyup = false; 17 | 18 | this.setMatchList = function (str) { 19 | matchList = JSON.parse(str); 20 | } 21 | 22 | // Keypress Handling: 23 | //---------------------------------- 24 | 25 | /** 26 | * During a long press, keydown events are fired repeatedly but keyup is 27 | * not fired till the end. Thus, checkForReplacement() should be called 28 | * here to make symbol replacement be responsive. 29 | * 30 | * However, the newly entered character does not appear in 31 | * focusNode.textContent until later. The new character is accessible via 32 | * the event object, but the WebView in Anki 2.0 doesn't support evt.key or 33 | * evt.code, so figuring out char mappings is complicated for many keys. 34 | * 35 | * Thus, the compromise is to call checkForReplacement() after whitespace 36 | * during keydown since those char mappings are relatively constant, but 37 | * defer to keyup for everything else. It works for the most part. 38 | */ 39 | this.onKeyDown = function (evt) { 40 | // Disable CTRL commands from triggering replacement: 41 | if (evt.ctrlKey) { 42 | return; 43 | } 44 | 45 | if (evt.which == KEY_SPACE || evt.which == KEY_ENTER) { 46 | checkForReplacement(evt.currentTarget.getRootNode(), true); 47 | } else { 48 | shouldCheckOnKeyup = true; 49 | } 50 | } 51 | 52 | this.onKeyUp = function (evt) { 53 | if (shouldCheckOnKeyup) { 54 | shouldCheckOnKeyup = false; 55 | checkForReplacement(evt.currentTarget.getRootNode(), false); 56 | } 57 | } 58 | 59 | /** 60 | * Add event handlers to Editor key events. Setup only needs to be 61 | * performed when the editor is first created. 62 | */ 63 | 64 | // For Anki 2.1.41 - 2.1.49 65 | this.addListenersV1 = function() { 66 | forEditorField([], (field) => { 67 | if (!field.hasAttribute("has-type-symbols")) { 68 | field.editingArea.editable.addEventListener("keydown", this.onKeyDown) 69 | field.editingArea.editable.addEventListener("keyup", this.onKeyUp) 70 | field.setAttribute("has-type-symbols", "") 71 | } 72 | }) 73 | } 74 | 75 | // For Anki 2.1.50+ 76 | this.addListenersV2 = function() { 77 | setTimeout(() => { 78 | editorFields = document.getElementsByClassName("rich-text-editable"); 79 | 80 | for (field of editorFields) { 81 | if (field.shadowRoot !== undefined) { 82 | if (!field.hasAttribute("has-type-symbols")) { 83 | field.shadowRoot.addEventListener("keydown", this.onKeyDown); 84 | field.shadowRoot.addEventListener("keyup", this.onKeyUp); 85 | field.setAttribute("has-type-symbols", ""); 86 | } 87 | } 88 | } 89 | }, SETUP_TIMEOUT); 90 | } 91 | 92 | if (typeof forEditorField !== 'undefined') { 93 | this.addListenersV1(); 94 | } else { 95 | this.addListenersV2(); 96 | } 97 | 98 | /** 99 | * Add event handlers to Reviewer key events to extend functionality to 100 | * "Edit Field During Review" plugin. This needs to be called each time 101 | * a question/answer is shown since that is when fields are made editable. 102 | */ 103 | this.setupReviewerKeyEvents = function () { 104 | $("[contenteditable]").keydown(this.onKeyDown); 105 | $("[contenteditable]").keyup(this.onKeyUp); 106 | } 107 | 108 | // Pattern Matching: 109 | //---------------------------------- 110 | 111 | /** 112 | * Checks whether the current text should be replaced by a symbol from the 113 | * symbol list. For simplicity, this function only looks at text within the 114 | * current Node. 115 | */ 116 | function checkForReplacement(root, isWhitespacePressed) { 117 | var sel = root.getSelection(); 118 | if (sel.isCollapsed) { 119 | var text = sel.focusNode.textContent; 120 | var cursorPos = sel.focusOffset; 121 | //debugDiv(sel.focusNode.textContent); 122 | 123 | var result = matchesKeyword(text, cursorPos, isWhitespacePressed); 124 | if (result.val !== null) { 125 | performReplacement(sel.focusNode, cursorPos - result.keylen, 126 | cursorPos, result.val, result.html); 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Checks whether the substring of TEXT up to END_INDEX matches any keys 133 | * from the match list. For non-special characters (ie. not colon-delimited 134 | * and not certain arrows), matching should only occur if both: 135 | * - when a whitespace key (space or enter) was pressed. 136 | * - when the character before the match is a whitespace. 137 | * See the README for more information. 138 | * 139 | * @param text A string containing the substring to check. 140 | * @param endIndex The length of the substring. 141 | * @return An object where VAL is value of the matched key-value pair 142 | * (or null if no match), KEYLEN is the length of the key in the 143 | * matched key-value pair, and HTML is whether the value should be 144 | * treated as raw HTML. 145 | */ 146 | function matchesKeyword(text, endIndex, isWhitespacePressed) { 147 | for (var i = 0, k; i < matchList.length; i++) { 148 | triggerOnSpace = (matchList[i].f == 0); 149 | 150 | // Skip entries that trigger only when whitespace is inputted: 151 | if (triggerOnSpace && !isWhitespacePressed) { 152 | continue; 153 | } 154 | 155 | key = matchList[i].key; 156 | var startIndex = endIndex - key.length; 157 | 158 | // Check if there is a match: 159 | if (text.substring(startIndex, endIndex) === key) { 160 | 161 | // If indicated, check if char before match is whitespace: 162 | if (triggerOnSpace && startIndex > 0 163 | && !/\s/.test(text[startIndex - 1])) { 164 | continue; 165 | } 166 | 167 | return { 168 | "val": matchList[i].val, 169 | "keylen": key.length, 170 | "html": (matchList[i].f == 2) 171 | }; 172 | } 173 | } 174 | 175 | return { "val": null, "keylen": 0, "html": false }; 176 | } 177 | 178 | /** 179 | * Replaces the text in the given node with new text. Assumes that the node 180 | * is of type TEXT_NODE. 181 | * 182 | * @param node The node to perform replacement on. 183 | * @param rangeStart The start index of the range. 184 | * @param rangeEnd The end index of the range (should be 1 + the index of 185 | * the last character to be deleted). 186 | * @param newText Replacement text. 187 | */ 188 | function performReplacement(node, rangeStart, rangeEnd, newText, isHTML) { 189 | // Delete key: 190 | for (var i = rangeStart; i < rangeEnd; i++) { 191 | document.execCommand("delete", false, null); 192 | } 193 | 194 | // Insert new symbol: 195 | command = isHTML ? "insertHTML" : "insertText"; 196 | document.execCommand(command, false, newText); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/symbol_manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains SymbolManager, which keeps track of the symbol list, 3 | validates new lists, and interfaces with the SQLite database. 4 | 5 | Symbol lists are stored on a per-profile basis in the the collections database, 6 | which is referenecd by mw.col.db. There is a separate profiles database that 7 | Anki uses to store user profiles (mw.pm.db). 8 | """ 9 | 10 | import sys 11 | import string 12 | import json 13 | import aqt 14 | 15 | from .get_version import * 16 | from .default_symbols import DEFAULT_MATCHES, SPECIAL_KEYS 17 | 18 | class SymbolManager(object): 19 | """ 20 | SymbolManager takes in a callback function so that when the symbol list 21 | is updated, each Anki editor window can be reloaded with the updated 22 | symbol list. 23 | """ 24 | 25 | TBL_NAME = 'ins_symbols' 26 | 27 | SUCCESS = 0 28 | ERR_NO_DATABASE = -1 29 | ERR_INVALID_FORMAT = -2 30 | ERR_KEY_CONFLICT = -3 31 | 32 | def __init__(self, main_window, update_callback): 33 | self._mw = main_window 34 | self._symbols = None 35 | self._defaults = None 36 | self._update_callback = update_callback 37 | 38 | def on_profile_loaded(self): 39 | """ 40 | Called when a new profile is loaded. First tries to load the symbol 41 | list from the database. If that is not successful, loads the default 42 | symbol list instead, then saves it to the database. 43 | """ 44 | is_load_successful = self._check_db_exists() 45 | 46 | if not is_load_successful: 47 | self._create_db() 48 | else: 49 | code = self._load_from_db() 50 | is_load_successful = (code == self.SUCCESS) 51 | 52 | # At this point, the database should have been created already. If load 53 | # wasn't successful for whatever reason, use the default list and save 54 | # it to the database. 55 | if not is_load_successful: 56 | self._symbols = self.get_default_list() 57 | self._save_to_db() 58 | 59 | 60 | """ Getters """ 61 | 62 | def get_match_list(self): 63 | """ 64 | Converts the symbol list into a match list sorted by key length in 65 | descending order. Each entry contains the key/value plus a flag 66 | indicating the type of entry. 67 | 68 | Flag: 2 = HTML block, 1 = immediate, 0 = normal 69 | """ 70 | if not self._symbols: 71 | return None 72 | 73 | symbols = sorted(self._symbols, key=lambda x: len(x[0]), reverse=True) 74 | output = [] 75 | for key, val in symbols: 76 | if key.startswith('::') and key.endswith('::'): 77 | flag = 2 78 | elif (key.startswith(':') and key.endswith(':') 79 | or key in SPECIAL_KEYS): 80 | flag = 1 81 | else: 82 | flag = 0 83 | 84 | output.append({"key": key,"val": val, "f": flag}) 85 | return output 86 | 87 | def get_JSON(self): 88 | """ 89 | Returns a JSON version of the match list 90 | """ 91 | output = self.get_match_list() 92 | if not output: 93 | return "'[]'" 94 | # sys.stderr.write(json.dumps(output)) 95 | # sys.stderr.write(json.dumps(json.dumps(output))) 96 | return json.dumps(json.dumps(output)) 97 | 98 | def get_list(self): 99 | """ 100 | Returns a copy of the symbol list sorted in alphabetical order. 101 | """ 102 | return sorted(self._symbols, key=lambda x: x[0]) 103 | 104 | def get_default_list(self): 105 | """ 106 | Returns a copy of the default symbol list sorted in alphabetical order. 107 | """ 108 | return sorted(DEFAULT_MATCHES, key=lambda x: x[0]) 109 | 110 | 111 | """ Setters """ 112 | 113 | def _set_symbol_list(self, new_list): 114 | """ 115 | Performs error-checking, then updates self._symbols. Returns None if 116 | there are no errors, or otherwise returns a tuple of the format 117 | (ERROR_CODE, ERRORS) as detailed below: 118 | 119 | Error Code: Content of ERRORS: 120 | ------------- ---------------------- 121 | ERR_INVALID_FORMAT List of indices where format of new_list is wrong. 122 | ERR_KEY_CONFLICT List of key conflicts in new_list. 123 | """ 124 | errors = SymbolManager.check_format(new_list) 125 | if errors: 126 | return (self.ERR_INVALID_FORMAT, errors) 127 | 128 | errors = SymbolManager.check_for_duplicates(new_list) 129 | if errors: 130 | return (self.ERR_KEY_CONFLICT, errors) 131 | 132 | self._symbols = new_list 133 | return None 134 | 135 | def update_and_save_symbol_list(self, new_list): 136 | """ 137 | Attempts to update the symbol list, and if successful, saves the symbol 138 | list to database and calls the callback function. Returns the same 139 | output as _set_symbol_list(). 140 | """ 141 | errors = self._set_symbol_list(new_list) 142 | if not errors: 143 | self._save_to_db() 144 | self._update_callback() 145 | return errors 146 | 147 | 148 | """ Validation Static Functions """ 149 | 150 | @staticmethod 151 | def check_format(kv_list, ignore_empty=False): 152 | """ 153 | Checks that each entry is of the format (key, value), and that each key 154 | is not None or the empty string. This function can be set to ignore 155 | emtpy lines. 156 | 157 | @param kv_list: A list. 158 | @param ignore_empty: Whether to skip empty lines (represented as an 159 | empty list) 160 | @return: Returns a list of (index, line_contents), or None if no 161 | invalid lines. 162 | """ 163 | has_error = False 164 | errors = [] 165 | for i in range(len(kv_list)): 166 | item = kv_list[i] 167 | if ignore_empty and len(item) == 0: 168 | continue 169 | 170 | if len(item) != 2 or not item[0] or not item[1]: 171 | has_error = True 172 | err_str = ' '.join(map(str, item)) 173 | errors.append(tuple((i + 1, err_str))) 174 | return errors if has_error else None 175 | 176 | @staticmethod 177 | def check_if_key_valid(key): 178 | """ Checks whether key is a valid standalone key. """ 179 | return not True in [c in key for c in string.whitespace] 180 | 181 | @staticmethod 182 | def check_if_key_duplicate(new_key, kv_list): 183 | """ 184 | Checks to see if the new key would be a duplicate of any existing keys 185 | in the given key-value list. 186 | """ 187 | for k, v in kv_list: 188 | if new_key == k: 189 | return True 190 | return None 191 | 192 | @staticmethod 193 | def check_for_duplicates(kv_list): 194 | """ 195 | Checks for duplicate keys within the key-value list and returns a list 196 | of duplicate keys. This function accepts empty lines within the key- 197 | value list, empty list. 198 | 199 | @return: Returns a set of duplicate keys, or None if there are no 200 | duplicates. 201 | """ 202 | has_duplicate = False 203 | duplicates = set() 204 | 205 | for i in range(len(kv_list)): 206 | if len(kv_list[i]) == 0: 207 | continue 208 | 209 | for j in range(i): 210 | if len(kv_list[j]) == 0: 211 | continue 212 | k1 = kv_list[i][0] 213 | k2 = kv_list[j][0] 214 | 215 | if k1 == k2: 216 | has_duplicate = True 217 | duplicates.add(k1) 218 | 219 | return duplicates if has_duplicate else None 220 | 221 | 222 | """ 223 | Database Access Functions 224 | """ 225 | 226 | def _check_db_exists(self): 227 | """ Returns whether the symbol database exists. """ 228 | query = "SELECT * FROM sqlite_master WHERE type='table' AND name='%s'" 229 | return self._mw.col.db.first(query % self.TBL_NAME) 230 | 231 | def _create_db(self): 232 | """ Creates a new table for the symbol list. """ 233 | query = "CREATE TABLE %s (key varchar(255), value varchar(255))" 234 | self._mw.col.db.execute(query % self.TBL_NAME) 235 | 236 | def _load_from_db(self): 237 | """ 238 | Attempts to load the symbol list from the database, and returns a code 239 | indicating the result. 240 | """ 241 | symbols = self._mw.col.db.all("SELECT * FROM %s" % self.TBL_NAME) 242 | 243 | if not symbols: 244 | return self.ERR_NO_DATABASE 245 | errors = self._set_symbol_list(symbols) 246 | 247 | return errors[0] if errors else self.SUCCESS 248 | 249 | def _save_to_db(self): 250 | """ 251 | Deletes all old values, then writes the symbol list into the database. 252 | """ 253 | self._mw.col.db.execute("delete from %s" % self.TBL_NAME) 254 | for (k, v) in self._symbols: 255 | query = "INSERT INTO %s VALUES (?, ?)" 256 | self._mw.col.db.execute(query % self.TBL_NAME, k, v) 257 | 258 | # Anki no longer requires (or supports) committing in 23.10 or later 259 | if get_anki_version() <= ANKI_VER_PRE_23_10: 260 | self._mw.col.db.commit() 261 | 262 | -------------------------------------------------------------------------------- /src/Ui_SymbolWindow_5.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'Ui_SymbolWindow.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.9.2 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_SymbolWindow(object): 12 | def setupUi(self, SymbolWindow): 13 | SymbolWindow.setObjectName("SymbolWindow") 14 | SymbolWindow.resize(500, 450) 15 | SymbolWindow.setMinimumSize(QtCore.QSize(500, 450)) 16 | SymbolWindow.setAcceptDrops(False) 17 | SymbolWindow.setAutoFillBackground(False) 18 | self.gridLayout = QtWidgets.QGridLayout(SymbolWindow) 19 | self.gridLayout.setObjectName("gridLayout") 20 | self.verticalLayout = QtWidgets.QVBoxLayout() 21 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 22 | self.verticalLayout.setSpacing(10) 23 | self.verticalLayout.setObjectName("verticalLayout") 24 | self.horizontalLayout = QtWidgets.QHBoxLayout() 25 | self.horizontalLayout.setSpacing(10) 26 | self.horizontalLayout.setObjectName("horizontalLayout") 27 | self.gridLayout_2 = QtWidgets.QGridLayout() 28 | self.gridLayout_2.setSpacing(8) 29 | self.gridLayout_2.setObjectName("gridLayout_2") 30 | self.keyLineEdit = QtWidgets.QLineEdit(SymbolWindow) 31 | self.keyLineEdit.setMinimumSize(QtCore.QSize(186, 0)) 32 | font = QtGui.QFont() 33 | font.setFamily("Segoe UI") 34 | font.setPointSize(9) 35 | self.keyLineEdit.setFont(font) 36 | self.keyLineEdit.setText("") 37 | self.keyLineEdit.setObjectName("keyLineEdit") 38 | self.gridLayout_2.addWidget(self.keyLineEdit, 1, 1, 1, 1) 39 | self.valueLineEdit = QtWidgets.QLineEdit(SymbolWindow) 40 | font = QtGui.QFont() 41 | font.setFamily("Segoe UI") 42 | font.setPointSize(9) 43 | self.valueLineEdit.setFont(font) 44 | self.valueLineEdit.setObjectName("valueLineEdit") 45 | self.gridLayout_2.addWidget(self.valueLineEdit, 1, 2, 1, 1) 46 | self.labelWith = QtWidgets.QLabel(SymbolWindow) 47 | self.labelWith.setObjectName("labelWith") 48 | self.gridLayout_2.addWidget(self.labelWith, 0, 2, 1, 1) 49 | self.labelReplace = QtWidgets.QLabel(SymbolWindow) 50 | self.labelReplace.setObjectName("labelReplace") 51 | self.gridLayout_2.addWidget(self.labelReplace, 0, 1, 1, 1) 52 | self.tableWidget = QtWidgets.QTableWidget(SymbolWindow) 53 | self.tableWidget.setEnabled(True) 54 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 55 | sizePolicy.setHorizontalStretch(0) 56 | sizePolicy.setVerticalStretch(0) 57 | sizePolicy.setHeightForWidth(self.tableWidget.sizePolicy().hasHeightForWidth()) 58 | self.tableWidget.setSizePolicy(sizePolicy) 59 | self.tableWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.WheelFocus) 60 | self.tableWidget.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) 61 | self.tableWidget.setMidLineWidth(0) 62 | self.tableWidget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 63 | self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) 64 | self.tableWidget.setTabKeyNavigation(False) 65 | self.tableWidget.setProperty("showDropIndicator", False) 66 | self.tableWidget.setDragDropOverwriteMode(False) 67 | self.tableWidget.setAlternatingRowColors(True) 68 | self.tableWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) 69 | self.tableWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) 70 | self.tableWidget.setTextElideMode(QtCore.Qt.TextElideMode.ElideRight) 71 | self.tableWidget.setShowGrid(True) 72 | self.tableWidget.setRowCount(2) 73 | self.tableWidget.setColumnCount(2) 74 | self.tableWidget.setObjectName("tableWidget") 75 | item = QtWidgets.QTableWidgetItem() 76 | self.tableWidget.setItem(0, 0, item) 77 | item = QtWidgets.QTableWidgetItem() 78 | self.tableWidget.setItem(0, 1, item) 79 | item = QtWidgets.QTableWidgetItem() 80 | self.tableWidget.setItem(1, 0, item) 81 | item = QtWidgets.QTableWidgetItem() 82 | self.tableWidget.setItem(1, 1, item) 83 | self.tableWidget.horizontalHeader().setVisible(False) 84 | self.tableWidget.horizontalHeader().setDefaultSectionSize(190) 85 | self.tableWidget.horizontalHeader().setMinimumSectionSize(100) 86 | self.tableWidget.verticalHeader().setVisible(False) 87 | self.gridLayout_2.addWidget(self.tableWidget, 2, 1, 1, 2) 88 | self.horizontalLayout.addLayout(self.gridLayout_2) 89 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 90 | self.verticalLayout_2.setContentsMargins(0, -1, -1, -1) 91 | self.verticalLayout_2.setObjectName("verticalLayout_2") 92 | self.labelBlank = QtWidgets.QLabel(SymbolWindow) 93 | self.labelBlank.setText("") 94 | self.labelBlank.setObjectName("labelBlank") 95 | self.verticalLayout_2.addWidget(self.labelBlank) 96 | self.addReplaceButton = QtWidgets.QPushButton(SymbolWindow) 97 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) 98 | sizePolicy.setHorizontalStretch(0) 99 | sizePolicy.setVerticalStretch(0) 100 | sizePolicy.setHeightForWidth(self.addReplaceButton.sizePolicy().hasHeightForWidth()) 101 | self.addReplaceButton.setSizePolicy(sizePolicy) 102 | self.addReplaceButton.setMaximumSize(QtCore.QSize(244, 16777215)) 103 | self.addReplaceButton.setObjectName("addReplaceButton") 104 | self.verticalLayout_2.addWidget(self.addReplaceButton) 105 | self.deleteButton = QtWidgets.QPushButton(SymbolWindow) 106 | self.deleteButton.setObjectName("deleteButton") 107 | self.verticalLayout_2.addWidget(self.deleteButton) 108 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) 109 | self.verticalLayout_2.addItem(spacerItem) 110 | self.horizontalLayout.addLayout(self.verticalLayout_2) 111 | self.verticalLayout.addLayout(self.horizontalLayout) 112 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 113 | self.horizontalLayout_2.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint) 114 | self.horizontalLayout_2.setContentsMargins(-1, 5, -1, 0) 115 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 116 | self.importButton = QtWidgets.QPushButton(SymbolWindow) 117 | self.importButton.setObjectName("importButton") 118 | self.horizontalLayout_2.addWidget(self.importButton) 119 | self.exportButton = QtWidgets.QPushButton(SymbolWindow) 120 | self.exportButton.setObjectName("exportButton") 121 | self.horizontalLayout_2.addWidget(self.exportButton) 122 | spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) 123 | self.horizontalLayout_2.addItem(spacerItem1) 124 | self.okButton = QtWidgets.QPushButton(SymbolWindow) 125 | self.okButton.setObjectName("okButton") 126 | self.horizontalLayout_2.addWidget(self.okButton) 127 | self.cancelButton = QtWidgets.QPushButton(SymbolWindow) 128 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) 129 | sizePolicy.setHorizontalStretch(0) 130 | sizePolicy.setVerticalStretch(0) 131 | sizePolicy.setHeightForWidth(self.cancelButton.sizePolicy().hasHeightForWidth()) 132 | self.cancelButton.setSizePolicy(sizePolicy) 133 | self.cancelButton.setObjectName("cancelButton") 134 | self.horizontalLayout_2.addWidget(self.cancelButton) 135 | self.resetButton = QtWidgets.QPushButton(SymbolWindow) 136 | self.resetButton.setObjectName("resetButton") 137 | self.horizontalLayout_2.addWidget(self.resetButton) 138 | self.verticalLayout.addLayout(self.horizontalLayout_2) 139 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 140 | 141 | self.retranslateUi(SymbolWindow) 142 | QtCore.QMetaObject.connectSlotsByName(SymbolWindow) 143 | 144 | def retranslateUi(self, SymbolWindow): 145 | _translate = QtCore.QCoreApplication.translate 146 | SymbolWindow.setWindowTitle(_translate("SymbolWindow", "Insert Symbol Options")) 147 | self.labelWith.setText(_translate("SymbolWindow", "With")) 148 | self.labelReplace.setText(_translate("SymbolWindow", "Replace")) 149 | __sortingEnabled = self.tableWidget.isSortingEnabled() 150 | self.tableWidget.setSortingEnabled(False) 151 | item = self.tableWidget.item(0, 0) 152 | item.setText(_translate("SymbolWindow", "key1")) 153 | item = self.tableWidget.item(0, 1) 154 | item.setText(_translate("SymbolWindow", "value1")) 155 | item = self.tableWidget.item(1, 0) 156 | item.setText(_translate("SymbolWindow", "key2")) 157 | item = self.tableWidget.item(1, 1) 158 | item.setText(_translate("SymbolWindow", "value2")) 159 | self.tableWidget.setSortingEnabled(__sortingEnabled) 160 | self.addReplaceButton.setText(_translate("SymbolWindow", "Add")) 161 | self.deleteButton.setText(_translate("SymbolWindow", "Delete")) 162 | self.importButton.setText(_translate("SymbolWindow", "Import")) 163 | self.exportButton.setText(_translate("SymbolWindow", "Export")) 164 | self.okButton.setText(_translate("SymbolWindow", "OK")) 165 | self.cancelButton.setText(_translate("SymbolWindow", "Cancel")) 166 | self.resetButton.setText(_translate("SymbolWindow", "Reset")) 167 | 168 | -------------------------------------------------------------------------------- /src/Ui_SymbolWindow_6.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'Ui_SymbolWindow.ui' 4 | # 5 | # Created by: PyQt6 UI code generator 6.5.3 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt6 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_SymbolWindow(object): 15 | def setupUi(self, SymbolWindow): 16 | SymbolWindow.setObjectName("SymbolWindow") 17 | SymbolWindow.resize(500, 450) 18 | SymbolWindow.setMinimumSize(QtCore.QSize(500, 450)) 19 | SymbolWindow.setAcceptDrops(False) 20 | SymbolWindow.setAutoFillBackground(False) 21 | self.gridLayout = QtWidgets.QGridLayout(SymbolWindow) 22 | self.gridLayout.setObjectName("gridLayout") 23 | self.verticalLayout = QtWidgets.QVBoxLayout() 24 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 25 | self.verticalLayout.setSpacing(10) 26 | self.verticalLayout.setObjectName("verticalLayout") 27 | self.horizontalLayout = QtWidgets.QHBoxLayout() 28 | self.horizontalLayout.setSpacing(10) 29 | self.horizontalLayout.setObjectName("horizontalLayout") 30 | self.gridLayout_2 = QtWidgets.QGridLayout() 31 | self.gridLayout_2.setSpacing(8) 32 | self.gridLayout_2.setObjectName("gridLayout_2") 33 | self.keyLineEdit = QtWidgets.QLineEdit(parent=SymbolWindow) 34 | self.keyLineEdit.setMinimumSize(QtCore.QSize(186, 0)) 35 | font = QtGui.QFont() 36 | font.setFamily("Segoe UI") 37 | font.setPointSize(9) 38 | self.keyLineEdit.setFont(font) 39 | self.keyLineEdit.setText("") 40 | self.keyLineEdit.setObjectName("keyLineEdit") 41 | self.gridLayout_2.addWidget(self.keyLineEdit, 1, 1, 1, 1) 42 | self.valueLineEdit = QtWidgets.QLineEdit(parent=SymbolWindow) 43 | font = QtGui.QFont() 44 | font.setFamily("Segoe UI") 45 | font.setPointSize(9) 46 | self.valueLineEdit.setFont(font) 47 | self.valueLineEdit.setObjectName("valueLineEdit") 48 | self.gridLayout_2.addWidget(self.valueLineEdit, 1, 2, 1, 1) 49 | self.labelWith = QtWidgets.QLabel(parent=SymbolWindow) 50 | self.labelWith.setObjectName("labelWith") 51 | self.gridLayout_2.addWidget(self.labelWith, 0, 2, 1, 1) 52 | self.labelReplace = QtWidgets.QLabel(parent=SymbolWindow) 53 | self.labelReplace.setObjectName("labelReplace") 54 | self.gridLayout_2.addWidget(self.labelReplace, 0, 1, 1, 1) 55 | self.tableWidget = QtWidgets.QTableWidget(parent=SymbolWindow) 56 | self.tableWidget.setEnabled(True) 57 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) 58 | sizePolicy.setHorizontalStretch(0) 59 | sizePolicy.setVerticalStretch(0) 60 | sizePolicy.setHeightForWidth(self.tableWidget.sizePolicy().hasHeightForWidth()) 61 | self.tableWidget.setSizePolicy(sizePolicy) 62 | self.tableWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.WheelFocus) 63 | self.tableWidget.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) 64 | self.tableWidget.setMidLineWidth(0) 65 | self.tableWidget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 66 | self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) 67 | self.tableWidget.setTabKeyNavigation(False) 68 | self.tableWidget.setProperty("showDropIndicator", False) 69 | self.tableWidget.setDragDropOverwriteMode(False) 70 | self.tableWidget.setAlternatingRowColors(True) 71 | self.tableWidget.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) 72 | self.tableWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) 73 | self.tableWidget.setTextElideMode(QtCore.Qt.TextElideMode.ElideRight) 74 | self.tableWidget.setShowGrid(True) 75 | self.tableWidget.setRowCount(2) 76 | self.tableWidget.setColumnCount(2) 77 | self.tableWidget.setObjectName("tableWidget") 78 | item = QtWidgets.QTableWidgetItem() 79 | self.tableWidget.setItem(0, 0, item) 80 | item = QtWidgets.QTableWidgetItem() 81 | self.tableWidget.setItem(0, 1, item) 82 | item = QtWidgets.QTableWidgetItem() 83 | self.tableWidget.setItem(1, 0, item) 84 | item = QtWidgets.QTableWidgetItem() 85 | self.tableWidget.setItem(1, 1, item) 86 | self.tableWidget.horizontalHeader().setVisible(False) 87 | self.tableWidget.horizontalHeader().setDefaultSectionSize(190) 88 | self.tableWidget.horizontalHeader().setMinimumSectionSize(100) 89 | self.tableWidget.verticalHeader().setVisible(False) 90 | self.gridLayout_2.addWidget(self.tableWidget, 2, 1, 1, 2) 91 | self.horizontalLayout.addLayout(self.gridLayout_2) 92 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 93 | self.verticalLayout_2.setContentsMargins(0, -1, -1, -1) 94 | self.verticalLayout_2.setObjectName("verticalLayout_2") 95 | self.labelBlank = QtWidgets.QLabel(parent=SymbolWindow) 96 | self.labelBlank.setText("") 97 | self.labelBlank.setObjectName("labelBlank") 98 | self.verticalLayout_2.addWidget(self.labelBlank) 99 | self.addReplaceButton = QtWidgets.QPushButton(parent=SymbolWindow) 100 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) 101 | sizePolicy.setHorizontalStretch(0) 102 | sizePolicy.setVerticalStretch(0) 103 | sizePolicy.setHeightForWidth(self.addReplaceButton.sizePolicy().hasHeightForWidth()) 104 | self.addReplaceButton.setSizePolicy(sizePolicy) 105 | self.addReplaceButton.setMaximumSize(QtCore.QSize(244, 16777215)) 106 | self.addReplaceButton.setObjectName("addReplaceButton") 107 | self.verticalLayout_2.addWidget(self.addReplaceButton) 108 | self.deleteButton = QtWidgets.QPushButton(parent=SymbolWindow) 109 | self.deleteButton.setObjectName("deleteButton") 110 | self.verticalLayout_2.addWidget(self.deleteButton) 111 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) 112 | self.verticalLayout_2.addItem(spacerItem) 113 | self.horizontalLayout.addLayout(self.verticalLayout_2) 114 | self.verticalLayout.addLayout(self.horizontalLayout) 115 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 116 | self.horizontalLayout_2.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetDefaultConstraint) 117 | self.horizontalLayout_2.setContentsMargins(-1, 5, -1, 0) 118 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 119 | self.importButton = QtWidgets.QPushButton(parent=SymbolWindow) 120 | self.importButton.setObjectName("importButton") 121 | self.horizontalLayout_2.addWidget(self.importButton) 122 | self.exportButton = QtWidgets.QPushButton(parent=SymbolWindow) 123 | self.exportButton.setObjectName("exportButton") 124 | self.horizontalLayout_2.addWidget(self.exportButton) 125 | spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) 126 | self.horizontalLayout_2.addItem(spacerItem1) 127 | self.okButton = QtWidgets.QPushButton(parent=SymbolWindow) 128 | self.okButton.setObjectName("okButton") 129 | self.horizontalLayout_2.addWidget(self.okButton) 130 | self.cancelButton = QtWidgets.QPushButton(parent=SymbolWindow) 131 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) 132 | sizePolicy.setHorizontalStretch(0) 133 | sizePolicy.setVerticalStretch(0) 134 | sizePolicy.setHeightForWidth(self.cancelButton.sizePolicy().hasHeightForWidth()) 135 | self.cancelButton.setSizePolicy(sizePolicy) 136 | self.cancelButton.setObjectName("cancelButton") 137 | self.horizontalLayout_2.addWidget(self.cancelButton) 138 | self.resetButton = QtWidgets.QPushButton(parent=SymbolWindow) 139 | self.resetButton.setObjectName("resetButton") 140 | self.horizontalLayout_2.addWidget(self.resetButton) 141 | self.verticalLayout.addLayout(self.horizontalLayout_2) 142 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 143 | 144 | self.retranslateUi(SymbolWindow) 145 | QtCore.QMetaObject.connectSlotsByName(SymbolWindow) 146 | 147 | def retranslateUi(self, SymbolWindow): 148 | _translate = QtCore.QCoreApplication.translate 149 | SymbolWindow.setWindowTitle(_translate("SymbolWindow", "Insert Symbol Options")) 150 | self.labelWith.setText(_translate("SymbolWindow", "With")) 151 | self.labelReplace.setText(_translate("SymbolWindow", "Replace")) 152 | __sortingEnabled = self.tableWidget.isSortingEnabled() 153 | self.tableWidget.setSortingEnabled(False) 154 | item = self.tableWidget.item(0, 0) 155 | item.setText(_translate("SymbolWindow", "key1")) 156 | item = self.tableWidget.item(0, 1) 157 | item.setText(_translate("SymbolWindow", "value1")) 158 | item = self.tableWidget.item(1, 0) 159 | item.setText(_translate("SymbolWindow", "key2")) 160 | item = self.tableWidget.item(1, 1) 161 | item.setText(_translate("SymbolWindow", "value2")) 162 | self.tableWidget.setSortingEnabled(__sortingEnabled) 163 | self.addReplaceButton.setText(_translate("SymbolWindow", "Add")) 164 | self.deleteButton.setText(_translate("SymbolWindow", "Delete")) 165 | self.importButton.setText(_translate("SymbolWindow", "Import")) 166 | self.exportButton.setText(_translate("SymbolWindow", "Export")) 167 | self.okButton.setText(_translate("SymbolWindow", "OK")) 168 | self.cancelButton.setText(_translate("SymbolWindow", "Cancel")) 169 | self.resetButton.setText(_translate("SymbolWindow", "Reset")) 170 | 171 | -------------------------------------------------------------------------------- /src/Ui_SymbolWindow_4.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'Ui_SymbolWindow.ui' 4 | # 5 | # Created by: PyQt4 UI code generator 4.11.4 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt4 import QtCore, QtGui 10 | 11 | try: 12 | _fromUtf8 = QtCore.QString.fromUtf8 13 | except AttributeError: 14 | def _fromUtf8(s): 15 | return s 16 | 17 | try: 18 | _encoding = QtGui.QApplication.UnicodeUTF8 19 | def _translate(context, text, disambig): 20 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 21 | except AttributeError: 22 | def _translate(context, text, disambig): 23 | return QtGui.QApplication.translate(context, text, disambig) 24 | 25 | class Ui_SymbolWindow(object): 26 | def setupUi(self, SymbolWindow): 27 | SymbolWindow.setObjectName(_fromUtf8("SymbolWindow")) 28 | SymbolWindow.resize(500, 450) 29 | SymbolWindow.setMinimumSize(QtCore.QSize(500, 450)) 30 | SymbolWindow.setAcceptDrops(False) 31 | SymbolWindow.setAutoFillBackground(False) 32 | self.gridLayout = QtGui.QGridLayout(SymbolWindow) 33 | self.gridLayout.setObjectName(_fromUtf8("gridLayout")) 34 | self.verticalLayout = QtGui.QVBoxLayout() 35 | self.verticalLayout.setMargin(0) 36 | self.verticalLayout.setSpacing(10) 37 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 38 | self.horizontalLayout = QtGui.QHBoxLayout() 39 | self.horizontalLayout.setSpacing(10) 40 | self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) 41 | self.gridLayout_2 = QtGui.QGridLayout() 42 | self.gridLayout_2.setSpacing(8) 43 | self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) 44 | self.keyLineEdit = QtGui.QLineEdit(SymbolWindow) 45 | self.keyLineEdit.setMinimumSize(QtCore.QSize(186, 0)) 46 | font = QtGui.QFont() 47 | font.setFamily(_fromUtf8("Segoe UI")) 48 | font.setPointSize(9) 49 | self.keyLineEdit.setFont(font) 50 | self.keyLineEdit.setText(_fromUtf8("")) 51 | self.keyLineEdit.setObjectName(_fromUtf8("keyLineEdit")) 52 | self.gridLayout_2.addWidget(self.keyLineEdit, 1, 1, 1, 1) 53 | self.valueLineEdit = QtGui.QLineEdit(SymbolWindow) 54 | font = QtGui.QFont() 55 | font.setFamily(_fromUtf8("Segoe UI")) 56 | font.setPointSize(9) 57 | self.valueLineEdit.setFont(font) 58 | self.valueLineEdit.setObjectName(_fromUtf8("valueLineEdit")) 59 | self.gridLayout_2.addWidget(self.valueLineEdit, 1, 2, 1, 1) 60 | self.labelWith = QtGui.QLabel(SymbolWindow) 61 | self.labelWith.setObjectName(_fromUtf8("labelWith")) 62 | self.gridLayout_2.addWidget(self.labelWith, 0, 2, 1, 1) 63 | self.labelReplace = QtGui.QLabel(SymbolWindow) 64 | self.labelReplace.setObjectName(_fromUtf8("labelReplace")) 65 | self.gridLayout_2.addWidget(self.labelReplace, 0, 1, 1, 1) 66 | self.tableWidget = QtGui.QTableWidget(SymbolWindow) 67 | self.tableWidget.setEnabled(True) 68 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) 69 | sizePolicy.setHorizontalStretch(0) 70 | sizePolicy.setVerticalStretch(0) 71 | sizePolicy.setHeightForWidth(self.tableWidget.sizePolicy().hasHeightForWidth()) 72 | self.tableWidget.setSizePolicy(sizePolicy) 73 | self.tableWidget.setFocusPolicy(QtCore.Qt.WheelFocus) 74 | self.tableWidget.setFrameShadow(QtGui.QFrame.Plain) 75 | self.tableWidget.setMidLineWidth(0) 76 | self.tableWidget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 77 | self.tableWidget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) 78 | self.tableWidget.setTabKeyNavigation(False) 79 | self.tableWidget.setProperty("showDropIndicator", False) 80 | self.tableWidget.setDragDropOverwriteMode(False) 81 | self.tableWidget.setAlternatingRowColors(True) 82 | self.tableWidget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) 83 | self.tableWidget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) 84 | self.tableWidget.setTextElideMode(QtCore.Qt.ElideRight) 85 | self.tableWidget.setShowGrid(True) 86 | self.tableWidget.setRowCount(2) 87 | self.tableWidget.setColumnCount(2) 88 | self.tableWidget.setObjectName(_fromUtf8("tableWidget")) 89 | item = QtGui.QTableWidgetItem() 90 | self.tableWidget.setItem(0, 0, item) 91 | item = QtGui.QTableWidgetItem() 92 | self.tableWidget.setItem(0, 1, item) 93 | item = QtGui.QTableWidgetItem() 94 | self.tableWidget.setItem(1, 0, item) 95 | item = QtGui.QTableWidgetItem() 96 | self.tableWidget.setItem(1, 1, item) 97 | self.tableWidget.horizontalHeader().setVisible(False) 98 | self.tableWidget.horizontalHeader().setDefaultSectionSize(190) 99 | self.tableWidget.horizontalHeader().setMinimumSectionSize(100) 100 | self.tableWidget.verticalHeader().setVisible(False) 101 | self.gridLayout_2.addWidget(self.tableWidget, 2, 1, 1, 2) 102 | self.horizontalLayout.addLayout(self.gridLayout_2) 103 | self.verticalLayout_2 = QtGui.QVBoxLayout() 104 | self.verticalLayout_2.setContentsMargins(0, -1, -1, -1) 105 | self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) 106 | self.labelBlank = QtGui.QLabel(SymbolWindow) 107 | self.labelBlank.setText(_fromUtf8("")) 108 | self.labelBlank.setObjectName(_fromUtf8("labelBlank")) 109 | self.verticalLayout_2.addWidget(self.labelBlank) 110 | self.addReplaceButton = QtGui.QPushButton(SymbolWindow) 111 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) 112 | sizePolicy.setHorizontalStretch(0) 113 | sizePolicy.setVerticalStretch(0) 114 | sizePolicy.setHeightForWidth(self.addReplaceButton.sizePolicy().hasHeightForWidth()) 115 | self.addReplaceButton.setSizePolicy(sizePolicy) 116 | self.addReplaceButton.setMaximumSize(QtCore.QSize(244, 16777215)) 117 | self.addReplaceButton.setObjectName(_fromUtf8("addReplaceButton")) 118 | self.verticalLayout_2.addWidget(self.addReplaceButton) 119 | self.deleteButton = QtGui.QPushButton(SymbolWindow) 120 | self.deleteButton.setObjectName(_fromUtf8("deleteButton")) 121 | self.verticalLayout_2.addWidget(self.deleteButton) 122 | spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) 123 | self.verticalLayout_2.addItem(spacerItem) 124 | self.horizontalLayout.addLayout(self.verticalLayout_2) 125 | self.verticalLayout.addLayout(self.horizontalLayout) 126 | self.horizontalLayout_2 = QtGui.QHBoxLayout() 127 | self.horizontalLayout_2.setSizeConstraint(QtGui.QLayout.SetDefaultConstraint) 128 | self.horizontalLayout_2.setContentsMargins(-1, 5, -1, 0) 129 | self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) 130 | self.importButton = QtGui.QPushButton(SymbolWindow) 131 | self.importButton.setObjectName(_fromUtf8("importButton")) 132 | self.horizontalLayout_2.addWidget(self.importButton) 133 | self.exportButton = QtGui.QPushButton(SymbolWindow) 134 | self.exportButton.setObjectName(_fromUtf8("exportButton")) 135 | self.horizontalLayout_2.addWidget(self.exportButton) 136 | spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) 137 | self.horizontalLayout_2.addItem(spacerItem1) 138 | self.okButton = QtGui.QPushButton(SymbolWindow) 139 | self.okButton.setObjectName(_fromUtf8("okButton")) 140 | self.horizontalLayout_2.addWidget(self.okButton) 141 | self.cancelButton = QtGui.QPushButton(SymbolWindow) 142 | sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) 143 | sizePolicy.setHorizontalStretch(0) 144 | sizePolicy.setVerticalStretch(0) 145 | sizePolicy.setHeightForWidth(self.cancelButton.sizePolicy().hasHeightForWidth()) 146 | self.cancelButton.setSizePolicy(sizePolicy) 147 | self.cancelButton.setObjectName(_fromUtf8("cancelButton")) 148 | self.horizontalLayout_2.addWidget(self.cancelButton) 149 | self.resetButton = QtGui.QPushButton(SymbolWindow) 150 | self.resetButton.setObjectName(_fromUtf8("resetButton")) 151 | self.horizontalLayout_2.addWidget(self.resetButton) 152 | self.verticalLayout.addLayout(self.horizontalLayout_2) 153 | self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) 154 | 155 | self.retranslateUi(SymbolWindow) 156 | QtCore.QMetaObject.connectSlotsByName(SymbolWindow) 157 | 158 | def retranslateUi(self, SymbolWindow): 159 | SymbolWindow.setWindowTitle(_translate("SymbolWindow", "Insert Symbol Options", None)) 160 | self.labelWith.setText(_translate("SymbolWindow", "With", None)) 161 | self.labelReplace.setText(_translate("SymbolWindow", "Replace", None)) 162 | __sortingEnabled = self.tableWidget.isSortingEnabled() 163 | self.tableWidget.setSortingEnabled(False) 164 | item = self.tableWidget.item(0, 0) 165 | item.setText(_translate("SymbolWindow", "key1", None)) 166 | item = self.tableWidget.item(0, 1) 167 | item.setText(_translate("SymbolWindow", "value1", None)) 168 | item = self.tableWidget.item(1, 0) 169 | item.setText(_translate("SymbolWindow", "key2", None)) 170 | item = self.tableWidget.item(1, 1) 171 | item.setText(_translate("SymbolWindow", "value2", None)) 172 | self.tableWidget.setSortingEnabled(__sortingEnabled) 173 | self.addReplaceButton.setText(_translate("SymbolWindow", "Add", None)) 174 | self.deleteButton.setText(_translate("SymbolWindow", "Delete", None)) 175 | self.importButton.setText(_translate("SymbolWindow", "Import", None)) 176 | self.exportButton.setText(_translate("SymbolWindow", "Export", None)) 177 | self.okButton.setText(_translate("SymbolWindow", "OK", None)) 178 | self.cancelButton.setText(_translate("SymbolWindow", "Cancel", None)) 179 | self.resetButton.setText(_translate("SymbolWindow", "Reset", None)) 180 | 181 | -------------------------------------------------------------------------------- /src/Ui_SymbolWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SymbolWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 500 10 | 450 11 | 12 | 13 | 14 | 15 | 500 16 | 450 17 | 18 | 19 | 20 | false 21 | 22 | 23 | Insert Symbol Options 24 | 25 | 26 | false 27 | 28 | 29 | 30 | 31 | 32 | 10 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | 40 | 10 41 | 42 | 43 | 44 | 45 | 8 46 | 47 | 48 | 49 | 50 | 51 | 186 52 | 0 53 | 54 | 55 | 56 | 57 | Segoe UI 58 | 9 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Segoe UI 71 | 9 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | With 80 | 81 | 82 | 83 | 84 | 85 | 86 | Replace 87 | 88 | 89 | 90 | 91 | 92 | 93 | true 94 | 95 | 96 | 97 | 0 98 | 0 99 | 100 | 101 | 102 | Qt::WheelFocus 103 | 104 | 105 | QFrame::Plain 106 | 107 | 108 | 0 109 | 110 | 111 | Qt::ScrollBarAlwaysOff 112 | 113 | 114 | QAbstractItemView::NoEditTriggers 115 | 116 | 117 | false 118 | 119 | 120 | false 121 | 122 | 123 | false 124 | 125 | 126 | true 127 | 128 | 129 | QAbstractItemView::SingleSelection 130 | 131 | 132 | QAbstractItemView::SelectRows 133 | 134 | 135 | Qt::ElideRight 136 | 137 | 138 | true 139 | 140 | 141 | 2 142 | 143 | 144 | 2 145 | 146 | 147 | false 148 | 149 | 150 | 190 151 | 152 | 153 | 100 154 | 155 | 156 | false 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | key1 165 | 166 | 167 | 168 | 169 | value1 170 | 171 | 172 | 173 | 174 | key2 175 | 176 | 177 | 178 | 179 | value2 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 0 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 0 203 | 0 204 | 205 | 206 | 207 | 208 | 244 209 | 16777215 210 | 211 | 212 | 213 | Add 214 | 215 | 216 | 217 | 218 | 219 | 220 | Delete 221 | 222 | 223 | 224 | 225 | 226 | 227 | Qt::Vertical 228 | 229 | 230 | 231 | 20 232 | 40 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | QLayout::SetDefaultConstraint 245 | 246 | 247 | 5 248 | 249 | 250 | 0 251 | 252 | 253 | 254 | 255 | Import 256 | 257 | 258 | 259 | 260 | 261 | 262 | Export 263 | 264 | 265 | 266 | 267 | 268 | 269 | Qt::Horizontal 270 | 271 | 272 | 273 | 40 274 | 20 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | OK 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 0 291 | 0 292 | 293 | 294 | 295 | Cancel 296 | 297 | 298 | 299 | 300 | 301 | 302 | Reset 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | on_reset_clicked() 316 | on_export_clicked() 317 | on_import_clicked() 318 | on_key_text_changed(QString) 319 | delete_pair_from_list() 320 | add_pair_to_list() 321 | on_cell_selected(int,int) 322 | on_value_text_changed(QString) 323 | on_kv_return_pressed() 324 | 325 | 326 | -------------------------------------------------------------------------------- /src/symbol_window.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file houses the controller for Ui_SymbolWindow, which is the PyQt GUI that 3 | lets users edit the symbol list. 4 | 5 | All symbol edits are performed on a local copy of the list owned by 6 | SymbolWindow (henceforth referred to as the "working list"). No changes are 7 | made to the symbolList in SymbolManager until the 'OK' button is clicked, which 8 | internally triggers a call to SymbolWindow.accept(). 9 | """ 10 | 11 | import aqt 12 | import io 13 | import csv 14 | 15 | from aqt.qt import * 16 | 17 | from .get_version import * 18 | from .symbol_manager import SymbolManager 19 | 20 | PYQT_VER = get_pyqt_version() 21 | 22 | if PYQT_VER == PYQT_VER_4: 23 | from .Ui_SymbolWindow_4 import Ui_SymbolWindow 24 | elif PYQT_VER == PYQT_VER_5: 25 | from .Ui_SymbolWindow_5 import Ui_SymbolWindow 26 | else: 27 | from .Ui_SymbolWindow_6 import Ui_SymbolWindow 28 | 29 | class SymbolWindow(QDialog): 30 | """ 31 | SymbolWindow is a controller for Ui_SymbolWindow. It makes changes to the 32 | working list and updates the GUI in accordance with user input. 33 | 34 | The working list must obey the following rules at all times: 35 | 1. It must be sorted in alphabetical order by key 36 | 2. There must be no duplicate or conflicting keys (ie. keys that are 37 | substrings of one another). 38 | """ 39 | 40 | def __init__(self, parent_widget, symbol_manager): 41 | super(SymbolWindow, self).__init__(parent_widget) 42 | self._sym_manager = symbol_manager 43 | self._working_list = None 44 | self._selected_row = -1 45 | 46 | self.ui = Ui_SymbolWindow() 47 | self.ui.setupUi(self) 48 | 49 | self.ui.importButton.clicked.connect(self.import_list) 50 | self.ui.exportButton.clicked.connect(self.export_list) 51 | self.ui.okButton.clicked.connect(self.accept) 52 | self.ui.cancelButton.clicked.connect(self.reject) 53 | self.ui.resetButton.clicked.connect(self.reset_working_list) 54 | 55 | self.ui.addReplaceButton.clicked.connect(self.add_pair_to_list) 56 | self.ui.deleteButton.clicked.connect(self.delete_pair_from_list) 57 | 58 | # This is the text box labeled 'key': 59 | self.ui.keyLineEdit.textEdited.connect(self.on_key_text_changed) 60 | self.ui.keyLineEdit.returnPressed.connect(self.on_kv_return_pressed) 61 | 62 | # This is the text box labeled 'value': 63 | self.ui.valueLineEdit.textEdited.connect(self.on_value_text_changed) 64 | self.ui.valueLineEdit.returnPressed.connect(self.on_kv_return_pressed) 65 | 66 | self.ui.tableWidget.cellClicked.connect(self.on_cell_clicked) 67 | h_header = self.ui.tableWidget.horizontalHeader() 68 | if PYQT_VER == PYQT_VER_4: 69 | h_header.setResizeMode(0, QHeaderView.ResizeMode.Stretch) 70 | h_header.setResizeMode(1, QHeaderView.ResizeMode.Stretch) 71 | else: 72 | h_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) 73 | h_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) 74 | 75 | 76 | """ Editor State Getters """ 77 | 78 | def _get_key_text(self): 79 | return self.ui.keyLineEdit.text().strip() 80 | 81 | def _get_val_text(self): 82 | return self.ui.valueLineEdit.text()#.strip() 83 | 84 | def is_row_selected(self): 85 | """ Returns true if a row in the tableWidget is selected. """ 86 | return self._selected_row >= 0 87 | 88 | def is_key_valid(self): 89 | text = self._get_key_text() 90 | return bool(text) and SymbolManager.check_if_key_valid(text) 91 | 92 | def is_val_valid(self): 93 | return bool(self._get_val_text()) 94 | 95 | def is_val_different(self): 96 | """ 97 | Checks if the value in the LineEdit is different than the value of the 98 | selected k-v entry. If nothing is selected, return False. 99 | """ 100 | if not self.is_row_selected(): 101 | return False 102 | old = self._working_list[self._selected_row][1] 103 | new = self._get_val_text() 104 | return old != new 105 | 106 | 107 | """ 108 | UI Update Functions 109 | 110 | An add operation (where no existing k-v pair is selected) can occur if: 111 | - The new key is valid 112 | - The new value is valid 113 | 114 | A replace operation (where an existing k-v pair is selected) can occur if: 115 | - The new key is valid 116 | - The new value is valid 117 | - The new value is different from the selected k-v pair's value 118 | """ 119 | 120 | def _on_row_selected(self, row, enable_add_replace): 121 | """ 122 | Called when a row in the tableView is selected, which occurs either 123 | A) when the user types a string into keyLineEdit that matches an 124 | existing key or B) when the user clicks on a cell in the tableView. 125 | 126 | Changes addReplaceButton to replace mode and enables the delete 127 | command. In scenario A addReplaceButton may be enabled if the text in 128 | valueLineEdit is different (so that users may replace the existing 129 | entry), but in scenario B addReplaceButton should NOT be enabled since 130 | both keyLineEdit and valueLineEdit are updated to cell contents. 131 | """ 132 | if self._selected_row == -1: 133 | self.ui.addReplaceButton.setText("Replace") 134 | self.ui.addReplaceButton.clicked.disconnect(self.add_pair_to_list) 135 | self.ui.addReplaceButton.clicked.connect(self.replace_pair_in_list) 136 | 137 | self.ui.addReplaceButton.setEnabled(enable_add_replace) 138 | self.ui.deleteButton.setEnabled(True) 139 | self._selected_row = row 140 | 141 | def _on_row_deselected(self, enable_add_replace): 142 | """ 143 | Called when a row in the tableView is deselected, which occurs either 144 | A) when the user types a string into keyLineEdit that doesn't match any 145 | existing keys or B) when the working list is somehow otherwise updated. 146 | 147 | Changes addReplaceButton to add mode and disables the delete command. 148 | In scenario A addReplaceButton may be enabled if the text in 149 | valueLineEdit is different (so that users may add a new entry), but in 150 | scenario B addReplaceButton should NOT be enabled since both 151 | keyLineEdit and valueLineEdit will be reset. 152 | """ 153 | if self._selected_row != -1: 154 | self.ui.addReplaceButton.setText("Add") 155 | self.ui.addReplaceButton.clicked.disconnect( 156 | self.replace_pair_in_list) 157 | self.ui.addReplaceButton.clicked.connect(self.add_pair_to_list) 158 | 159 | self.ui.addReplaceButton.setEnabled(enable_add_replace) 160 | self.ui.deleteButton.setEnabled(False) 161 | self._selected_row = -1 162 | 163 | def _on_working_list_updated(self): 164 | """ 165 | Called when the working list is updated. Clears keyLineEdit, 166 | valueLineEdit, and deselects any selected rows in the tableView. 167 | """ 168 | self.ui.keyLineEdit.setText("") 169 | self.ui.valueLineEdit.setText("") 170 | 171 | self._on_row_deselected(False) 172 | self._check_table_widget_integrity() 173 | 174 | def _scroll_to_index(self, index): 175 | if len(self._working_list) <= 0: 176 | return 177 | # Scroll to last row if key would be placed at the end 178 | index = min(index, self.ui.tableWidget.rowCount() - 1) 179 | 180 | item = self.ui.tableWidget.item(index, 0) 181 | self.ui.tableWidget.scrollToItem(item, QAbstractItemView.ScrollHint.PositionAtTop) 182 | 183 | def on_key_text_changed(self, current_text): 184 | """ 185 | Called when the text in keyLineEdit is changed. First scrolls the 186 | tableWidget, then updates add/replace and delete buttons. 187 | """ 188 | current_text = current_text.strip() 189 | found, idx = self._find_prospective_index(current_text) 190 | self._scroll_to_index(idx) 191 | 192 | if not self.is_key_valid(): 193 | self._on_row_deselected(False) 194 | else: 195 | # If current_text is not found, this should be an add operation. 196 | # Otherwise, this should be a replace. 197 | if found: 198 | can_replace = self.is_val_valid() and self.is_val_different() 199 | self._on_row_selected(idx, can_replace) 200 | else: 201 | self._on_row_deselected(self.is_val_valid()) 202 | 203 | def on_value_text_changed(self, current_text): 204 | """ 205 | Called when the text in valueLineEdit is changed. Toggles whether 206 | addReplaceButton is clickable. 207 | """ 208 | if self.is_row_selected(): 209 | can_replace = (self.is_key_valid() and self.is_val_valid() 210 | and self.is_val_different()) 211 | self.ui.addReplaceButton.setEnabled(can_replace) 212 | else: 213 | can_add = (self.is_key_valid() and self.is_val_valid()) 214 | self.ui.addReplaceButton.setEnabled(can_add) 215 | 216 | def on_kv_return_pressed(self): 217 | """ 218 | Called when the Enter key is pressed while either keyLineEdit or 219 | valueLineEdit has focus, and performs an add or replace action if 220 | allowed. 221 | """ 222 | if self.is_row_selected(): 223 | can_replace = (self.is_key_valid() and self.is_val_valid() 224 | and self.is_val_different()) 225 | if can_replace: 226 | self.replace_pair_in_list() 227 | else: 228 | can_add = (self.is_key_valid() and self.is_val_valid()) 229 | if can_add: 230 | self.add_pair_to_list() 231 | 232 | def on_cell_clicked(self, row, col): 233 | """ 234 | When a cell in the tableWidget is clicked, update keyLineEdit, 235 | valueLineEdit, and tableWidget to select that key-value pair. 236 | """ 237 | self.ui.keyLineEdit.setText(self.ui.tableWidget.item(row, 0).text()) 238 | self.ui.valueLineEdit.setText(self.ui.tableWidget.item(row, 1).text()) 239 | self._on_row_selected(row, False) 240 | 241 | 242 | """ Protected Actions """ 243 | 244 | def _reload_view(self): 245 | """ 246 | Reloads the entire editor and populates it with the working list. 247 | """ 248 | self.ui.tableWidget.clear() 249 | 250 | count = 0 251 | for k, v in self._working_list: 252 | self.ui.tableWidget.insertRow(count) 253 | self.ui.tableWidget.setItem(count, 0, QTableWidgetItem(k)) 254 | self.ui.tableWidget.setItem(count, 1, QTableWidgetItem(v)) 255 | count += 1 256 | 257 | self.ui.tableWidget.setRowCount(count) 258 | self._on_working_list_updated() 259 | 260 | def _save(self): 261 | """ 262 | Attemps to save the working list. This is the ONLY time where changes 263 | are pushed to SymbolManager. 264 | """ 265 | errors = self._sym_manager.update_and_save_symbol_list( 266 | self._working_list) 267 | 268 | if errors: 269 | if errors[0] == SymbolManager.ERR_INVALID_FORMAT: 270 | aqt.utils.showInfo(self._make_err_str_format(errors, 271 | 'Changes will not be saved', 'Row')) 272 | elif errors[0] == SymbolManager.ERR_KEY_CONFLICT: 273 | aqt.utils.showInfo(self._make_err_str_duplicate(errors[1], 274 | 'Changes will not be saved')) 275 | else: 276 | aqt.utils.showInfo("Error: Invalid key-value list to save. " 277 | "Changes will not be saved.") 278 | return False 279 | return True 280 | 281 | 282 | """ Open & Close Actions """ 283 | 284 | def open(self): 285 | """ Opens the editor and sets up the UI. """ 286 | super(SymbolWindow, self).open() 287 | self._working_list = self._sym_manager.get_list() 288 | self._reload_view() 289 | 290 | def accept(self): 291 | """ Saves changes if possible, then closes the editor. """ 292 | self._save() 293 | super(SymbolWindow, self).accept() 294 | 295 | def reject(self): 296 | """ Closes the editor without saving. """ 297 | old_list = self._sym_manager.get_list() 298 | 299 | if old_list != self._working_list: 300 | confirm_msg = "Close without saving?" 301 | reply = QMessageBox.question(self, 'Message', confirm_msg, 302 | QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) 303 | if reply == QMessageBox.StandardButton.Yes: 304 | super(SymbolWindow, self).reject() 305 | else: 306 | super(SymbolWindow, self).reject() 307 | 308 | 309 | """ 310 | Working List Update Actions 311 | 312 | These functions update the working list and the UI, but does NOT push any 313 | changes back to SymbolManager. 314 | """ 315 | 316 | def _find_prospective_index(self, key): 317 | """ 318 | Checks if the given key exists in the working list. If it does, returns 319 | the index where the key can be found. If it does not, returns the index 320 | where the key would be inserted. 321 | 322 | @return: (key_exists, index) 323 | """ 324 | low, high = 0, len(self._working_list) - 1 325 | mid = 0 326 | 327 | while low <= high: 328 | mid = int((low + high) / 2) 329 | k = self._working_list[mid][0] 330 | 331 | if key == k: 332 | return (True, mid) 333 | elif k < key: 334 | low = mid + 1 335 | mid += 1 336 | else: 337 | high = mid - 1 338 | return (False, mid) 339 | 340 | def add_pair_to_list(self): 341 | """ 342 | Adds an entry to the working list, performs validation, and then 343 | updates the UI. 344 | """ 345 | if self.is_row_selected(): 346 | aqt.utils.showInfo("Error: Cannot add entry when a row is " 347 | "already selected.") 348 | return 349 | 350 | new_key = self._get_key_text() 351 | new_val = self._get_val_text() 352 | 353 | has_conflict = SymbolManager.check_if_key_duplicate(new_key, 354 | self._working_list) 355 | if has_conflict: 356 | aqt.utils.showInfo(("Error: Cannot add '%s' as a key with the same" 357 | " name already exists." % (new_key))) 358 | return 359 | 360 | (_, idx) = self._find_prospective_index(new_key) 361 | self._working_list.insert(idx, (new_key, new_val)) 362 | 363 | self.ui.tableWidget.insertRow(idx) 364 | self.ui.tableWidget.setItem(idx, 0, QTableWidgetItem(new_key)) 365 | self.ui.tableWidget.setItem(idx, 1, QTableWidgetItem(new_val)) 366 | self._on_working_list_updated() 367 | 368 | def replace_pair_in_list(self): 369 | """ Replaces an existing key-value pair from the working list. """ 370 | if not self.is_row_selected(): 371 | aqt.utils.showInfo("Error: Cannot replace when no valid " 372 | "row is selected.") 373 | return 374 | 375 | new_val = self._get_val_text() 376 | old_pair = self._working_list[self._selected_row] 377 | 378 | self._working_list[self._selected_row] = (old_pair[0], new_val) 379 | 380 | widget_item = self.ui.tableWidget.item(self._selected_row, 1) 381 | widget_item.setText(new_val) 382 | self._on_working_list_updated() 383 | 384 | def delete_pair_from_list(self): 385 | """ Deletes an existing key-value pair from the working list. """ 386 | if not self.is_row_selected(): 387 | aqt.utils.showInfo("Error: Cannot delete when no valid " 388 | "row is selected.") 389 | return 390 | 391 | del self._working_list[self._selected_row] 392 | 393 | self.ui.tableWidget.removeRow(self._selected_row) 394 | self._on_working_list_updated() 395 | 396 | def reset_working_list(self): 397 | """ Resets the working list to the default symbol list. """ 398 | confirm_msg = ("Load default symbols? This will delete any " 399 | "unsaved changes!") 400 | reply = QMessageBox.question(self, 'Message', confirm_msg, 401 | QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) 402 | if reply == QMessageBox.StandardButton.Yes: 403 | self._working_list = self._sym_manager.get_default_list() 404 | self._reload_view() 405 | 406 | 407 | """ Import and Export Actions """ 408 | 409 | def import_list(self): 410 | """ 411 | Imports key-value pairs from a .txt file into the editor. The import 412 | procedure is successful only if each and every entry in the .txt file 413 | is valid; otherwise, an error will be displayed and the operation 414 | will abort. 415 | """ 416 | if PYQT_VER == PYQT_VER_4: 417 | fname = QFileDialog.getOpenFileName(self, 'Open file', '', 418 | "CSV (*.csv)") 419 | else: 420 | fname, _ = QFileDialog.getOpenFileName(self, 'Open file', '', 421 | "CSV (*.csv)") 422 | if not fname: 423 | return 424 | 425 | with io.open(fname, 'r', encoding='utf8') as file: 426 | reader = csv.reader(file) 427 | new_list = [] 428 | 429 | for row in reader: 430 | new_list.append(row) 431 | 432 | if self._validate_imported_list(new_list): 433 | # Filter out empty lines before updating the list, but do so 434 | # AFTER error checking so that accurate line numbers will be 435 | # shown during error checking. 436 | new_list = [x for x in new_list if len(x) > 0] 437 | 438 | new_list = sorted(new_list, key=lambda x: x[0]) 439 | self._working_list = new_list 440 | self._reload_view() 441 | 442 | def _validate_imported_list(self, new_list): 443 | """ 444 | Checks that the imported file is valid, and displays an error message 445 | if not. This function takes in a list that DOES contain empty lines 446 | (which will be an empty list) so that accurate line numbers will be 447 | displayed. 448 | """ 449 | errors = SymbolManager.check_format(new_list, ignore_empty=True) 450 | if errors: 451 | aqt.utils.showInfo(self._make_err_str_format(errors, 452 | 'Unable to import', 'Line')) 453 | return False 454 | 455 | errors = SymbolManager.check_for_duplicates(new_list) 456 | if errors: 457 | aqt.utils.showInfo(self._make_err_str_duplicate(errors, 458 | 'Unable to import')) 459 | return False 460 | 461 | return True 462 | 463 | def export_list(self): 464 | """ 465 | Exports the current symbol list into a .txt file. Before exporting, 466 | the list displayed in the editor must match the symbol list stored 467 | in the system. 468 | """ 469 | old_list = self._sym_manager.get_list() 470 | 471 | if old_list != self._working_list: 472 | confirm_msg = "You must save changes before exporting. Save now?" 473 | reply = QMessageBox.question(self, 'Message', confirm_msg, 474 | QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) 475 | if reply == QMessageBox.StandardButton.Yes: 476 | is_success = self._save() 477 | if is_success: 478 | aqt.utils.showInfo('The symbol list has been saved.') 479 | else: 480 | return 481 | else: 482 | return 483 | 484 | if PYQT_VER == PYQT_VER_4: 485 | fname = QFileDialog.getSaveFileName(self, 'Save file', '', 486 | "CSV (*.csv)") 487 | else: 488 | fname, _ = QFileDialog.getSaveFileName(self, 'Save file', '', 489 | "CSV (*.csv)") 490 | if not fname: 491 | return 492 | 493 | with io.open(fname, 'w', newline='\n', encoding='utf-8') as file: 494 | writer = csv.writer(file) 495 | for k, v in self._working_list: 496 | writer.writerow([k, v]) 497 | aqt.utils.showInfo("Symbol list written to: " + fname) 498 | 499 | 500 | """ Error Strings """ 501 | 502 | def _make_err_str_format(self, errors, op_desc, entry_type): 503 | """ 504 | Creates an error message for format errors: 505 | 506 | @param op_desc: Which operation is being performed. 507 | @param entry_type: Either 'Line' or 'Row' 508 | """ 509 | err_str = ("Error: %s due to incorrect format in the following lines " 510 | "(expecting ).\n\n") % op_desc 511 | 512 | for i, string in errors: 513 | err_str += "%s %d: %s\n" % (entry_type, i, string) 514 | return err_str 515 | 516 | def _make_err_str_duplicate(self, errors, op_desc): 517 | """ 518 | Creates an error message for key conflicts. 519 | 520 | @param op_desc: Which operation is being performed. 521 | @param entry_type: Either 'Line' or 'Row' 522 | """ 523 | err_str = ("Error: %s as the following duplicate keys " 524 | "were detected: \n\n" % op_desc) 525 | 526 | for key in errors: 527 | err_str += "%s\n" % key 528 | return err_str 529 | 530 | 531 | """ Validation Functions """ 532 | 533 | def _check_table_widget_integrity(self): 534 | """ 535 | Checks that the tableWidget displays the same items in the same order 536 | as the working list. 537 | """ 538 | wl_len = len(self._working_list) 539 | tw_len = self.ui.tableWidget.rowCount() 540 | 541 | # Checks that tableWidget has same # of entries as the working list: 542 | if wl_len != tw_len: 543 | aqt.utils.showInfo(("Error: working list length %d does not match " 544 | "tableWidget length %d.") % (wl_len, tw_len)) 545 | return 546 | 547 | # Checks that entries in the tableWidget & working list match: 548 | for i in range(wl_len): 549 | tw_k = self.ui.tableWidget.item(i, 0).text() 550 | tw_v = self.ui.tableWidget.item(i, 1).text() 551 | 552 | l_k = self._working_list[i][0] 553 | l_v = self._working_list[i][1] 554 | 555 | k_match = (tw_k == l_k) 556 | v_match = (tw_v == l_v) 557 | 558 | if not k_match or not v_match: 559 | err_str = ("Error: kv pair at row %d does not match.\n" 560 | "List: %s, %s\nWidget: %s, %s") % (i, l_k, l_v, tw_k, tw_v) 561 | aqt.utils.showInfo(err_str) 562 | return 563 | 564 | # Checks that the tableWidget is displaying entries in alphabetical 565 | # order by key: 566 | sorted_list = sorted(self._working_list, key=lambda x: x[0]) 567 | has_error = False 568 | err_str = "" 569 | 570 | for i in range(wl_len): 571 | l_k = self._working_list[i][0] 572 | s_k = sorted_list[i][0] 573 | 574 | if l_k != s_k: 575 | has_error = True 576 | err_str += ("at row %d key is %s, but should be %s\n" 577 | % (i, l_k, s_k)) 578 | 579 | if has_error: 580 | aqt.utils.showInfo("Error: list not alphabetical:" + err_str) 581 | --------------------------------------------------------------------------------