Edit dialog: fix editor buttons on Anki 2.1.50
Also add a few tests for the buttons to make sure that they get actually added and enabled/disabled.
This commit is contained in:
parent
901d92b067
commit
056e722187
100
plugin/edit.py
100
plugin/edit.py
@ -1,5 +1,6 @@
|
||||
import aqt
|
||||
import aqt.editor
|
||||
import aqt.browser.previewer
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import QDialog, Qt, QKeySequence, QShortcut
|
||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom, tooltip
|
||||
@ -24,6 +25,8 @@ from anki.utils import ids2str
|
||||
|
||||
DOMAIN_PREFIX = "foosoft.ankiconnect."
|
||||
|
||||
anki_version = tuple(int(segment) for segment in aqt.appVersion.split("."))
|
||||
|
||||
|
||||
def get_note_by_note_id(note_id):
|
||||
return aqt.mw.col.get_note(note_id)
|
||||
@ -225,8 +228,6 @@ class Edit(aqt.editcurrent.EditCurrent):
|
||||
if changes.note_text and handler is not self.editor:
|
||||
self.reload_notes_after_user_action_elsewhere()
|
||||
|
||||
# adjusting buttons right after initializing doesn't have any effect;
|
||||
# this seems to do the trick
|
||||
def editor_did_load_note(self, _editor):
|
||||
self.enable_disable_next_and_previous_buttons()
|
||||
|
||||
@ -304,29 +305,66 @@ class Edit(aqt.editcurrent.EditCurrent):
|
||||
gui_hooks.editor_did_init.append(self.add_preview_button)
|
||||
gui_hooks.editor_did_init_buttons.append(self.add_right_hand_side_buttons)
|
||||
|
||||
self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self)
|
||||
# on Anki 2.1.50, browser mode makes the Preview button visible
|
||||
extra_kwargs = {} if anki_version < (2, 1, 50) else {
|
||||
"editor_mode": aqt.editor.EditorMode.BROWSER
|
||||
}
|
||||
|
||||
self.editor = aqt.editor.Editor(aqt.mw, self.form.fieldsArea, self,
|
||||
**extra_kwargs)
|
||||
|
||||
gui_hooks.editor_did_init_buttons.remove(self.add_right_hand_side_buttons)
|
||||
gui_hooks.editor_did_init.remove(self.add_preview_button)
|
||||
|
||||
# taken from `setupEditor` of browser.py
|
||||
# PreviewButton calls pycmd `preview`, which is hardcoded.
|
||||
# copying _links is needed so that opening Anki's browser does not
|
||||
# screw them up as they are apparently shared between instances?!
|
||||
# * on Anki < 2.1.50, make the button via js (`setupEditor` of browser.py);
|
||||
# also, make a copy of _links so that opening Anki's browser does not
|
||||
# screw them up as they are apparently shared between instances?!
|
||||
# the last part seems to have been fixed in Anki 2.1.50
|
||||
# * on Anki 2.1.50, the button is created by setting editor mode,
|
||||
# see above; so we only need to add the link.
|
||||
def add_preview_button(self, editor):
|
||||
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.show_preview)
|
||||
|
||||
editor._links = editor._links.copy()
|
||||
editor._links["preview"] = self.show_preview
|
||||
editor.web.eval("""
|
||||
$editorToolbar.then(({notetypeButtons}) =>
|
||||
notetypeButtons.appendButton(
|
||||
{component: editorToolbar.PreviewButton, id: 'preview'}
|
||||
)
|
||||
);
|
||||
""")
|
||||
if anki_version < (2, 1, 50):
|
||||
editor._links = editor._links.copy()
|
||||
editor.web.eval("""
|
||||
$editorToolbar.then(({notetypeButtons}) =>
|
||||
notetypeButtons.appendButton(
|
||||
{component: editorToolbar.PreviewButton, id: 'preview'}
|
||||
)
|
||||
);
|
||||
""")
|
||||
|
||||
editor._links["preview"] = lambda _editor: self.show_preview() and None
|
||||
|
||||
# * on Anki < 2.1.50, button style is okay-ish from get-go,
|
||||
# except when disabled; adding class `btn` fixes that;
|
||||
# * on Anki 2.1.50, buttons have weird font size and are square';
|
||||
# the style below makes them in line with left-hand side buttons
|
||||
def add_right_hand_side_buttons(self, buttons, editor):
|
||||
if anki_version < (2, 1, 50):
|
||||
extra_button_class = "btn"
|
||||
else:
|
||||
extra_button_class = "anki-connect-button"
|
||||
editor.web.eval("""
|
||||
(function(){
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = `
|
||||
.anki-connect-button {
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
padding: 0 2px;
|
||||
font-size: var(--base-font-size);
|
||||
}
|
||||
.anki-connect-button:disabled {
|
||||
pointer-events: none;
|
||||
opacity: .4;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
""")
|
||||
|
||||
def add(cmd, function, label, tip, keys):
|
||||
button_html = editor.addButton(
|
||||
icon=None,
|
||||
@ -338,30 +376,34 @@ class Edit(aqt.editcurrent.EditCurrent):
|
||||
keys=keys,
|
||||
)
|
||||
|
||||
# adding class `btn` properly styles buttons when disabled
|
||||
button_html = button_html.replace('class="', 'class="btn ')
|
||||
button_html = button_html.replace('class="',
|
||||
f'class="{extra_button_class} ')
|
||||
buttons.append(button_html)
|
||||
|
||||
add("browse", self.show_browser, "Browse", "Browse", "Ctrl+F")
|
||||
add("previous", self.show_previous, "<", "Previous", "Alt+Left")
|
||||
add("next", self.show_next, ">", "Next", "Alt+Right")
|
||||
|
||||
def run_javascript_after_toolbar_ready(self, js):
|
||||
js = f"setTimeout(function() {{ {js} }}, 1)"
|
||||
if anki_version < (2, 1, 50):
|
||||
js = f'$editorToolbar.then(({{ toolbar }}) => {js})'
|
||||
else:
|
||||
js = f'require("anki/ui").loaded.then(() => {js})'
|
||||
self.editor.web.eval(js)
|
||||
|
||||
def enable_disable_next_and_previous_buttons(self):
|
||||
def to_js(boolean):
|
||||
return "true" if boolean else "false"
|
||||
|
||||
disable_previous = to_js(not(history.has_note_to_left_of(self.note)))
|
||||
disable_next = to_js(not(history.has_note_to_right_of(self.note)))
|
||||
disable_previous = not(history.has_note_to_left_of(self.note))
|
||||
disable_next = not(history.has_note_to_right_of(self.note))
|
||||
|
||||
self.editor.web.eval(f"""
|
||||
$editorToolbar.then(({{ toolbar }}) => {{
|
||||
setTimeout(function() {{
|
||||
document.getElementById("{DOMAIN_PREFIX}previous")
|
||||
.disabled = {disable_previous};
|
||||
document.getElementById("{DOMAIN_PREFIX}next")
|
||||
.disabled = {disable_next};
|
||||
}}, 1);
|
||||
}});
|
||||
self.run_javascript_after_toolbar_ready(f"""
|
||||
document.getElementById("{DOMAIN_PREFIX}previous")
|
||||
.disabled = {to_js(disable_previous)};
|
||||
document.getElementById("{DOMAIN_PREFIX}next")
|
||||
.disabled = {to_js(disable_next)};
|
||||
""")
|
||||
|
||||
##########################################################################
|
||||
|
@ -1,8 +1,63 @@
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import aqt.operations.note
|
||||
import pytest
|
||||
|
||||
from conftest import get_dialog_instance
|
||||
from plugin.edit import Edit, DecentPreviewer, history
|
||||
from conftest import get_dialog_instance, wait_until
|
||||
from plugin.edit import Edit, DecentPreviewer, history, DOMAIN_PREFIX
|
||||
|
||||
|
||||
NOTHING = object()
|
||||
|
||||
|
||||
class Value:
|
||||
def __init__(self):
|
||||
self.value = NOTHING
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
def has_been_set(self):
|
||||
return self.value is not NOTHING
|
||||
|
||||
|
||||
@dataclass
|
||||
class JavascriptDialogButtonManipulator:
|
||||
dialog: ...
|
||||
|
||||
def eval_js(self, js):
|
||||
evaluation_result = Value()
|
||||
self.dialog.editor.web.evalWithCallback(js, evaluation_result.set)
|
||||
wait_until(evaluation_result.has_been_set)
|
||||
return evaluation_result.value
|
||||
|
||||
def wait_until_toolbar_buttons_are_ready(self):
|
||||
ready_flag = Value()
|
||||
self.dialog.editor._links["set_ready_flag"] = ready_flag.set # noqa
|
||||
self.dialog.run_javascript_after_toolbar_ready("pycmd('set_ready_flag');")
|
||||
wait_until(ready_flag.has_been_set)
|
||||
|
||||
# preview button doesn't have an id, so find by label
|
||||
def click_preview_button(self):
|
||||
self.eval_js("""
|
||||
document.evaluate("//button[text()='Preview']", document)
|
||||
.iterateNext()
|
||||
.click()
|
||||
""")
|
||||
|
||||
def click_button(self, button_id):
|
||||
self.eval_js(f"""
|
||||
document.getElementById("{DOMAIN_PREFIX}{button_id}").click()
|
||||
""")
|
||||
|
||||
def is_button_disabled(self, button_id):
|
||||
return self.eval_js(f"""
|
||||
document.getElementById("{DOMAIN_PREFIX}{button_id}").disabled
|
||||
""")
|
||||
|
||||
|
||||
##############################################################################
|
||||
|
||||
|
||||
def test_edit_dialog_opens(setup):
|
||||
@ -99,6 +154,30 @@ class TestPreviewDialog:
|
||||
assert dialog._should_enable_next() is next_enabled
|
||||
|
||||
|
||||
class TestButtons:
|
||||
@pytest.fixture
|
||||
def manipulator(self, setup):
|
||||
dialog = Edit.open_dialog_and_show_note_with_id(setup.note1_id)
|
||||
return JavascriptDialogButtonManipulator(dialog)
|
||||
|
||||
def test_preview_button_can_be_clicked(self, manipulator, monkeypatch):
|
||||
monkeypatch.setattr(manipulator.dialog, "show_preview", MagicMock())
|
||||
manipulator.wait_until_toolbar_buttons_are_ready()
|
||||
manipulator.click_preview_button()
|
||||
wait_until(lambda: manipulator.dialog.show_preview.call_count == 1)
|
||||
|
||||
def test_addon_buttons_can_be_clicked(self, manipulator):
|
||||
manipulator.wait_until_toolbar_buttons_are_ready()
|
||||
manipulator.click_button(button_id="browse")
|
||||
wait_until(lambda: get_dialog_instance("Browser") is not None)
|
||||
|
||||
def test_addon_buttons_get_disabled_enabled(self, setup, manipulator):
|
||||
Edit.open_dialog_and_show_note_with_id(setup.note2_id)
|
||||
manipulator.wait_until_toolbar_buttons_are_ready()
|
||||
assert manipulator.is_button_disabled("previous") is False
|
||||
assert manipulator.is_button_disabled("next") is True
|
||||
|
||||
|
||||
class TestHistory:
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup(self):
|
||||
|
Loading…
Reference in New Issue
Block a user