# -*- coding: utf-8 -*- # Copyright: Damien Elmes # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html from PyQt4.QtGui import * from PyQt4.QtCore import * import time, types, sys, re from operator import attrgetter import anki, anki.utils, ankiqt.forms from ankiqt import ui from anki.cards import cardsTable, Card from anki.facts import factsTable, fieldsTable, Fact from anki.utils import fmtTimeSpan, parseTags, findTag, addTags, deleteTags, \ stripHTML, ids2str from ankiqt.ui.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter from anki.errors import * from anki.db import * from anki.stats import CardStats from anki.hooks import runHook # Deck editor ########################################################################## class DeckModel(QAbstractTableModel): def __init__(self, parent, deck): QAbstractTableModel.__init__(self) self.parent = parent self.deck = deck self.filterTag = None self.sortKey = None # column title, display accessor, sort attr self.columns = [("Question", self.currentQuestion, self.currentQuestion), ("Answer", self.currentAnswer, self.currentAnswer), (" "*10 + "Due" + " "*10, self.nextDue, "nextTime")] self.searchStr = "" self.tag = None self.cards = [] self.deleted = {} # Model interface ###################################################################### def rowCount(self, index): return len(self.cards) def columnCount(self, index): return len(self.columns) def data(self, index, role): if not index.isValid(): return QVariant() if role == Qt.FontRole and index.column() == 2: f = QFont() f.setPixelSize(12) return QVariant(f) elif role == Qt.DisplayRole or role == Qt.EditRole: s = self.columns[index.column()][1](index) s = s.replace("
", u" ") s = s.replace("\n", u" ") s = stripHTML(s) s = re.sub("\[sound:[^]]+\]", "", s) s = s.strip() return QVariant(s) elif role == Qt.SizeHintRole: if index.column() == 2: return QVariant(20) else: return QVariant() def headerData(self, section, orientation, role): if orientation == Qt.Vertical: return QVariant() elif role == Qt.DisplayRole: return QVariant(self.columns[section][0]) else: return QVariant() def flags(self, index): return Qt.ItemFlag(Qt.ItemIsEnabled | Qt.ItemIsSelectable) # Filtering ###################################################################### def showMatching(self): if not self.sortKey: self.cards = [] return # searching searchLimit = "" if self.searchStr: searchLimit = "cards.factId in (%s)" % ( ",".join([str(x) for x in self.deck.s.column0( "select factId from fields where value like :val", val="%" + self.searchStr + "%")])) # tags tagLimit = "" if self.tag: if self.tag == "notag": tagLimit = "cards.id in %s" % ids2str(self.deck.cardsWithNoTags()) else: tagLimit = "cards.id in %s" % ids2str( [id for (id, tags, pri) in self.deck.tagsList() if findTag(self.tag, parseTags(tags))]) # sorting sort = "" ads = [] if searchLimit: ads.append(searchLimit) if tagLimit: ads.append(tagLimit) if not self.parent.config['showSuspendedCards']: ads.append("cards.priority != 0") ads = " and ".join(ads) if isinstance(self.sortKey, types.StringType): # card property sort = "order by cards." + self.sortKey if self.sortKey in ("question", "answer"): sort += " collate nocase" query = ("select id, priority, question, answer, due, " "reps, factId from cards ") if ads: query += "where %s " % ads query += sort else: # field value ret = self.deck.s.all( "select id, numeric from fieldModels where name = :name", name=self.sortKey[1]) fields = ",".join([str(x[0]) for x in ret]) # if multiple models have the same field, use the first numeric bool numeric = ret[0][1] if numeric: order = "cast(fields.value as real)" else: order = "fields.value collate nocase" if ads: ads = " and " + ads query = ("select cards.id, cards.priority, cards.question, " "cards.answer, cards.due, cards.reps, cards.factId " "from fields, cards where fields.fieldModelId in (%s) " "and fields.factId = cards.factId" + ads + " order by cards.ordinal, %s") % (fields, order) # run the query self.cards = self.deck.s.all(query) if self.parent.config['editorReverseOrder']: self.cards.reverse() self.reset() def updateCard(self, index): try: self.cards[index.row()] = self.deck.s.first(""" select id, priority, question, answer, due, reps, factId from cards where id = :id""", id=self.cards[index.row()][0]) self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, self.index(index.row(), 1)) except IndexError: # called after search changed pass # Tools ###################################################################### def getCardID(self, index): return self.cards[index.row()][0] def getCard(self, index): try: return self.deck.s.query(Card).get(self.getCardID(index)) except IndexError: return None def isDeleted(self, id): return id in self.deleted def cardIndex(self, card): "Return the index of CARD, if currently displayed." return self.cards.index(card) def currentQuestion(self, index): return self.cards[index.row()][2] def currentAnswer(self, index): return self.cards[index.row()][3] def nextDue(self, index): d = self.cards[index.row()][4] reps = self.cards[index.row()][5] secs = d - time.time() if secs <= 0: if not reps: return _("(new card)") else: return _("%s ago") % fmtTimeSpan(abs(secs), pad=0) else: return _("in %s") % fmtTimeSpan(secs, pad=0) class EditDeck(QMainWindow): def __init__(self, parent): QDialog.__init__(self, parent, Qt.Window) self.parent = parent self.deck = self.parent.deck self.config = parent.config self.origModTime = parent.deck.modified self.currentRow = None self.dialog = ankiqt.forms.cardlist.Ui_MainWindow() self.dialog.setupUi(self) # flush all changes before we load self.deck.s.flush() self.model = DeckModel(self.parent, self.parent.deck) self.dialog.tableView.setSortingEnabled(False) self.dialog.tableView.setModel(self.model) self.dialog.tableView.selectionModel() self.connect(self.dialog.tableView.selectionModel(), SIGNAL("selectionChanged(QItemSelection,QItemSelection)"), self.updateFilterLabel) self.dialog.tableView.setFont(QFont( self.config['editFontFamily'], self.config['editFontSize'])) if self.parent.config['editorReverseOrder']: self.dialog.actionReverseOrder.setChecked(True) self.setupMenus() self.setupFilter() self.setupSort() self.setupHeaders() self.setupEditor() self.setupCardInfo() self.dialog.filterEdit.setFocus() ui.dialogs.open("CardList", self) self.drawTags() self.updateFilterLabel() restoreGeom(self, "editor") restoreSplitter(self.dialog.splitter, "editor") self.show() self.updateSearch() if self.parent.currentCard: self.currentCard = self.parent.currentCard self.focusCurrentCard() def findCardInDeckModel(self, model, card): for i, thisCard in enumerate(model.cards): if thisCard[0] == card.id: return i return -1 def setupFilter(self): self.filterTimer = None self.currentTag = None self.connect(self.dialog.filterEdit, SIGNAL("textChanged(QString)"), self.filterTextChanged) self.connect(self.dialog.filterEdit, SIGNAL("returnPressed()"), self.showFilterNow) self.setTabOrder(self.dialog.filterEdit, self.dialog.tableView) self.connect(self.dialog.tagList, SIGNAL("activated(int)"), self.tagChanged) def setupSort(self): self.dialog.sortBox.setMaxVisibleItems(30) self.sortIndex = self.config['sortIndex'] self.drawSort() self.connect(self.dialog.sortBox, SIGNAL("activated(int)"), self.sortChanged) self.sortChanged(self.sortIndex, refresh=False) def drawTags(self): self.dialog.tagList.setMaxVisibleItems(30) tags = self.deck.allTags() self.alltags = tags self.alltags.sort() self.dialog.tagList.clear() self.dialog.tagList.addItems(QStringList( [_('All tags'), _('No tags')] + self.alltags)) if self.currentTag: try: idx = self.alltags.index(self.currentTag) + 2 except ValueError: idx = 0 self.dialog.tagList.setCurrentIndex(idx) def drawSort(self): self.sortList = [ _("Question"), _("Answer"), _("Created"), _("Modified"), _("Due"), _("Interval"), _("Reps"), _("Ease"), ] self.sortFields = sorted(self.deck.allFields()) self.sortList.extend([_("Field '%s'") % f for f in self.sortFields]) self.dialog.sortBox.clear() self.dialog.sortBox.addItems(QStringList(self.sortList)) if self.sortIndex >= len(self.sortList): self.sortIndex = 0 self.dialog.sortBox.setCurrentIndex(self.sortIndex) def sortChanged(self, idx, refresh=True): if idx == 0: self.sortKey = "question" elif idx == 1: self.sortKey = "answer" elif idx == 2: self.sortKey = "created" elif idx == 3: self.sortKey = "modified" elif idx == 4: self.sortKey = "due" elif idx == 5: self.sortKey = "interval" elif idx == 6: self.sortKey = "reps" elif idx == 7: self.sortKey = "factor" else: self.sortKey = ("field", self.sortFields[idx-8]) self.sortIndex = idx if idx <= 7: self.config['sortIndex'] = idx self.model.sortKey = self.sortKey if refresh: self.model.showMatching() self.updateFilterLabel() self.onEvent() self.focusCurrentCard() def tagChanged(self, idx): if idx == 0: self.currentTag = None elif idx == 1: self.currentTag = "notag" else: self.currentTag = self.alltags[idx-2] self.updateSearch() def updateFilterLabel(self): self.setWindowTitle(_("Editor (%(cur)d " "of %(tot)d cards shown; %(sel)d selected)") % { "cur": len(self.model.cards), "tot": self.deck.cardCount, "sel": len(self.dialog.tableView.selectionModel().selectedRows()) }) def onEvent(self): if self.deck.undoAvailable(): self.dialog.actionUndo.setText(_("Undo %s") % self.deck.undoName()) self.dialog.actionUndo.setEnabled(True) else: self.dialog.actionUndo.setEnabled(False) if self.deck.redoAvailable(): self.dialog.actionRedo.setText(_("Redo %s") % self.deck.redoName()) self.dialog.actionRedo.setEnabled(True) else: self.dialog.actionRedo.setEnabled(False) # update list if self.currentRow and self.model.cards: self.model.updateCard(self.currentRow) def filterTextChanged(self): interval = 500 if self.filterTimer: self.filterTimer.setInterval(interval) else: self.filterTimer = QTimer(self) self.filterTimer.setSingleShot(True) self.filterTimer.start(interval) self.connect(self.filterTimer, SIGNAL("timeout()"), self.updateSearch) def showFilterNow(self): if self.filterTimer: self.filterTimer.stop() self.updateSearch() def updateSearch(self): idx = self.dialog.tableView.currentIndex() self.model.searchStr = unicode(self.dialog.filterEdit.text()) self.model.tag = self.currentTag self.model.showMatching() self.updateFilterLabel() self.onEvent() self.filterTimer = None if self.model.cards: self.dialog.cardInfoGroup.show() self.dialog.fieldsArea.show() self.dialog.tableView.selectionModel().setCurrentIndex( self.model.index(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) else: self.dialog.cardInfoGroup.hide() self.dialog.fieldsArea.hide() self.dialog.tableView.selectRow(idx.row()) self.dialog.tableView.scrollTo(idx, QAbstractItemView.PositionAtCenter) def focusCurrentCard(self): if self.currentCard: currentCardIndex = self.findCardInDeckModel( self.model, self.currentCard) if currentCardIndex >= 0: sm = self.dialog.tableView.selectionModel() sm.clear() self.dialog.tableView.selectRow(currentCardIndex) self.dialog.tableView.scrollTo( self.model.index(currentCardIndex,0), self.dialog.tableView.PositionAtCenter) def setupHeaders(self): if not sys.platform.startswith("win32"): self.dialog.tableView.verticalHeader().hide() self.dialog.tableView.horizontalHeader().hide() for i in range(2): self.dialog.tableView.horizontalHeader().setResizeMode(i, QHeaderView.Stretch) self.dialog.tableView.horizontalHeader().setResizeMode(2, QHeaderView.ResizeToContents) def setupMenus(self): # actions self.connect(self.dialog.actionDelete, SIGNAL("triggered()"), self.deleteCards) self.connect(self.dialog.actionAddTag, SIGNAL("triggered()"), self.addTags) self.connect(self.dialog.actionDeleteTag, SIGNAL("triggered()"), self.deleteTags) self.connect(self.dialog.actionReschedule, SIGNAL("triggered()"), self.reschedule) self.connect(self.dialog.actionAddCards, SIGNAL("triggered()"), self.addCards) self.connect(self.dialog.actionChangeModel, SIGNAL("triggered()"), self.onChangeModel) # edit self.connect(self.dialog.actionUndo, SIGNAL("triggered()"), self.onUndo) self.connect(self.dialog.actionRedo, SIGNAL("triggered()"), self.onRedo) self.connect(self.dialog.actionSelectFacts, SIGNAL("triggered()"), self.selectFacts) self.connect(self.dialog.actionInvertSelection, SIGNAL("triggered()"), self.invertSelection) self.connect(self.dialog.actionReverseOrder, SIGNAL("triggered()"), self.reverseOrder) # jumps self.connect(self.dialog.actionFirstCard, SIGNAL("triggered()"), self.onFirstCard) self.connect(self.dialog.actionLastCard, SIGNAL("triggered()"), self.onLastCard) self.connect(self.dialog.actionPreviousCard, SIGNAL("triggered()"), self.onPreviousCard) self.connect(self.dialog.actionNextCard, SIGNAL("triggered()"), self.onNextCard) self.connect(self.dialog.actionFind, SIGNAL("triggered()"), self.onFind) self.connect(self.dialog.actionFact, SIGNAL("triggered()"), self.onFact) # help self.connect(self.dialog.actionGuide, SIGNAL("triggered()"), self.onHelp) runHook('editor.setupMenus', self) def onClose(self): saveSplitter(self.dialog.splitter, "editor") self.editor.saveFieldsNow() if not self.factValid: ui.utils.showInfo(_( "Some fields are missing or not unique."), parent=self, help="AddItems#AddError") return self.editor.setFact(None) saveGeom(self, "editor") self.hide() ui.dialogs.close("CardList") self.parent.moveToState("auto") return True def closeEvent(self, evt): if self.onClose(): evt.accept() else: evt.ignore() def keyPressEvent(self, evt): "Show answer on RET or register answer." if evt.key() in (Qt.Key_Escape,): self.close() # Editor ###################################################################### def setupEditor(self): self.editor = ui.facteditor.FactEditor(self, self.dialog.fieldsArea, self.deck) self.factValid = True self.editor.onFactValid = self.onFactValid self.editor.onFactInvalid = self.onFactInvalid self.editor.onChange = self.onEvent self.connect(self.dialog.tableView.selectionModel(), SIGNAL("currentRowChanged(QModelIndex, QModelIndex)"), self.rowChanged) def onFactValid(self, fact): self.factValid = True self.dialog.tableView.setEnabled(True) self.dialog.filterEdit.setEnabled(True) self.dialog.sortBox.setEnabled(True) self.dialog.tagList.setEnabled(True) self.dialog.menubar.setEnabled(True) self.dialog.cardInfoGroup.setEnabled(True) def onFactInvalid(self, fact): self.factValid = False self.dialog.tableView.setEnabled(False) self.dialog.filterEdit.setEnabled(False) self.dialog.sortBox.setEnabled(False) self.dialog.tagList.setEnabled(False) self.dialog.menubar.setEnabled(False) self.dialog.cardInfoGroup.setEnabled(False) def rowChanged(self, current, previous): self.currentRow = current self.currentCard = self.model.getCard(current) if not self.currentCard: self.editor.setFact(None, True) return self.deck.s.flush() self.deck.s.refresh(self.currentCard) self.deck.s.refresh(self.currentCard.fact) fact = self.currentCard.fact self.editor.setFact(fact, True) self.showCardInfo(self.currentCard) self.onEvent() def setupCardInfo(self): self.currentCard = None self.cardStats = CardStats(self.deck, None) def showCardInfo(self, card): self.cardStats.card = self.currentCard self.dialog.cardLabel.setText( self.cardStats.report()) # Menu helpers ###################################################################### def selectedCards(self): return [self.model.cards[idx.row()][0] for idx in self.dialog.tableView.selectionModel().selectedRows()] def selectedFacts(self): return self.deck.s.column0(""" select distinct factId from cards where id in (%s)""" % ",".join([ str(self.model.cards[idx.row()][0]) for idx in self.dialog.tableView.selectionModel().selectedRows()])) def selectedFactsAsCards(self): return self.deck.s.column0( "select id from cards where factId in (%s)" % ",".join([str(s) for s in self.selectedFacts()])) def updateAfterCardChange(self, reset=False): "Refresh info like stats on current card" self.currentRow = self.dialog.tableView.currentIndex() self.rowChanged(self.currentRow, None) if reset: self.updateSearch() self.parent.moveToState("auto") # Menu options ###################################################################### def deleteCards(self): cards = self.selectedCards() n = _("Delete Cards") self.deck.setUndoStart(n) self.deck.deleteCards(cards) self.deck.setUndoEnd(n) self.updateSearch() self.updateAfterCardChange() def addTags(self): (tags, r) = ui.utils.getTag(self, self.deck, _("Enter tags to add:")) if tags: n = _("Add Tags") self.deck.setUndoStart(n) self.deck.addTags(self.selectedFacts(), tags) self.deck.setUndoEnd(n) self.updateAfterCardChange() def deleteTags(self): (tags, r) = ui.utils.getTag(self, self.deck, _("Enter tags to delete:")) if tags: n = _("Delete Tags") self.deck.setUndoStart(n) self.deck.deleteTags(self.selectedFacts(), tags) self.deck.setUndoEnd(n) self.updateAfterCardChange() def reschedule(self): n = _("Reschedule") d = QDialog(self) frm = ankiqt.forms.reschedule.Ui_Dialog() frm.setupUi(d) if not d.exec_(): return self.deck.setUndoStart(n) try: if frm.asNew.isChecked(): self.deck.resetCards(self.selectedCards()) else: try: min = float(str(frm.rangeMin.text())) max = float(str(frm.rangeMax.text())) except ValueError: ui.utils.showInfo( _("Please enter a valid start and end range."), parent=self) return self.deck.rescheduleCards(self.selectedCards(), min, max) finally: self.deck.rebuildQueue() self.deck.setUndoEnd(n) self.updateAfterCardChange(reset=True) def addCards(self): sf = self.selectedFacts() if not sf: return cms = [x.id for x in self.deck.s.query(Fact).get(sf[0]).\ model.cardModels] d = AddCardChooser(self, cms) if not d.exec_(): return n = _("Generate Cards") self.deck.setUndoStart(n) for id in sf: self.deck.addCards(self.deck.s.query(Fact).get(id), d.selectedCms) self.deck.flushMod() self.deck.updateAllPriorities() self.deck.setUndoEnd(n) self.updateSearch() self.updateAfterCardChange() def onChangeModel(self): sf = self.selectedFacts() cms = self.deck.s.column0(""" select distinct modelId from facts where id in %s""" % ids2str(sf)) if not len(cms) == 1: ui.utils.showInfo( _("Can only change one model at a time."), parent=self) return d = ChangeModelDialog(self, self.currentCard.fact.model, self.currentCard.cardModel) d.exec_() self.parent.setProgressParent(self) if d.ret: n = _("Change Model") self.deck.setUndoStart(n) self.deck.changeModel(sf, *d.ret) self.deck.setUndoEnd(n) self.updateSearch() self.updateAfterCardChange() # Edit: selection ###################################################################### def selectFacts(self): sm = self.dialog.tableView.selectionModel() cardIds = dict([(x, 1) for x in self.selectedFactsAsCards()]) for i, card in enumerate(self.model.cards): if card.id in cardIds: sm.select(self.model.index(i, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) def invertSelection(self): sm = self.dialog.tableView.selectionModel() items = sm.selection() self.dialog.tableView.selectAll() sm.select(items, QItemSelectionModel.Deselect | QItemSelectionModel.Rows) def reverseOrder(self): if self.parent.config['editorReverseOrder']: self.parent.config['editorReverseOrder'] = False; else: self.parent.config['editorReverseOrder'] = True; self.model.cards.reverse() self.model.reset() self.focusCurrentCard() # Edit: undo/redo ###################################################################### def onUndo(self): self.deck.undo() self.updateFilterLabel() self.updateSearch() self.updateAfterCardChange() def onRedo(self): self.deck.redo() self.updateFilterLabel() self.updateSearch() self.updateAfterCardChange() # Jumping ###################################################################### def onFirstCard(self): if not self.model.cards: return self.dialog.tableView.selectionModel().clear() self.dialog.tableView.selectRow(0) def onLastCard(self): if not self.model.cards: return self.dialog.tableView.selectionModel().clear() self.dialog.tableView.selectRow(len(self.model.cards) - 1) def onPreviousCard(self): if not self.model.cards: return row = self.dialog.tableView.currentIndex().row() row = max(0, row - 1) self.dialog.tableView.selectionModel().clear() self.dialog.tableView.selectRow(row) def onNextCard(self): if not self.model.cards: return row = self.dialog.tableView.currentIndex().row() row = min(len(self.model.cards) - 1, row + 1) self.dialog.tableView.selectionModel().clear() self.dialog.tableView.selectRow(row) def onFind(self): self.dialog.filterEdit.setFocus() def onFact(self): self.editor.focusFirst() # Help ###################################################################### def onHelp(self): QDesktopServices.openUrl(QUrl(ankiqt.appWiki + "Editor")) # Generate card dialog ###################################################################### class AddCardChooser(QDialog): def __init__(self, parent, cms): QDialog.__init__(self, parent, Qt.Window) self.parent = parent self.cms = cms self.dialog = ankiqt.forms.addcardmodels.Ui_Dialog() self.dialog.setupUi(self) self.connect(self.dialog.buttonBox, SIGNAL("helpRequested()"), self.onHelp) self.displayCards() restoreGeom(self, "addCardModels") def displayCards(self): self.cms = self.parent.deck.s.all(""" select id, name, active from cardModels where id in %s order by ordinal""" % ids2str(self.cms)) self.items = [] for cm in self.cms: item = QListWidgetItem(cm[1], self.dialog.list) self.dialog.list.addItem(item) self.items.append(item) idx = self.dialog.list.indexFromItem(item) if cm[2]: mode = QItemSelectionModel.Select else: mode = QItemSelectionModel.Deselect self.dialog.list.selectionModel().select(idx, mode) def accept(self): self.selectedCms = [] for i, item in enumerate(self.items): idx = self.dialog.list.indexFromItem(item) if self.dialog.list.selectionModel().isSelected(idx): self.selectedCms.append(self.cms[i][0]) saveGeom(self, "addCardModels") QDialog.accept(self) def onHelp(self): QDesktopServices.openUrl(QUrl(ankiqt.appWiki + "Editor#GenerateCards")) # Change model dialog ###################################################################### class ChangeModelDialog(QDialog): def __init__(self, parent, oldModel, oldTemplate): QDialog.__init__(self, parent, Qt.Window) self.parent = parent self.oldModel = oldModel self.oldTemplate = oldTemplate self.form = ankiqt.forms.changemodel.Ui_Dialog() self.form.setupUi(self) # maps self.fieldMapWidget = None self.fieldMapLayout = QHBoxLayout() self.form.fieldMap.setLayout(self.fieldMapLayout) self.templateMapWidget = None self.templateMapLayout = QHBoxLayout() self.form.templateMap.setLayout(self.templateMapLayout) # model chooser self.parent.deck.currentModel = oldModel self.form.oldModelLabel.setText(self.oldModel.name) self.modelChooser = ui.modelchooser.ModelChooser(self, self.parent, self.parent.deck, self.modelChanged, cards=False, label=False) self.form.modelChooserWidget.setLayout(self.modelChooser) self.modelChooser.models.setFocus() self.connect(self.form.buttonBox, SIGNAL("helpRequested()"), self.onHelp) restoreGeom(self, "changeModel") self.modelChanged(self.oldModel) self.ret = None def modelChanged(self, model): self.targetModel = model # just changing template? self.form.fieldMap.setEnabled(self.targetModel != self.oldModel) self.rebuildTemplateMap() self.rebuildFieldMap() def rebuildTemplateMap(self, key=None, attr=None): if not key: key = "template" attr = "cardModels" map = getattr(self, key + "MapWidget") lay = getattr(self, key + "MapLayout") src = getattr(self.oldModel, attr) dst = getattr(self.targetModel, attr) if map: lay.removeWidget(map) map.deleteLater() setattr(self, key + "MapWidget", None) map = QWidget() l = QGridLayout() combos = [] targets = [x.name for x in dst] + [_("Nothing")] qtargets = QStringList(targets) for i, x in enumerate(src): l.addWidget(QLabel(_("Change %s to:") % x.name), i, 0) cb = QComboBox() cb.addItems(qtargets) cb.setCurrentIndex(min(i, len(targets)-1)) combos.append(cb) l.addWidget(cb, i, 1) map.setLayout(l) lay.addWidget(map) setattr(self, key + "MapWidget", map) setattr(self, key + "MapLayout", lay) setattr(self, key + "Combos", combos) def rebuildFieldMap(self): return self.rebuildTemplateMap(key="field", attr="fieldModels") def getTemplateMap(self, old=None, combos=None, new=None): if not old: old = self.oldModel.cardModels combos = self.templateCombos new = self.targetModel.cardModels map = {} for i, f in enumerate(old): idx = combos[i].currentIndex() if idx == len(new): # ignore map[f] = None else: f2 = new[idx] if f2 in map.values(): return None map[f] = f2 return map def getFieldMap(self): return self.getTemplateMap( old=self.oldModel.fieldModels, combos=self.fieldCombos, new=self.targetModel.fieldModels) def accept(self): saveGeom(self, "changeModel") # check maps fmap = self.getFieldMap() cmap = self.getTemplateMap() if not cmap or (self.targetModel != self.oldModel and not fmap): return ui.utils.showInfo( _("Targets must be unique."), parent=self) if self.targetModel == self.oldModel: self.ret = (self.targetModel, None, cmap) return QDialog.accept(self) self.ret = (self.targetModel, fmap, cmap) return QDialog.accept(self) def onHelp(self): QDesktopServices.openUrl(QUrl(ankiqt.appWiki + "Editor#ChangeModel"))