# -*- 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, addHook CARD_ID = 0 CARD_QUESTION = 1 CARD_ANSWER = 2 CARD_DUE = 3 CARD_REPS = 4 CARD_FACTID = 5 # 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.cards = [] self.deleted = {} # Model interface ###################################################################### def rowCount(self, index): return len(self.cards) def columnCount(self, index): return 3 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: if len(self.cards[index.row()]) == 1: # not cached yet self.updateCard(index) 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 parseSearch(self): search = self.searchStr d = {'str': [], 'tag': [], } for elem in search.split(): if len(elem) > 2 and elem.startswith("t:"): d['tag'].append(elem[2:]) else: d['str'].append(elem) return d def showMatching(self): if not self.sortKey: self.cards = [] return search = self.parseSearch() # text search textLimit = "" if search['str']: ids = None for s in search['str']: i = self.deck.s.column0( "select factId from fields where value like :s", s="%"+s+"%") if not ids: ids = set(i) else: ids.intersection_update(i) if not ids: break if not ids: ids = [] textLimit = "cards.factId in %s" % ids2str(ids) # tags tagLimit = "" if search['tag']: ids = None if "none" in search['tag']: search['tag'].remove("none") ids = set(self.deck.cardsWithNoTags()) if search['tag']: def find(tag, tags): if tag.startswith('"'): # direct match return findTag(tag.replace('"', ""), parseTags(tags)) else: return tag.lower() in tags.lower() for tag in search['tag']: like = "%" + tag.replace('"', "") + "%" i = [id for (id, tags, pri) in self.deck.tagsList( where=""" and (facts.tags like :s or models.tags like :s or cardModels.name like :s)""", kwargs = {'s': like}) if find(tag, tags)] if not ids: ids = set(i) else: ids.intersection_update(i) if not ids: ids = [] tagLimit = "cards.id in %s" % ids2str(ids) # sorting sort = "" ads = [] if textLimit: ads.append(textLimit) if tagLimit: ads.append(tagLimit) 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 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 " "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, 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 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()][CARD_QUESTION] def currentAnswer(self, index): return self.cards[index.row()][CARD_ANSWER] def nextDue(self, index): d = self.cards[index.row()][CARD_DUE] reps = self.cards[index.row()][CARD_REPS] 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) restoreGeom(self, "editor") restoreSplitter(self.dialog.splitter, "editor") # 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.setupUndo() self.setupEditor() self.setupCardInfo() self.dialog.filterEdit.setFocus() ui.dialogs.open("CardList", self) self.drawTags() self.updateFilterLabel() 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.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.setFixedWidth(120) self.dialog.tagList.clear() self.dialog.tagList.addItems(QStringList( [_('