├── .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 | 
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 | 
25 |
26 | This should bring up the following window:
27 |
28 | 
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 |
--------------------------------------------------------------------------------