Anki/ankiqt/ui/cardlist.py
2009-04-06 13:29:24 +09:00

1123 lines
40 KiB
Python

# -*- coding: utf-8 -*-
# Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import sre_constants
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 ankiqt.ui.utils import saveHeader, restoreHeader
from anki.errors import *
from anki.db import *
from anki.stats import CardStats
from anki.hooks import runHook, addHook, removeHook
CARD_ID = 0
CARD_QUESTION = 1
CARD_ANSWER = 2
CARD_DUE = 3
CARD_REPS = 4
CARD_FACTID = 5
CARD_CREATED = 6
CARD_MODIFIED = 7
CARD_INTERVAL = 8
CARD_EASE = 9
CARD_PRIORITY = 10
# 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),
(_("Answer"), self.currentAnswer),
[_("Due"), self.thirdColumn],
]
self.searchStr = ""
self.lastSearch = ""
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:
f = QFont()
f.setPixelSize(self.parent.config['editFontSize'])
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("<br>", u" ")
s = s.replace("<br />", u" ")
s = s.replace("\n", u" ")
s = stripHTML(s)
s = re.sub("\[sound:[^]]+\]", "", s)
s = s.strip()
return QVariant(s)
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])
elif role == Qt.FontRole:
f = QFont()
f.setPixelSize(10)
return QVariant(f)
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
# sorting
if not self.searchStr:
ads = ""
self.lastSearch = ""
else:
if self.searchStr.strip() == self.lastSearch.strip():
# just whitespace
return
QApplication.instance().processEvents()
self.lastSearch = self.searchStr
ids = self.deck.findCards(self.searchStr)
ads = "cards.id in %s" % ids2str(ids)
sort = ""
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, created, modified,
interval, factor, priority 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)
def thirdColumn(self, index):
if self.sortKey == "created":
return self.createdColumn(index)
elif self.sortKey == "modified":
return self.modifiedColumn(index)
elif self.sortKey == "interval":
return self.intervalColumn(index)
elif self.sortKey == "reps":
return self.repsColumn(index)
elif self.sortKey == "factor":
return self.easeColumn(index)
else:
return self.nextDue(index)
def updateHeader(self):
if self.sortKey == "created":
k = _("Created")
elif self.sortKey == "modified":
k = _("Modified")
elif self.sortKey == "interval":
k = _("Interval")
elif self.sortKey == "reps":
k = _("Reps")
elif self.sortKey == "factor":
k = _("Ease")
else:
k = _("Due")
self.columns[-1][0] = k
def createdColumn(self, index):
return time.strftime("%Y-%m-%d", time.localtime(
self.cards[index.row()][CARD_CREATED]))
def modifiedColumn(self, index):
return time.strftime("%Y-%m-%d", time.localtime(
self.cards[index.row()][CARD_MODIFIED]))
def intervalColumn(self, index):
return fmtTimeSpan(
self.cards[index.row()][CARD_INTERVAL]*86400)
def repsColumn(self, index):
return str(self.cards[index.row()][CARD_REPS])
def easeColumn(self, index):
return "%0.2f" % self.cards[index.row()][CARD_EASE]
class StatusDelegate(QItemDelegate):
def __init__(self, parent, model):
QItemDelegate.__init__(self, parent)
self.model = model
def paint(self, painter, option, index):
if len(self.model.cards[index.row()]) == 1:
self.model.updateCard(index)
row = self.model.cards[index.row()]
if row[CARD_PRIORITY] == 0:
# custom render
brush = QBrush(QColor("#ffffaa"))
painter.save()
painter.fillRect(option.rect, brush)
painter.restore()
return QItemDelegate.paint(self, painter, option, index)
class EditDeck(QMainWindow):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.parent = parent
self.deck = self.parent.deck
self.config = parent.config
self.forceClose = False
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.setItemDelegate(StatusDelegate(self, self.model))
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(130)
self.dialog.tagList.clear()
self.dialog.tagList.addItems(QStringList(
[_('<Tag filter>'), _('No tags')] + self.alltags))
self.dialog.tagList.view().setFixedWidth(200)
def drawSort(self):
self.sortList = [
_("Question"),
_("Answer"),
_("Created"),
_("Modified"),
_("Due"),
_("Interval"),
_("Reps"),
_("Ease"),
]
self.sortFields = sorted(self.deck.allFields())
self.sortList.extend([_("'%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.rebuildSortIndex(self.sortKey)
self.sortIndex = idx
if idx <= 7:
self.config['sortIndex'] = idx
self.model.sortKey = self.sortKey
self.model.updateHeader()
if refresh:
self.model.showMatching()
self.updateFilterLabel()
self.onEvent()
self.focusCurrentCard()
def rebuildSortIndex(self, key):
if key not in (
"question", "answer", "created", "modified", "due", "interval",
"reps", "factor"):
return
old = self.deck.s.scalar("select sql from sqlite_master where name = :k",
k="ix_cards_sort")
if old and key in old:
return
self.parent.setProgressParent(self)
self.deck.startProgress(2)
self.deck.updateProgress(_("Building Index..."))
self.deck.s.statement("drop index if exists ix_cards_sort")
self.deck.updateProgress()
if key in ("question", "answer"):
key = key + " collate nocase"
self.deck.s.statement(
"create index ix_cards_sort on cards (%s)" % key)
self.deck.s.statement("analyze")
self.deck.finishProgress()
self.parent.setProgressParent(None)
def tagChanged(self, idx):
if idx == 0:
self.dialog.filterEdit.setText("")
elif idx == 1:
self.dialog.filterEdit.setText("tag:none")
else:
self.dialog.filterEdit.setText("tag:" + self.alltags[idx-2].lower())
self.showFilterNow()
self.dialog.tagList.setCurrentIndex(0)
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 = 300
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.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().show()
restoreHeader(self.dialog.tableView.horizontalHeader(), "editor")
self.dialog.tableView.verticalHeader().setDefaultSectionSize(
self.parent.config['editLineSize'])
for i in range(2):
self.dialog.tableView.horizontalHeader().setResizeMode(i, QHeaderView.Stretch)
self.dialog.tableView.horizontalHeader().setResizeMode(2, QHeaderView.Interactive)
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.actionCram, SIGNAL("triggered()"), self.cram)
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.actionInvertSelection, SIGNAL("triggered()"), self.invertSelection)
self.connect(self.dialog.actionReverseOrder, SIGNAL("triggered()"), self.reverseOrder)
self.connect(self.dialog.actionSelectFacts, SIGNAL("triggered()"), self.selectFacts)
self.connect(self.dialog.actionFindReplace, SIGNAL("triggered()"), self.onFindReplace)
# 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.forceClose:
if not self.factValid:
ui.utils.showInfo(_(
"Some fields are missing or not unique."),
parent=self, help="AddItems#AddError")
return
self.editor.setFact(None)
self.editor.close()
saveGeom(self, "editor")
saveHeader(self.dialog.tableView.horizontalHeader(), "editor")
self.hide()
ui.dialogs.close("CardList")
self.parent.moveToState("auto")
self.teardownUndo()
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
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
mods = self.deck.s.column0("""
select distinct modelId from facts
where id in %s""" % ids2str(sf))
if not len(mods) == 1:
ui.utils.showInfo(
_("Can only operate on one model at a time."),
parent=self)
return
# get cards to enable
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
# for each fact id, generate
n = _("Generate Cards")
self.parent.setProgressParent(self)
self.deck.startProgress()
self.deck.setUndoStart(n)
facts = self.deck.s.query(Fact).filter(
text("id in %s" % ids2str(sf))).order_by(Fact.created).all()
self.deck.updateProgress(_("Generating Cards..."))
for c, fact in enumerate(facts):
self.deck.addCards(fact, d.selectedCms)
if c % 50 == 0:
self.deck.updateProgress()
self.deck.flushMod()
self.deck.updateAllPriorities()
self.deck.finishProgress()
self.parent.setProgressParent(None)
self.deck.setUndoEnd(n)
self.updateSearch()
self.updateAfterCardChange()
def cram(self):
if ui.utils.askUser(
_("Cram selected cards in new deck?"),
help="CramMode",
parent=self):
self.close()
self.parent.onCram(self.selectedCards())
def onChangeModel(self):
sf = self.selectedFacts()
mods = self.deck.s.column0("""
select distinct modelId from facts
where id in %s""" % ids2str(sf))
if not len(mods) == 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_()
if d.ret:
n = _("Change Model")
self.parent.setProgressParent(self)
self.deck.setUndoStart(n)
self.deck.changeModel(sf, *d.ret)
self.deck.setUndoEnd(n)
self.parent.setProgressParent(None)
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 setupUndo(self):
addHook("postUndoRedo", self.postUndoRedo)
def teardownUndo(self):
removeHook("postUndoRedo", self.postUndoRedo)
def postUndoRedo(self):
self.updateFilterLabel()
self.updateSearch()
self.updateAfterCardChange()
def onUndo(self):
self.deck.undo()
def onRedo(self):
self.deck.redo()
# Edit: replacing
######################################################################
def onFindReplace(self):
sf = self.selectedFacts()
if not sf:
return
mods = self.deck.s.column0("""
select distinct modelId from facts
where id in %s""" % ids2str(sf))
if not len(mods) == 1:
ui.utils.showInfo(
_("Can only operate on one model at a time."),
parent=self)
return
d = QDialog(self)
frm = ankiqt.forms.findreplace.Ui_Dialog()
frm.setupUi(d)
fields = sorted(self.currentCard.fact.model.fieldModels, key=attrgetter("name"))
frm.field.addItems(QStringList(
[_("All Fields")] + [f.name for f in fields]))
self.connect(frm.buttonBox, SIGNAL("helpRequested()"),
self.onFindReplaceHelp)
if not d.exec_():
return
n = _("Find and Replace")
self.parent.setProgressParent(self)
self.deck.startProgress(2)
self.deck.updateProgress(_("Replacing..."))
self.deck.setUndoStart(n)
self.deck.updateProgress()
changed = None
try:
if frm.field.currentIndex() == 0:
field = None
else:
field = fields[frm.field.currentIndex()-1].id
changed = self.deck.findReplace(sf,
unicode(frm.find.text()),
unicode(frm.replace.text()),
frm.re.isChecked(),
field)
except sre_constants.error:
ui.utils.showInfo(_("Invalid regular expression."),
parent=self)
self.deck.setUndoEnd(n)
self.deck.finishProgress()
self.parent.setProgressParent(None)
self.parent.reset()
self.updateSearch()
self.updateAfterCardChange()
if changed is not None:
ui.utils.showInfo(_("%(a)d of %(b)d facts updated") % {
'a': changed,
'b': len(sf),
}, parent=self)
def onFindReplaceHelp(self):
QDesktopServices.openUrl(QUrl(ankiqt.appWiki +
"Browser#FindReplace"))
# Jumping
######################################################################
def onFirstCard(self):
if not self.model.cards:
return
self.editor.saveFieldsNow()
self.dialog.tableView.selectionModel().clear()
self.dialog.tableView.selectRow(0)
def onLastCard(self):
if not self.model.cards:
return
self.editor.saveFieldsNow()
self.dialog.tableView.selectionModel().clear()
self.dialog.tableView.selectRow(len(self.model.cards) - 1)
def onPreviousCard(self):
if not self.model.cards:
return
self.editor.saveFieldsNow()
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
self.editor.saveFieldsNow()
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 + "Browser"))
# 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 +
"Browser#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
self.pauseUpdate = False
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)
indices = {}
for i, x in enumerate(src):
l.addWidget(QLabel(_("Change %s to:") % x.name), i, 0)
cb = QComboBox()
cb.addItems(qtargets)
idx = min(i, len(targets)-1)
cb.setCurrentIndex(idx)
indices[cb] = idx
self.connect(cb, SIGNAL("currentIndexChanged(int)"),
lambda i, cb=cb, key=key: self.onComboChanged(i, cb, key))
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)
setattr(self, key + "Indices", indices)
def rebuildFieldMap(self):
return self.rebuildTemplateMap(key="field", attr="fieldModels")
def onComboChanged(self, i, cb, key):
indices = getattr(self, key + "Indices")
if self.pauseUpdate:
indices[cb] = i
return
combos = getattr(self, key + "Combos")
if i == cb.count() - 1:
# set to 'nothing'
return
# find another combo with same index
for c in combos:
if c == cb:
continue
if c.currentIndex() == i:
self.pauseUpdate = True
c.setCurrentIndex(indices[cb])
self.pauseUpdate = False
break
indices[cb] = i
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 +
"Browser#ChangeModel"))