mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
1372 lines
50 KiB
Python
1372 lines
50 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, saveState, restoreState
|
|
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_NO = 10
|
|
CARD_PRIORITY = 11
|
|
CARD_TAGS = 12
|
|
|
|
# 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)
|
|
if role == Qt.TextAlignmentRole and index.column() == 2:
|
|
return QVariant(Qt.AlignHCenter)
|
|
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, force=True):
|
|
if not self.sortKey:
|
|
self.cards = []
|
|
return
|
|
# sorting
|
|
if not self.searchStr:
|
|
ads = ""
|
|
self.lastSearch = ""
|
|
else:
|
|
if (self.searchStr.strip() == self.lastSearch.strip()
|
|
and not force):
|
|
# 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
|
|
if self.sortKey == "fact":
|
|
sort = "order by facts.created, cards.created"
|
|
else:
|
|
sort = "order by cards." + self.sortKey
|
|
if self.sortKey in ("question", "answer"):
|
|
sort += " collate nocase"
|
|
if self.sortKey == "fact":
|
|
query = """
|
|
select cards.id from cards, facts
|
|
where cards.factId = facts.id """
|
|
if ads:
|
|
query += "and " + ads + " "
|
|
else:
|
|
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.deck.getInt('reverseOrder'):
|
|
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, noCount, priority, (select tags from facts where
|
|
facts.id = cards.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:
|
|
# 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)
|
|
elif self.sortKey == "noCount":
|
|
return self.noColumn(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")
|
|
elif self.sortKey == "noCount":
|
|
k = _("Lapses")
|
|
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]
|
|
|
|
def noColumn(self, index):
|
|
return "%d" % self.cards[index.row()][CARD_NO]
|
|
|
|
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] == -3:
|
|
# custom render
|
|
if index.row() % 2 == 0:
|
|
brush = QBrush(QColor("#ffffcc"))
|
|
else:
|
|
brush = QBrush(QColor("#ffffaa"))
|
|
painter.save()
|
|
painter.fillRect(option.rect, brush)
|
|
painter.restore()
|
|
if row[CARD_PRIORITY] == 0:
|
|
# custom render
|
|
if index.row() % 2 == 0:
|
|
brush = QBrush(QColor("#ffcccc"))
|
|
else:
|
|
brush = QBrush(QColor("#ffaaaa"))
|
|
painter.save()
|
|
painter.fillRect(option.rect, brush)
|
|
painter.restore()
|
|
elif "Marked" in row[CARD_TAGS]:
|
|
if index.row() % 2 == 0:
|
|
brush = QBrush(QColor("#ccccff"))
|
|
else:
|
|
brush = QBrush(QColor("#aaaaff"))
|
|
painter.save()
|
|
painter.fillRect(option.rect, brush)
|
|
painter.restore()
|
|
return QItemDelegate.paint(self, painter, option, index)
|
|
|
|
class EditDeck(QMainWindow):
|
|
|
|
def __init__(self, parent):
|
|
if parent.config['standaloneWindows']:
|
|
windParent = None
|
|
else:
|
|
windParent = parent
|
|
QMainWindow.__init__(self, windParent)
|
|
self.parent = parent
|
|
self.deck = self.parent.deck
|
|
self.config = parent.config
|
|
self.forceClose = False
|
|
self.origModTime = parent.deck.modified
|
|
self.currentRow = None
|
|
self.lastFilter = ""
|
|
self.dialog = ankiqt.forms.cardlist.Ui_MainWindow()
|
|
self.dialog.setupUi(self)
|
|
self.setUnifiedTitleAndToolBarOnMac(True)
|
|
restoreGeom(self, "editor")
|
|
restoreState(self, "editor")
|
|
restoreSplitter(self.dialog.splitter, "editor")
|
|
# toolbar
|
|
self.dialog.toolBar.setIconSize(QSize(self.config['iconSize'],
|
|
self.config['iconSize']))
|
|
self.dialog.toolBar.toggleViewAction().setText(_("Toggle Toolbar"))
|
|
# 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.setShowGrid(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))
|
|
if self.deck.getInt("reverseOrder"):
|
|
self.dialog.actionReverseOrder.setChecked(True)
|
|
self.updateFont()
|
|
self.setupMenus()
|
|
self.setupFilter()
|
|
self.setupSort()
|
|
self.setupHeaders()
|
|
self.setupHooks()
|
|
self.setupEditor()
|
|
self.setupCardInfo()
|
|
self.dialog.filterEdit.setFocus()
|
|
ui.dialogs.open("CardList", self)
|
|
self.drawTags()
|
|
self.updateFilterLabel()
|
|
self.show()
|
|
if self.parent.currentCard:
|
|
self.currentCard = self.parent.currentCard
|
|
self.updateSearch()
|
|
if sys.platform.startswith("darwin"):
|
|
self.macCloseShortcut = QShortcut(QKeySequence("Ctrl+w"), self)
|
|
self.connect(self.macCloseShortcut, SIGNAL("activated()"),
|
|
self.close)
|
|
|
|
def findCardInDeckModel(self):
|
|
for i, thisCard in enumerate(self.model.cards):
|
|
if thisCard[0] == self.currentCard.id:
|
|
return i
|
|
return -1
|
|
|
|
def updateFont(self):
|
|
self.dialog.tableView.setFont(QFont(
|
|
self.config['editFontFamily'],
|
|
self.config['editFontSize']))
|
|
self.dialog.tableView.verticalHeader().setDefaultSectionSize(
|
|
self.parent.config['editLineSize'])
|
|
self.model.reset()
|
|
|
|
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.deck.getInt("sortIndex") or 0
|
|
self.drawSort()
|
|
self.connect(self.dialog.sortBox, SIGNAL("activated(int)"),
|
|
self.sortChanged)
|
|
self.sortChanged(self.sortIndex, refresh=False)
|
|
|
|
def drawTags(self):
|
|
self.dialog.tagList.view().setFixedWidth(200)
|
|
self.dialog.tagList.setMaxVisibleItems(30)
|
|
self.dialog.tagList.setFixedWidth(130)
|
|
self.dialog.tagList.clear()
|
|
alltags = [None, "Marked", None, None, "Leech", None, None]
|
|
# system tags
|
|
self.dialog.tagList.addItem(_("<Filter>"))
|
|
self.dialog.tagList.addItem(QIcon(":/icons/rating.png"),
|
|
_('Marked'))
|
|
self.dialog.tagList.addItem(QIcon(":/icons/media-playback-pause.png"),
|
|
_('Suspended'))
|
|
self.dialog.tagList.addItem(QIcon(":/icons/chronometer.png"),
|
|
_('Due'))
|
|
self.dialog.tagList.addItem(QIcon(":/icons/emblem-important.png"),
|
|
_('Leech'))
|
|
self.dialog.tagList.addItem(QIcon(":/icons/editclear.png"),
|
|
_('No fact tags'))
|
|
self.dialog.tagList.insertSeparator(
|
|
self.dialog.tagList.count())
|
|
# model and card templates
|
|
for (type, sql, icon) in (
|
|
("models", "select tags from models", "contents.png"),
|
|
("cms", "select name from cardModels", "Anki_Card.png")):
|
|
d = {}
|
|
tagss = self.deck.s.column0(sql)
|
|
for tags in tagss:
|
|
for tag in parseTags(tags):
|
|
d[tag] = 1
|
|
sortedtags = sorted(d.keys())
|
|
alltags.extend(sortedtags)
|
|
icon = QIcon(":/icons/" + icon)
|
|
for t in sortedtags:
|
|
self.dialog.tagList.addItem(icon, t.replace("_", " "))
|
|
if sortedtags:
|
|
self.dialog.tagList.insertSeparator(
|
|
self.dialog.tagList.count())
|
|
alltags.append(None)
|
|
# fact tags
|
|
alluser = sorted(self.deck.allTags())
|
|
for tag in alltags:
|
|
try:
|
|
alluser.remove(tag)
|
|
except:
|
|
pass
|
|
icon = QIcon(":/icons/Anki_Fact.png")
|
|
for t in alluser:
|
|
t = t.replace("_", " ")
|
|
self.dialog.tagList.addItem(icon, t)
|
|
alltags.extend(alluser)
|
|
self.alltags = alltags
|
|
|
|
def drawSort(self):
|
|
self.sortList = [
|
|
_("Question"),
|
|
_("Answer"),
|
|
_("Created"),
|
|
_("Modified"),
|
|
_("Due"),
|
|
_("Interval"),
|
|
_("Reps"),
|
|
_("Ease"),
|
|
_("Fact Created"),
|
|
_("Lapses"),
|
|
]
|
|
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"
|
|
elif idx == 8:
|
|
self.sortKey = "fact"
|
|
elif idx == 9:
|
|
self.sortKey = "noCount"
|
|
else:
|
|
self.sortKey = ("field", self.sortFields[idx-10])
|
|
self.rebuildSortIndex(self.sortKey)
|
|
self.sortIndex = idx
|
|
self.deck.setVar('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", "noCount"):
|
|
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:
|
|
filter = ""
|
|
elif idx == 1:
|
|
filter = "tag:marked"
|
|
elif idx == 2:
|
|
filter = "is:suspended"
|
|
elif idx == 3:
|
|
filter = "is:due"
|
|
elif idx == 4:
|
|
filter = "tag:leech"
|
|
elif idx == 5:
|
|
filter = "tag:none"
|
|
else:
|
|
filter = "tag:" + self.alltags[idx]
|
|
self.lastFilter = filter
|
|
self.dialog.filterEdit.setText(filter)
|
|
self.showFilterNow()
|
|
|
|
def updateFilterLabel(self):
|
|
selected = len(self.dialog.tableView.selectionModel().selectedRows())
|
|
self.setWindowTitle(ngettext("Browser (%(cur)d "
|
|
"of %(tot)d card shown; %(sel)s)", "Browser (%(cur)d "
|
|
"of %(tot)d cards shown; %(sel)s)", self.deck.cardCount) %
|
|
{
|
|
"cur": len(self.model.cards),
|
|
"tot": self.deck.cardCount,
|
|
"sel": ngettext("%d selected", "%d selected", selected) % selected
|
|
} + " - " + self.parent.deck.name())
|
|
|
|
def onEvent(self, type='field'):
|
|
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)
|
|
if type == "tag":
|
|
self.drawTags()
|
|
|
|
def filterTextChanged(self):
|
|
interval = 300
|
|
# update filter dropdown
|
|
if (self.lastFilter.lower()
|
|
not in unicode(self.dialog.filterEdit.text()).lower()):
|
|
self.dialog.tagList.setCurrentIndex(0)
|
|
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()"),
|
|
lambda: self.updateSearch(force=False))
|
|
|
|
def showFilterNow(self):
|
|
if self.filterTimer:
|
|
self.filterTimer.stop()
|
|
self.updateSearch()
|
|
|
|
def updateSearch(self, force=True):
|
|
if self.parent.inDbHandler:
|
|
return
|
|
self.model.searchStr = unicode(self.dialog.filterEdit.text())
|
|
self.model.showMatching(force)
|
|
self.updateFilterLabel()
|
|
self.onEvent()
|
|
self.filterTimer = None
|
|
if self.model.cards:
|
|
self.dialog.cardInfoGroup.show()
|
|
self.dialog.fieldsArea.show()
|
|
else:
|
|
self.dialog.cardInfoGroup.hide()
|
|
self.dialog.fieldsArea.hide()
|
|
if not self.focusCurrentCard():
|
|
if self.model.cards:
|
|
self.dialog.tableView.selectRow(0)
|
|
if not self.model.cards:
|
|
self.editor.setFact(None)
|
|
|
|
def focusCurrentCard(self):
|
|
if self.currentCard:
|
|
try:
|
|
self.currentCard.id
|
|
except:
|
|
return False
|
|
currentCardIndex = self.findCardInDeckModel()
|
|
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)
|
|
return True
|
|
return False
|
|
|
|
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")
|
|
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.actionAddItems, SIGNAL("triggered()"), self.parent.onAddCard)
|
|
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)
|
|
self.connect(self.dialog.actionToggleSuspend, SIGNAL("triggered(bool)"), self.onSuspend)
|
|
self.connect(self.dialog.actionToggleMark, SIGNAL("triggered(bool)"), self.onMark)
|
|
# edit
|
|
self.connect(self.dialog.actionFont, SIGNAL("triggered()"), self.onFont)
|
|
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)
|
|
self.connect(self.dialog.actionTags, SIGNAL("triggered()"), self.onTags)
|
|
self.connect(self.dialog.actionSort, SIGNAL("triggered()"), self.onSort)
|
|
# 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")
|
|
saveState(self, "editor")
|
|
saveHeader(self.dialog.tableView.horizontalHeader(), "editor")
|
|
self.hide()
|
|
ui.dialogs.close("CardList")
|
|
if self.parent.currentCard:
|
|
self.parent.moveToState("showQuestion")
|
|
else:
|
|
self.parent.moveToState("auto")
|
|
self.teardownHooks()
|
|
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()
|
|
self.updateToggles()
|
|
|
|
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.drawTags()
|
|
self.parent.moveToState("auto")
|
|
|
|
# Menu options
|
|
######################################################################
|
|
|
|
def deleteCards(self):
|
|
cards = self.selectedCards()
|
|
n = _("Delete Cards")
|
|
new = self.findCardInDeckModel() + 1
|
|
self.dialog.tableView.setFocus()
|
|
self.deck.setUndoStart(n)
|
|
self.deck.deleteCards(cards)
|
|
self.deck.setUndoEnd(n)
|
|
new = min(max(0, new), len(self.model.cards) - 1)
|
|
self.dialog.tableView.selectRow(new)
|
|
self.updateSearch()
|
|
self.updateAfterCardChange()
|
|
|
|
def addTags(self, tags=None, label=None):
|
|
if tags is None:
|
|
(tags, r) = ui.utils.getTag(self, self.deck, _("Enter tags to add:"))
|
|
if label is None:
|
|
label = _("Add Tags")
|
|
if tags:
|
|
self.parent.setProgressParent(self)
|
|
self.deck.setUndoStart(label)
|
|
self.deck.addTags(self.selectedFacts(), tags)
|
|
self.deck.setUndoEnd(label)
|
|
self.parent.setProgressParent(None)
|
|
self.updateAfterCardChange(reset=True)
|
|
|
|
def deleteTags(self, tags=None, label=None):
|
|
if tags is None:
|
|
(tags, r) = ui.utils.getTag(self, self.deck, _("Enter tags to delete:"))
|
|
if label is None:
|
|
label = _("Delete Tags")
|
|
if tags:
|
|
self.parent.setProgressParent(self)
|
|
self.deck.setUndoStart(label)
|
|
self.deck.deleteTags(self.selectedFacts(), tags)
|
|
self.deck.setUndoEnd(label)
|
|
self.parent.setProgressParent(None)
|
|
self.updateAfterCardChange(reset=True)
|
|
|
|
def updateToggles(self):
|
|
self.dialog.actionToggleSuspend.setChecked(self.isSuspended())
|
|
self.dialog.actionToggleMark.setChecked(self.isMarked())
|
|
|
|
def isSuspended(self):
|
|
return self.currentCard and self.currentCard.priority == -3
|
|
|
|
def onSuspend(self, sus):
|
|
if sus:
|
|
self._onSuspend()
|
|
else:
|
|
self._onUnsuspend()
|
|
|
|
def _onSuspend(self):
|
|
n = _("Suspend")
|
|
self.parent.setProgressParent(self)
|
|
self.deck.setUndoStart(n)
|
|
self.deck.suspendCards(self.selectedCards())
|
|
self.deck.setUndoEnd(n)
|
|
self.parent.setProgressParent(None)
|
|
self.updateAfterCardChange(reset=True)
|
|
|
|
def _onUnsuspend(self):
|
|
n = _("Unsuspend")
|
|
self.parent.setProgressParent(self)
|
|
self.deck.setUndoStart(n)
|
|
self.deck.unsuspendCards(self.selectedCards())
|
|
self.deck.setUndoEnd(n)
|
|
self.parent.setProgressParent(None)
|
|
self.updateAfterCardChange(reset=True)
|
|
|
|
def isMarked(self):
|
|
return self.currentCard and "Marked" in self.currentCard.fact.tags
|
|
|
|
def onMark(self, mark):
|
|
if mark:
|
|
self._onMark()
|
|
else:
|
|
self._onUnmark()
|
|
|
|
def _onMark(self):
|
|
self.addTags(tags="Marked", label=_("Toggle Mark"))
|
|
|
|
def _onUnmark(self):
|
|
self.deleteTags(tags="Marked", label=_("Toggle Mark"))
|
|
|
|
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.rebuildCounts(full=False)
|
|
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):
|
|
self.deck.startProgress()
|
|
sm = self.dialog.tableView.selectionModel()
|
|
sm.blockSignals(True)
|
|
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)
|
|
if i % 100 == 0:
|
|
self.deck.updateProgress()
|
|
sm.blockSignals(False)
|
|
self.deck.finishProgress()
|
|
|
|
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):
|
|
self.deck.setVar("reverseOrder", not self.deck.getInt("reverseOrder"))
|
|
self.model.cards.reverse()
|
|
self.model.reset()
|
|
self.focusCurrentCard()
|
|
|
|
# Edit: undo/redo
|
|
######################################################################
|
|
|
|
def setupHooks(self):
|
|
addHook("postUndoRedo", self.postUndoRedo)
|
|
addHook("currentCardDeleted", self.updateSearch)
|
|
|
|
def teardownHooks(self):
|
|
removeHook("postUndoRedo", self.postUndoRedo)
|
|
removeHook("currentCardDeleted", self.updateSearch)
|
|
|
|
def postUndoRedo(self):
|
|
self.updateFilterLabel()
|
|
self.updateSearch()
|
|
self.updateAfterCardChange()
|
|
|
|
def onUndo(self):
|
|
self.deck.undo()
|
|
|
|
def onRedo(self):
|
|
self.deck.redo()
|
|
|
|
# Edit: font
|
|
######################################################################
|
|
|
|
def onFont(self):
|
|
d = QDialog(self)
|
|
frm = ankiqt.forms.editfont.Ui_Dialog()
|
|
frm.setupUi(d)
|
|
frm.fontCombo.setCurrentFont(QFont(
|
|
self.parent.config['editFontFamily']))
|
|
frm.fontSize.setValue(self.parent.config['editFontSize'])
|
|
frm.lineSize.setValue(self.parent.config['editLineSize'])
|
|
if d.exec_():
|
|
self.parent.config['editFontFamily'] = (
|
|
unicode(frm.fontCombo.currentFont().family()))
|
|
self.parent.config['editFontSize'] = (
|
|
int(frm.fontSize.value()))
|
|
self.parent.config['editLineSize'] = (
|
|
int(frm.lineSize.value()))
|
|
self.updateFont()
|
|
|
|
# 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(ngettext("%(a)d of %(b)d fact updated", "%(a)d of %(b)d facts updated", len(sf)) % {
|
|
'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()
|
|
self.dialog.filterEdit.selectAll()
|
|
|
|
def onFact(self):
|
|
self.editor.focusFirst()
|
|
|
|
def onTags(self):
|
|
self.dialog.tagList.setFocus()
|
|
|
|
def onSort(self):
|
|
self.dialog.sortBox.setFocus()
|
|
|
|
# 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.origModel = self.parent.deck.currentModel
|
|
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 reject(self):
|
|
self.parent.deck.currentModel = self.origModel
|
|
self.modelChooser.deinit()
|
|
return QDialog.reject(self)
|
|
|
|
def accept(self):
|
|
saveGeom(self, "changeModel")
|
|
self.parent.deck.currentModel = self.origModel
|
|
# check maps
|
|
fmap = self.getFieldMap()
|
|
cmap = self.getTemplateMap()
|
|
def any(l):
|
|
for x in l:
|
|
if x:
|
|
return True
|
|
return False
|
|
if not cmap or (self.targetModel != self.oldModel and
|
|
not fmap):
|
|
return ui.utils.showInfo(
|
|
_("Targets must be unique."), parent=self)
|
|
if not any(cmap.values()):
|
|
return ui.utils.showInfo(
|
|
_("Must map at least one template."), parent=self)
|
|
if self.targetModel == self.oldModel:
|
|
self.ret = (self.targetModel, None, cmap)
|
|
return QDialog.accept(self)
|
|
self.modelChooser.deinit()
|
|
self.ret = (self.targetModel, fmap, cmap)
|
|
return QDialog.accept(self)
|
|
|
|
def onHelp(self):
|
|
QDesktopServices.openUrl(QUrl(ankiqt.appWiki +
|
|
"Browser#ChangeModel"))
|