diff --git a/AnkiConnect.py b/AnkiConnect.py index 57b662a..a4535ad 100644 --- a/AnkiConnect.py +++ b/AnkiConnect.py @@ -31,6 +31,8 @@ import sys from operator import itemgetter from time import time from unicodedata import normalize +from random import choice +from string import ascii_letters # # Constants @@ -45,6 +47,7 @@ TICK_INTERVAL = 25 URL_TIMEOUT = 10 URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py' +ANKI21 = anki.version.startswith('2.1') # # Helpers @@ -1071,10 +1074,186 @@ class AnkiConnect: @api() - def guiAddCards(self): - addCards = aqt.dialogs.open('AddCards', self.window()) - addCards.activateWindow() + def guiAddCards(self, note=None): + if note is not None: + collection = self.collection() + + deck = collection.decks.byName(note['deckName']) + if deck is None: + raise Exception('deck was not found: {}'.format(note['deckName'])) + + self.collection().decks.select(deck['id']) + savedMid = deck.pop('mid', None) + + model = collection.models.byName(note['modelName']) + if model is None: + raise Exception('model was not found: {}'.format(note['modelName'])) + + self.collection().models.setCurrent(model) + self.collection().models.update(model) + + closeAfterAdding = False + if note is not None and 'options' in note: + if 'closeAfterAdding' in note['options']: + closeAfterAdding = note['options']['closeAfterAdding'] + if type(closeAfterAdding) is not bool: + raise Exception('option parameter \'closeAfterAdding\' must be boolean') + + addCards = None + + if closeAfterAdding: + + randomString = ''.join(choice(ascii_letters) for _ in range(10)) + windowName = 'AddCardsAndClose' + randomString + + if ANKI21: + class AddCardsAndClose(aqt.addcards.AddCards): + + def __init__(self, mw): + # the window must only reset if + # * function `onModelChange` has been called prior + # * window was newly opened + + self.modelHasChanged = True + super().__init__(mw) + + self.addButton.setText("Add and Close") + self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return")) + + def _addCards(self): + super()._addCards() + + # if adding was successful it must mean it was added to the history of the window + if len(self.history): + self.reject() + + def onModelChange(self): + if self.isActiveWindow(): + super().onModelChange() + self.modelHasChanged = True + + def onReset(self, model=None, keep=False): + if self.isActiveWindow() or self.modelHasChanged: + super().onReset(model, keep) + self.modelHasChanged = False + + else: + # modelchoosers text is changed by a reset hook + # therefore we need to change it back manually + self.modelChooser.models.setText(self.editor.note.model()['name']) + self.modelHasChanged = False + + def _reject(self): + savedMarkClosed = aqt.dialogs.markClosed + aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName) + super()._reject() + aqt.dialogs.markClosed = savedMarkClosed + + else: + class AddCardsAndClose(aqt.addcards.AddCards): + + def __init__(self, mw): + self.modelHasChanged = True + super(AddCardsAndClose, self).__init__(mw) + + self.addButton.setText("Add and Close") + self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return")) + + def addCards(self): + super(AddCardsAndClose, self).addCards() + + # if adding was successful it must mean it was added to the history of the window + if len(self.history): + self.reject() + + def onModelChange(self): + if self.isActiveWindow(): + super(AddCardsAndClose, self).onModelChange() + self.modelHasChanged = True + + def onReset(self, model=None, keep=False): + if self.isActiveWindow() or self.modelHasChanged: + super(AddCardsAndClose, self).onReset(model, keep) + self.modelHasChanged = False + + else: + self.modelChooser.models.setText(self.editor.note.model()['name']) + self.modelHasChanged = False + + def reject(self): + savedClose = aqt.dialogs.close + aqt.dialogs.close = lambda _: savedClose(windowName) + super(AddCardsAndClose, self).reject() + aqt.dialogs.close = savedClose + + aqt.dialogs._dialogs[windowName] = [AddCardsAndClose, None] + addCards = aqt.dialogs.open(windowName, self.window()) + + if savedMid: + deck['mid'] = savedMid + + editor = addCards.editor + ankiNote = editor.note + + if 'fields' in note: + for name, value in note['fields'].items(): + if name in ankiNote: + ankiNote[name] = value + editor.loadNote() + + if 'tags' in note: + ankiNote.tags = note['tags'] + editor.updateTags() + + # if Anki does not Focus, the window will not notice that the + # fields are actually filled + aqt.dialogs.open(windowName, self.window()) + if ANKI21: + addCards.setAndFocusNote(editor.note) + + elif note is not None: + currentWindow = aqt.dialogs._dialogs['AddCards'][1] + + def openNewWindow(): + addCards = aqt.dialogs.open('AddCards', self.window()) + + if savedMid: + deck['mid'] = savedMid + + editor = addCards.editor + ankiNote = editor.note + + # we have to fill out the card in the callback + # otherwise we can't assure, the new window is open + if 'fields' in note: + for name, value in note['fields'].items(): + if name in ankiNote: + ankiNote[name] = value + editor.loadNote() + + if 'tags' in note: + ankiNote.tags = note['tags'] + editor.updateTags() + + addCards.activateWindow() + + aqt.dialogs.open('AddCards', self.window()) + if ANKI21: + addCards.setAndFocusNote(editor.note) + + if currentWindow is not None: + if ANKI21: + currentWindow.closeWithCallback(openNewWindow) + else: + currentWindow.reject() + openNewWindow() + else: + openNewWindow() + + else: + addCards = aqt.dialogs.open('AddCards', self.window()) + addCards.activateWindow() @api() def guiReviewActive(self): diff --git a/README.md b/README.md index c98ebbc..343da1a 100644 --- a/README.md +++ b/README.md @@ -1490,13 +1490,33 @@ guarantee that your application continues to function properly in the future. * **guiAddCards** - Invokes the *Add Cards* dialog. + Invokes the *Add Cards* dialog, presets the note using the given deck and model, with the provided field values and tags. + Invoking it multiple times closes the old window and _reopen the window_ with the new provided values. + + The `closeAfterAdding` member inside `options` group can be set to true to create a dialog that closes upon adding the note. + Invoking the action mutliple times with this option will create _multiple windows_. *Sample request*: ```json { "action": "guiAddCards", - "version": 6 + "version": 6, + "params": { + "note": { + "deckName": "Default", + "modelName": "Cloze", + "fields": { + "Text": "The capital of Romania is {{c1::Bucharest}}", + "Extra": "Romania is a country in Europe" + }, + "options": { + "closeAfterAdding": true + }, + "tags": [ + "countries" + ] + } + } } ``` diff --git a/tests/test_graphical.py b/tests/test_graphical.py index 97a68c8..0512f0c 100755 --- a/tests/test_graphical.py +++ b/tests/test_graphical.py @@ -12,6 +12,15 @@ class TestGui(unittest.TestCase): # guiAddCards util.invoke('guiAddCards') + # guiAddCards with preset + util.invoke('createDeck', deck='test') + note = {'deckName': 'test', 'modelName': 'Basic', 'fields': {'Front': 'front1', 'Back': 'back1'}, 'tags': ['tag1']} + util.invoke('guiAddCards', note=note) + + # guiAddCards with preset and closeAfterAdding + noteWithOption = {'deckName': 'test', 'modelName': 'Basic', 'fields': {'Front': 'front1', 'Back': 'back1'}, 'options': { 'closeAfterAdding': True }, 'tags': ['tag1']} + util.invoke('guiAddCards', note=noteWithOption) + # guiCurrentCard # util.invoke('guiCurrentCard')