2012-12-21 08:51:59 +01:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
# Copyright: Damien Elmes <anki@ichi2.net>
|
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
import sre_constants
|
2017-12-11 07:20:00 +01:00
|
|
|
|
import html
|
2013-05-03 10:52:46 +02:00
|
|
|
|
import time
|
|
|
|
|
import re
|
2012-12-22 01:17:10 +01:00
|
|
|
|
from operator import itemgetter
|
2013-10-03 17:07:11 +02:00
|
|
|
|
from anki.lang import ngettext
|
2017-08-08 08:28:53 +02:00
|
|
|
|
import json
|
2013-05-03 10:52:46 +02:00
|
|
|
|
|
|
|
|
|
from aqt.qt import *
|
|
|
|
|
import anki
|
|
|
|
|
import aqt.forms
|
2017-12-04 03:20:56 +01:00
|
|
|
|
from anki.utils import fmtTimeSpan, ids2str, stripHTMLMedia, htmlToTextLine, \
|
|
|
|
|
isWin, intTime, \
|
|
|
|
|
isMac, isLin, bodyClass
|
2012-12-21 08:51:59 +01:00
|
|
|
|
from aqt.utils import saveGeom, restoreGeom, saveSplitter, restoreSplitter, \
|
|
|
|
|
saveHeader, restoreHeader, saveState, restoreState, applyStyles, getTag, \
|
2017-08-11 11:06:29 +02:00
|
|
|
|
showInfo, askUser, tooltip, openHelp, showWarning, shortcut, mungeQA, \
|
2017-08-15 10:41:36 +02:00
|
|
|
|
getOnlyText, MenuList, SubMenu
|
2017-09-06 05:02:00 +02:00
|
|
|
|
from anki.hooks import runHook, addHook, remHook, runFilter
|
2012-12-21 08:51:59 +01:00
|
|
|
|
from aqt.webview import AnkiWebView
|
|
|
|
|
from anki.consts import *
|
2017-12-04 03:45:22 +01:00
|
|
|
|
from anki.sound import playFromText, clearAudioQueue, allSounds, play
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Data model
|
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
|
|
class DataModel(QAbstractTableModel):
|
|
|
|
|
|
|
|
|
|
def __init__(self, browser):
|
|
|
|
|
QAbstractTableModel.__init__(self)
|
|
|
|
|
self.browser = browser
|
|
|
|
|
self.col = browser.col
|
|
|
|
|
self.sortKey = None
|
|
|
|
|
self.activeCols = self.col.conf.get(
|
|
|
|
|
"activeCols", ["noteFld", "template", "cardDue", "deck"])
|
|
|
|
|
self.cards = []
|
|
|
|
|
self.cardObjs = {}
|
|
|
|
|
|
|
|
|
|
def getCard(self, index):
|
|
|
|
|
id = self.cards[index.row()]
|
|
|
|
|
if not id in self.cardObjs:
|
2013-10-30 14:42:32 +01:00
|
|
|
|
self.cardObjs[id] = self.col.getCard(id)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return self.cardObjs[id]
|
|
|
|
|
|
|
|
|
|
def refreshNote(self, note):
|
|
|
|
|
refresh = False
|
|
|
|
|
for c in note.cards():
|
|
|
|
|
if c.id in self.cardObjs:
|
|
|
|
|
del self.cardObjs[c.id]
|
|
|
|
|
refresh = True
|
|
|
|
|
if refresh:
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.layoutChanged.emit()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Model interface
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2017-02-19 05:30:35 +01:00
|
|
|
|
def rowCount(self, parent):
|
2017-02-23 07:21:00 +01:00
|
|
|
|
if parent and parent.isValid():
|
2017-02-19 05:30:35 +01:00
|
|
|
|
return 0
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return len(self.cards)
|
|
|
|
|
|
2017-02-19 05:30:35 +01:00
|
|
|
|
def columnCount(self, parent):
|
2017-02-23 07:21:00 +01:00
|
|
|
|
if parent and parent.isValid():
|
2017-02-19 05:30:35 +01:00
|
|
|
|
return 0
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return len(self.activeCols)
|
|
|
|
|
|
|
|
|
|
def data(self, index, role):
|
|
|
|
|
if not index.isValid():
|
|
|
|
|
return
|
|
|
|
|
if role == Qt.FontRole:
|
2012-12-22 02:10:52 +01:00
|
|
|
|
if self.activeCols[index.column()] not in (
|
|
|
|
|
"question", "answer", "noteFld"):
|
|
|
|
|
return
|
|
|
|
|
row = index.row()
|
|
|
|
|
c = self.getCard(index)
|
|
|
|
|
t = c.template()
|
2017-07-29 08:00:02 +02:00
|
|
|
|
if not t.get("bfont"):
|
|
|
|
|
return
|
|
|
|
|
f = QFont()
|
2016-06-23 04:06:16 +02:00
|
|
|
|
f.setFamily(t.get("bfont", "arial"))
|
|
|
|
|
f.setPixelSize(t.get("bsize", 12))
|
2012-12-22 02:10:52 +01:00
|
|
|
|
return f
|
2017-07-29 08:00:02 +02:00
|
|
|
|
|
2012-12-22 02:10:52 +01:00
|
|
|
|
elif role == Qt.TextAlignmentRole:
|
2012-12-21 08:51:59 +01:00
|
|
|
|
align = Qt.AlignVCenter
|
|
|
|
|
if self.activeCols[index.column()] not in ("question", "answer",
|
|
|
|
|
"template", "deck", "noteFld", "note"):
|
|
|
|
|
align |= Qt.AlignHCenter
|
|
|
|
|
return align
|
|
|
|
|
elif role == Qt.DisplayRole or role == Qt.EditRole:
|
|
|
|
|
return self.columnData(index)
|
|
|
|
|
else:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def headerData(self, section, orientation, role):
|
|
|
|
|
if orientation == Qt.Vertical:
|
|
|
|
|
return
|
|
|
|
|
elif role == Qt.DisplayRole and section < len(self.activeCols):
|
|
|
|
|
type = self.columnType(section)
|
2014-01-13 11:07:34 +01:00
|
|
|
|
txt = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
for stype, name in self.browser.columns:
|
|
|
|
|
if type == stype:
|
|
|
|
|
txt = name
|
|
|
|
|
break
|
2014-01-13 11:07:34 +01:00
|
|
|
|
# handle case where extension has set an invalid column type
|
|
|
|
|
if not txt:
|
|
|
|
|
txt = self.browser.columns[0][1]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return txt
|
|
|
|
|
else:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def flags(self, index):
|
|
|
|
|
return Qt.ItemFlag(Qt.ItemIsEnabled |
|
|
|
|
|
Qt.ItemIsSelectable)
|
|
|
|
|
|
|
|
|
|
# Filtering
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2016-07-14 12:23:44 +02:00
|
|
|
|
def search(self, txt):
|
|
|
|
|
self.beginReset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
t = time.time()
|
|
|
|
|
# the db progress handler may cause a refresh, so we need to zero out
|
|
|
|
|
# old data first
|
|
|
|
|
self.cards = []
|
2017-02-02 12:00:58 +01:00
|
|
|
|
invalid = False
|
|
|
|
|
try:
|
|
|
|
|
self.cards = self.col.findCards(txt, order=True)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
if str(e) == "invalidSearch":
|
|
|
|
|
self.cards = []
|
|
|
|
|
invalid = True
|
|
|
|
|
else:
|
|
|
|
|
raise
|
2012-12-21 08:51:59 +01:00
|
|
|
|
#print "fetch cards in %dms" % ((time.time() - t)*1000)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.endReset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2017-02-02 12:00:58 +01:00
|
|
|
|
if invalid:
|
|
|
|
|
showWarning(_("Invalid search - please check for typing mistakes."))
|
|
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def reset(self):
|
|
|
|
|
self.beginReset()
|
|
|
|
|
self.endReset()
|
|
|
|
|
|
2016-07-14 12:23:44 +02:00
|
|
|
|
# caller must have called editor.saveNow() before calling this or .reset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def beginReset(self):
|
|
|
|
|
self.browser.editor.setNote(None, hide=False)
|
|
|
|
|
self.browser.mw.progress.start()
|
|
|
|
|
self.saveSelection()
|
|
|
|
|
self.beginResetModel()
|
|
|
|
|
self.cardObjs = {}
|
|
|
|
|
|
|
|
|
|
def endReset(self):
|
|
|
|
|
t = time.time()
|
|
|
|
|
self.endResetModel()
|
|
|
|
|
self.restoreSelection()
|
|
|
|
|
self.browser.mw.progress.finish()
|
|
|
|
|
|
|
|
|
|
def reverse(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.browser.editor.saveNow(self._reverse)
|
|
|
|
|
|
|
|
|
|
def _reverse(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.beginReset()
|
|
|
|
|
self.cards.reverse()
|
|
|
|
|
self.endReset()
|
|
|
|
|
|
|
|
|
|
def saveSelection(self):
|
|
|
|
|
cards = self.browser.selectedCards()
|
|
|
|
|
self.selectedCards = dict([(id, True) for id in cards])
|
|
|
|
|
if getattr(self.browser, 'card', None):
|
|
|
|
|
self.focusedCard = self.browser.card.id
|
|
|
|
|
else:
|
|
|
|
|
self.focusedCard = None
|
|
|
|
|
|
|
|
|
|
def restoreSelection(self):
|
|
|
|
|
if not self.cards:
|
|
|
|
|
return
|
|
|
|
|
sm = self.browser.form.tableView.selectionModel()
|
|
|
|
|
sm.clear()
|
|
|
|
|
# restore selection
|
|
|
|
|
items = QItemSelection()
|
|
|
|
|
count = 0
|
|
|
|
|
firstIdx = None
|
|
|
|
|
focusedIdx = None
|
|
|
|
|
for row, id in enumerate(self.cards):
|
|
|
|
|
# if the id matches the focused card, note the index
|
|
|
|
|
if self.focusedCard == id:
|
|
|
|
|
focusedIdx = self.index(row, 0)
|
|
|
|
|
items.select(focusedIdx, focusedIdx)
|
|
|
|
|
self.focusedCard = None
|
|
|
|
|
# if the card was previously selected, select again
|
|
|
|
|
if id in self.selectedCards:
|
|
|
|
|
count += 1
|
|
|
|
|
idx = self.index(row, 0)
|
|
|
|
|
items.select(idx, idx)
|
|
|
|
|
# note down the first card of the selection, in case we don't
|
|
|
|
|
# have a focused card
|
|
|
|
|
if not firstIdx:
|
|
|
|
|
firstIdx = idx
|
|
|
|
|
# focus previously focused or first in selection
|
|
|
|
|
idx = focusedIdx or firstIdx
|
|
|
|
|
tv = self.browser.form.tableView
|
|
|
|
|
if idx:
|
|
|
|
|
tv.selectRow(idx.row())
|
2016-06-07 08:32:19 +02:00
|
|
|
|
# we save and then restore the horizontal scroll position because
|
|
|
|
|
# scrollTo() also scrolls horizontally which is confusing
|
|
|
|
|
h = tv.horizontalScrollBar().value()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
tv.scrollTo(idx, tv.PositionAtCenter)
|
2016-06-07 08:32:19 +02:00
|
|
|
|
tv.horizontalScrollBar().setValue(h)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if count < 500:
|
|
|
|
|
# discard large selections; they're too slow
|
|
|
|
|
sm.select(items, QItemSelectionModel.SelectCurrent |
|
|
|
|
|
QItemSelectionModel.Rows)
|
|
|
|
|
else:
|
|
|
|
|
tv.selectRow(0)
|
|
|
|
|
|
|
|
|
|
# Column data
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def columnType(self, column):
|
|
|
|
|
return self.activeCols[column]
|
|
|
|
|
|
|
|
|
|
def columnData(self, index):
|
|
|
|
|
row = index.row()
|
|
|
|
|
col = index.column()
|
|
|
|
|
type = self.columnType(col)
|
|
|
|
|
c = self.getCard(index)
|
|
|
|
|
if type == "question":
|
|
|
|
|
return self.question(c)
|
|
|
|
|
elif type == "answer":
|
|
|
|
|
return self.answer(c)
|
|
|
|
|
elif type == "noteFld":
|
|
|
|
|
f = c.note()
|
2017-03-14 07:48:40 +01:00
|
|
|
|
return htmlToTextLine(f.fields[self.col.models.sortIdx(f.model())])
|
2012-12-21 08:51:59 +01:00
|
|
|
|
elif type == "template":
|
|
|
|
|
t = c.template()['name']
|
|
|
|
|
if c.model()['type'] == MODEL_CLOZE:
|
|
|
|
|
t += " %d" % (c.ord+1)
|
|
|
|
|
return t
|
|
|
|
|
elif type == "cardDue":
|
|
|
|
|
# catch invalid dates
|
|
|
|
|
try:
|
|
|
|
|
t = self.nextDue(c, index)
|
|
|
|
|
except:
|
|
|
|
|
t = ""
|
|
|
|
|
if c.queue < 0:
|
|
|
|
|
t = "(" + t + ")"
|
|
|
|
|
return t
|
|
|
|
|
elif type == "noteCrt":
|
|
|
|
|
return time.strftime("%Y-%m-%d", time.localtime(c.note().id/1000))
|
|
|
|
|
elif type == "noteMod":
|
|
|
|
|
return time.strftime("%Y-%m-%d", time.localtime(c.note().mod))
|
|
|
|
|
elif type == "cardMod":
|
|
|
|
|
return time.strftime("%Y-%m-%d", time.localtime(c.mod))
|
|
|
|
|
elif type == "cardReps":
|
|
|
|
|
return str(c.reps)
|
|
|
|
|
elif type == "cardLapses":
|
|
|
|
|
return str(c.lapses)
|
2013-09-03 20:20:20 +02:00
|
|
|
|
elif type == "noteTags":
|
2013-10-08 00:42:06 +02:00
|
|
|
|
return " ".join(c.note().tags)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
elif type == "note":
|
|
|
|
|
return c.model()['name']
|
|
|
|
|
elif type == "cardIvl":
|
|
|
|
|
if c.type == 0:
|
|
|
|
|
return _("(new)")
|
|
|
|
|
elif c.type == 1:
|
|
|
|
|
return _("(learning)")
|
|
|
|
|
return fmtTimeSpan(c.ivl*86400)
|
|
|
|
|
elif type == "cardEase":
|
|
|
|
|
if c.type == 0:
|
|
|
|
|
return _("(new)")
|
|
|
|
|
return "%d%%" % (c.factor/10)
|
|
|
|
|
elif type == "deck":
|
|
|
|
|
if c.odid:
|
|
|
|
|
# in a cram deck
|
|
|
|
|
return "%s (%s)" % (
|
|
|
|
|
self.browser.mw.col.decks.name(c.did),
|
|
|
|
|
self.browser.mw.col.decks.name(c.odid))
|
|
|
|
|
# normal deck
|
|
|
|
|
return self.browser.mw.col.decks.name(c.did)
|
|
|
|
|
|
|
|
|
|
def question(self, c):
|
2017-03-14 07:48:40 +01:00
|
|
|
|
return htmlToTextLine(c.q(browser=True))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def answer(self, c):
|
|
|
|
|
if c.template().get('bafmt'):
|
|
|
|
|
# they have provided a template, use it verbatim
|
|
|
|
|
c.q(browser=True)
|
2017-03-14 07:48:40 +01:00
|
|
|
|
return htmlToTextLine(c.a())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# need to strip question from answer
|
|
|
|
|
q = self.question(c)
|
2017-03-14 07:48:40 +01:00
|
|
|
|
a = htmlToTextLine(c.a())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if a.startswith(q):
|
|
|
|
|
return a[len(q):].strip()
|
|
|
|
|
return a
|
|
|
|
|
|
|
|
|
|
def nextDue(self, c, index):
|
|
|
|
|
if c.odid:
|
|
|
|
|
return _("(filtered)")
|
|
|
|
|
elif c.queue == 1:
|
|
|
|
|
date = c.due
|
|
|
|
|
elif c.queue == 0 or c.type == 0:
|
|
|
|
|
return str(c.due)
|
|
|
|
|
elif c.queue in (2,3) or (c.type == 2 and c.queue < 0):
|
|
|
|
|
date = time.time() + ((c.due - self.col.sched.today)*86400)
|
|
|
|
|
else:
|
|
|
|
|
return ""
|
|
|
|
|
return time.strftime("%Y-%m-%d", time.localtime(date))
|
|
|
|
|
|
2018-08-08 02:39:54 +02:00
|
|
|
|
def isRTL(self, index):
|
|
|
|
|
col = index.column()
|
|
|
|
|
type = self.columnType(col)
|
|
|
|
|
if type != "noteFld":
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
row = index.row()
|
|
|
|
|
c = self.getCard(index)
|
|
|
|
|
nt = c.note().model()
|
|
|
|
|
return nt['flds'][self.col.models.sortIdx(nt)]['rtl']
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# Line painter
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2017-08-12 08:08:10 +02:00
|
|
|
|
COLOUR_SUSPENDED = "#FFFFB2"
|
2017-08-16 12:30:29 +02:00
|
|
|
|
COLOUR_MARKED = "#ccc"
|
2017-08-12 08:08:10 +02:00
|
|
|
|
|
|
|
|
|
flagColours = {
|
|
|
|
|
1: "#F5B7B1",
|
|
|
|
|
2: "#BB8FCE",
|
|
|
|
|
3: "#82E0AA",
|
|
|
|
|
4: "#85C1E9",
|
|
|
|
|
}
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
class StatusDelegate(QItemDelegate):
|
|
|
|
|
|
|
|
|
|
def __init__(self, browser, model):
|
|
|
|
|
QItemDelegate.__init__(self, browser)
|
2013-01-28 22:45:29 +01:00
|
|
|
|
self.browser = browser
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.model = model
|
|
|
|
|
|
|
|
|
|
def paint(self, painter, option, index):
|
2013-01-28 22:45:29 +01:00
|
|
|
|
self.browser.mw.progress.blockUpdates = True
|
2012-12-21 08:51:59 +01:00
|
|
|
|
try:
|
|
|
|
|
c = self.model.getCard(index)
|
|
|
|
|
except:
|
|
|
|
|
# in the the middle of a reset; return nothing so this row is not
|
|
|
|
|
# rendered until we have a chance to reset the model
|
|
|
|
|
return
|
2013-01-28 22:45:29 +01:00
|
|
|
|
finally:
|
|
|
|
|
self.browser.mw.progress.blockUpdates = True
|
2017-08-12 08:08:10 +02:00
|
|
|
|
|
2018-08-08 02:39:54 +02:00
|
|
|
|
if self.model.isRTL(index):
|
|
|
|
|
option.direction = Qt.RightToLeft
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
col = None
|
2017-08-12 08:08:10 +02:00
|
|
|
|
if c.queue == -1:
|
2012-12-21 08:51:59 +01:00
|
|
|
|
col = COLOUR_SUSPENDED
|
2017-08-12 08:08:10 +02:00
|
|
|
|
elif c.userFlag() > 0:
|
|
|
|
|
col = flagColours[c.userFlag()]
|
2017-08-16 12:30:29 +02:00
|
|
|
|
elif c.note().hasTag("Marked"):
|
|
|
|
|
col = COLOUR_MARKED
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if col:
|
|
|
|
|
brush = QBrush(QColor(col))
|
|
|
|
|
painter.save()
|
|
|
|
|
painter.fillRect(option.rect, brush)
|
|
|
|
|
painter.restore()
|
2017-08-12 08:08:10 +02:00
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return QItemDelegate.paint(self, painter, option, index)
|
|
|
|
|
|
|
|
|
|
# Browser window
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
# fixme: respond to reset+edit hooks
|
|
|
|
|
|
|
|
|
|
class Browser(QMainWindow):
|
|
|
|
|
|
|
|
|
|
def __init__(self, mw):
|
|
|
|
|
QMainWindow.__init__(self, None, Qt.Window)
|
|
|
|
|
applyStyles(self)
|
|
|
|
|
self.mw = mw
|
|
|
|
|
self.col = self.mw.col
|
|
|
|
|
self.lastFilter = ""
|
2017-08-05 07:15:19 +02:00
|
|
|
|
self.focusTo = None
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._previewWindow = None
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self._closeEventHasCleanedUp = False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.form = aqt.forms.browser.Ui_Dialog()
|
|
|
|
|
self.form.setupUi(self)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
self.setupSidebar()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
restoreGeom(self, "editor", 0)
|
|
|
|
|
restoreState(self, "editor")
|
|
|
|
|
restoreSplitter(self.form.splitter, "editor3")
|
|
|
|
|
self.form.splitter.setChildrenCollapsible(False)
|
|
|
|
|
self.card = None
|
|
|
|
|
self.setupColumns()
|
|
|
|
|
self.setupTable()
|
|
|
|
|
self.setupMenus()
|
|
|
|
|
self.setupHeaders()
|
|
|
|
|
self.setupHooks()
|
|
|
|
|
self.setupEditor()
|
|
|
|
|
self.updateFont()
|
|
|
|
|
self.onUndoState(self.mw.form.actionUndo.isEnabled())
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.setupSearch()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.show()
|
|
|
|
|
|
|
|
|
|
def setupMenus(self):
|
|
|
|
|
# actions
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f = self.form
|
|
|
|
|
f.previewButton.clicked.connect(self.onTogglePreview)
|
2013-05-22 03:04:34 +02:00
|
|
|
|
f.previewButton.setToolTip(_("Preview Selected Card (%s)") %
|
2017-08-11 06:40:51 +02:00
|
|
|
|
shortcut(_("Ctrl+Shift+P")))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
f.filter.clicked.connect(self.onFilterButton)
|
2017-08-11 06:40:51 +02:00
|
|
|
|
# edit
|
|
|
|
|
f.actionUndo.triggered.connect(self.mw.onUndo)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f.actionInvertSelection.triggered.connect(self.invertSelection)
|
|
|
|
|
f.actionSelectNotes.triggered.connect(self.selectNotes)
|
2017-08-11 06:40:51 +02:00
|
|
|
|
if not isMac:
|
|
|
|
|
f.actionClose.setVisible(False)
|
|
|
|
|
# notes
|
|
|
|
|
f.actionAdd.triggered.connect(self.mw.onAddCard)
|
|
|
|
|
f.actionAdd_Tags.triggered.connect(lambda: self.addTags())
|
|
|
|
|
f.actionRemove_Tags.triggered.connect(lambda: self.deleteTags())
|
2017-08-15 10:48:22 +02:00
|
|
|
|
f.actionClear_Unused_Tags.triggered.connect(self.clearUnusedTags)
|
2017-08-16 12:30:29 +02:00
|
|
|
|
f.actionToggle_Mark.triggered.connect(lambda: self.onMark())
|
2017-08-11 06:40:51 +02:00
|
|
|
|
f.actionChangeModel.triggered.connect(self.onChangeModel)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f.actionFindDuplicates.triggered.connect(self.onFindDupes)
|
2017-08-11 06:40:51 +02:00
|
|
|
|
f.actionFindReplace.triggered.connect(self.onFindReplace)
|
2017-08-15 10:49:41 +02:00
|
|
|
|
f.actionManage_Note_Types.triggered.connect(self.mw.onNoteTypes)
|
2017-08-11 06:40:51 +02:00
|
|
|
|
f.actionDelete.triggered.connect(self.deleteNotes)
|
|
|
|
|
# cards
|
|
|
|
|
f.actionChange_Deck.triggered.connect(self.setDeck)
|
|
|
|
|
f.action_Info.triggered.connect(self.showCardInfo)
|
|
|
|
|
f.actionReposition.triggered.connect(self.reposition)
|
|
|
|
|
f.actionReschedule.triggered.connect(self.reschedule)
|
|
|
|
|
f.actionToggle_Suspend.triggered.connect(self.onSuspend)
|
2017-08-12 08:08:10 +02:00
|
|
|
|
f.actionRed_Flag.triggered.connect(lambda: self.onSetFlag(1))
|
|
|
|
|
f.actionPurple_Flag.triggered.connect(lambda: self.onSetFlag(2))
|
|
|
|
|
f.actionGreen_Flag.triggered.connect(lambda: self.onSetFlag(3))
|
|
|
|
|
f.actionBlue_Flag.triggered.connect(lambda: self.onSetFlag(4))
|
|
|
|
|
f.actionClear_Flag.triggered.connect(lambda: self.onSetFlag(0))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# jumps
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f.actionPreviousCard.triggered.connect(self.onPreviousCard)
|
|
|
|
|
f.actionNextCard.triggered.connect(self.onNextCard)
|
|
|
|
|
f.actionFirstCard.triggered.connect(self.onFirstCard)
|
|
|
|
|
f.actionLastCard.triggered.connect(self.onLastCard)
|
|
|
|
|
f.actionFind.triggered.connect(self.onFind)
|
|
|
|
|
f.actionNote.triggered.connect(self.onNote)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
f.actionTags.triggered.connect(self.onFilterButton)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
f.actionSidebar.triggered.connect(self.focusSidebar)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f.actionCardList.triggered.connect(self.onCardList)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# help
|
2016-05-31 10:51:40 +02:00
|
|
|
|
f.actionGuide.triggered.connect(self.onHelp)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# keyboard shortcut for shift+home/end
|
|
|
|
|
self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.pgUpCut.activated.connect(self.onFirstCard)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.pgDownCut.activated.connect(self.onLastCard)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# add-on hook
|
|
|
|
|
runHook('browser.setupMenus', self)
|
|
|
|
|
self.mw.maybeHideAccelerators(self)
|
|
|
|
|
|
2017-12-14 05:49:51 +01:00
|
|
|
|
# context menu
|
|
|
|
|
self.form.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
|
|
|
self.form.tableView.customContextMenuRequested.connect(self.onContextMenu)
|
|
|
|
|
|
|
|
|
|
def onContextMenu(self, _point):
|
|
|
|
|
m = QMenu()
|
|
|
|
|
for act in self.form.menu_Cards.actions():
|
|
|
|
|
m.addAction(act)
|
|
|
|
|
m.addSeparator()
|
|
|
|
|
for act in self.form.menu_Notes.actions():
|
|
|
|
|
m.addAction(act)
|
2018-07-25 11:40:49 +02:00
|
|
|
|
runHook("browser.onContextMenu", self, m)
|
2017-12-14 05:49:51 +01:00
|
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def updateFont(self):
|
2013-05-23 07:12:04 +02:00
|
|
|
|
# we can't choose different line heights efficiently, so we need
|
|
|
|
|
# to pick a line height big enough for any card template
|
|
|
|
|
curmax = 16
|
|
|
|
|
for m in self.col.models.all():
|
|
|
|
|
for t in m['tmpls']:
|
|
|
|
|
bsize = t.get("bsize", 0)
|
|
|
|
|
if bsize > curmax:
|
|
|
|
|
curmax = bsize
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.form.tableView.verticalHeader().setDefaultSectionSize(
|
2013-05-23 07:12:04 +02:00
|
|
|
|
curmax + 6)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def closeEvent(self, evt):
|
2017-08-16 04:45:33 +02:00
|
|
|
|
if self._closeEventHasCleanedUp:
|
2016-07-14 12:23:44 +02:00
|
|
|
|
evt.accept()
|
2017-08-16 04:45:33 +02:00
|
|
|
|
return
|
|
|
|
|
self.editor.saveNow(self._closeWindow)
|
|
|
|
|
evt.ignore()
|
2016-07-14 12:23:44 +02:00
|
|
|
|
|
2017-08-16 04:45:33 +02:00
|
|
|
|
def _closeWindow(self):
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._cancelPreviewTimer()
|
2017-08-16 04:45:33 +02:00
|
|
|
|
self.editor.cleanup()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
saveSplitter(self.form.splitter, "editor3")
|
|
|
|
|
saveGeom(self, "editor")
|
|
|
|
|
saveState(self, "editor")
|
2014-03-14 07:29:48 +01:00
|
|
|
|
saveHeader(self.form.tableView.horizontalHeader(), "editor")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.col.conf['activeCols'] = self.model.activeCols
|
|
|
|
|
self.col.setMod()
|
|
|
|
|
self.teardownHooks()
|
|
|
|
|
self.mw.maybeReset()
|
2017-08-16 04:45:33 +02:00
|
|
|
|
aqt.dialogs.markClosed("Browser")
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self._closeEventHasCleanedUp = True
|
2017-08-16 04:45:33 +02:00
|
|
|
|
self.mw.gcWindow(self)
|
|
|
|
|
self.close()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2017-08-16 04:45:33 +02:00
|
|
|
|
def closeWithCallback(self, onsuccess):
|
|
|
|
|
def callback():
|
|
|
|
|
self._closeWindow()
|
|
|
|
|
onsuccess()
|
|
|
|
|
self.editor.saveNow(callback)
|
2013-04-11 12:23:32 +02:00
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def keyPressEvent(self, evt):
|
|
|
|
|
if evt.key() == Qt.Key_Escape:
|
|
|
|
|
self.close()
|
2017-08-13 11:11:40 +02:00
|
|
|
|
else:
|
|
|
|
|
super().keyPressEvent(evt)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def setupColumns(self):
|
|
|
|
|
self.columns = [
|
2013-02-22 03:13:59 +01:00
|
|
|
|
('question', _("Question")),
|
|
|
|
|
('answer', _("Answer")),
|
2012-12-21 08:51:59 +01:00
|
|
|
|
('template', _("Card")),
|
|
|
|
|
('deck', _("Deck")),
|
|
|
|
|
('noteFld', _("Sort Field")),
|
|
|
|
|
('noteCrt', _("Created")),
|
|
|
|
|
('noteMod', _("Edited")),
|
|
|
|
|
('cardMod', _("Changed")),
|
|
|
|
|
('cardDue', _("Due")),
|
|
|
|
|
('cardIvl', _("Interval")),
|
|
|
|
|
('cardEase', _("Ease")),
|
|
|
|
|
('cardReps', _("Reviews")),
|
|
|
|
|
('cardLapses', _("Lapses")),
|
2013-09-03 20:20:20 +02:00
|
|
|
|
('noteTags', _("Tags")),
|
2012-12-21 08:51:59 +01:00
|
|
|
|
('note', _("Note")),
|
|
|
|
|
]
|
|
|
|
|
self.columns.sort(key=itemgetter(1))
|
|
|
|
|
|
|
|
|
|
# Searching
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def setupSearch(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.form.searchButton.clicked.connect(self.onSearchActivated)
|
|
|
|
|
self.form.searchEdit.lineEdit().returnPressed.connect(self.onSearchActivated)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.form.searchEdit.setCompleter(None)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self._searchPrompt = _("<type here to search; hit enter to show current deck>")
|
2017-08-08 02:05:10 +02:00
|
|
|
|
self.form.searchEdit.addItems([self._searchPrompt] + self.mw.pm.profile['searchHistory'])
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self._lastSearchTxt = "is:current"
|
|
|
|
|
self.search()
|
|
|
|
|
# then replace text for easily showing the deck
|
|
|
|
|
self.form.searchEdit.lineEdit().setText(self._searchPrompt)
|
|
|
|
|
self.form.searchEdit.lineEdit().selectAll()
|
|
|
|
|
self.form.searchEdit.setFocus()
|
|
|
|
|
|
|
|
|
|
# search triggered by user
|
|
|
|
|
def onSearchActivated(self):
|
|
|
|
|
self.editor.saveNow(self._onSearchActivated)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2016-07-14 12:23:44 +02:00
|
|
|
|
def _onSearchActivated(self):
|
|
|
|
|
# convert guide text before we save history
|
|
|
|
|
if self.form.searchEdit.lineEdit().text() == self._searchPrompt:
|
|
|
|
|
self.form.searchEdit.lineEdit().setText("deck:current ")
|
|
|
|
|
|
|
|
|
|
# update history
|
2018-07-23 03:01:21 +02:00
|
|
|
|
txt = str(self.form.searchEdit.lineEdit().text())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
sh = self.mw.pm.profile['searchHistory']
|
|
|
|
|
if txt in sh:
|
|
|
|
|
sh.remove(txt)
|
|
|
|
|
sh.insert(0, txt)
|
|
|
|
|
sh = sh[:30]
|
|
|
|
|
self.form.searchEdit.clear()
|
|
|
|
|
self.form.searchEdit.addItems(sh)
|
|
|
|
|
self.mw.pm.profile['searchHistory'] = sh
|
2016-07-14 12:23:44 +02:00
|
|
|
|
|
|
|
|
|
# keep track of search string so that we reuse identical search when
|
|
|
|
|
# refreshing, rather than whatever is currently in the search field
|
|
|
|
|
self._lastSearchTxt = txt
|
|
|
|
|
self.search()
|
|
|
|
|
|
|
|
|
|
# search triggered programmatically. caller must have saved note first.
|
|
|
|
|
def search(self):
|
|
|
|
|
if "is:current" in self._lastSearchTxt:
|
|
|
|
|
# show current card if there is one
|
|
|
|
|
c = self.mw.reviewer.card
|
2018-07-23 04:50:10 +02:00
|
|
|
|
self.card = self.mw.reviewer.card
|
2016-07-14 12:23:44 +02:00
|
|
|
|
nid = c and c.nid or 0
|
|
|
|
|
self.model.search("nid:%d"%nid)
|
|
|
|
|
else:
|
|
|
|
|
self.model.search(self._lastSearchTxt)
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if not self.model.cards:
|
|
|
|
|
# no row change will fire
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self._onRowChanged(None, None)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def updateTitle(self):
|
|
|
|
|
selected = len(self.form.tableView.selectionModel().selectedRows())
|
|
|
|
|
cur = len(self.model.cards)
|
2017-08-11 14:04:06 +02:00
|
|
|
|
self.setWindowTitle(ngettext("Browse (%(cur)d card shown; %(sel)s)",
|
|
|
|
|
"Browse (%(cur)d cards shown; %(sel)s)",
|
2012-12-21 08:51:59 +01:00
|
|
|
|
cur) % {
|
|
|
|
|
"cur": cur,
|
|
|
|
|
"sel": ngettext("%d selected", "%d selected", selected) % selected
|
|
|
|
|
})
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
|
def onReset(self):
|
|
|
|
|
self.editor.setNote(None)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Table view & editor
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def setupTable(self):
|
|
|
|
|
self.model = DataModel(self)
|
|
|
|
|
self.form.tableView.setSortingEnabled(True)
|
|
|
|
|
self.form.tableView.setModel(self.model)
|
|
|
|
|
self.form.tableView.selectionModel()
|
|
|
|
|
self.form.tableView.setItemDelegate(StatusDelegate(self, self.model))
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.form.tableView.selectionModel().selectionChanged.connect(self.onRowChanged)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def setupEditor(self):
|
|
|
|
|
self.editor = aqt.editor.Editor(
|
|
|
|
|
self.mw, self.form.fieldsArea, self)
|
|
|
|
|
|
|
|
|
|
def onRowChanged(self, current, previous):
|
|
|
|
|
"Update current note and hide/show editor."
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._onRowChanged(current, previous))
|
|
|
|
|
|
|
|
|
|
def _onRowChanged(self, current, previous):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
update = self.updateTitle()
|
|
|
|
|
show = self.model.cards and update == 1
|
2013-04-15 06:46:07 +02:00
|
|
|
|
self.form.splitter.widget(1).setVisible(not not show)
|
2017-08-31 12:38:12 +02:00
|
|
|
|
idx = self.form.tableView.selectionModel().currentIndex()
|
|
|
|
|
if idx.isValid():
|
|
|
|
|
self.card = self.model.getCard(idx)
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if not show:
|
|
|
|
|
self.editor.setNote(None)
|
2013-05-10 08:02:58 +02:00
|
|
|
|
self.singleCard = False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
else:
|
2017-08-05 07:15:19 +02:00
|
|
|
|
self.editor.setNote(self.card.note(reload=True), focusTo=self.focusTo)
|
|
|
|
|
self.focusTo = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.editor.card = self.card
|
2013-05-10 08:02:58 +02:00
|
|
|
|
self.singleCard = True
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._renderPreview(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def refreshCurrentCard(self, note):
|
|
|
|
|
self.model.refreshNote(note)
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._renderPreview(False)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def refreshCurrentCardFilter(self, flag, note, fidx):
|
|
|
|
|
self.refreshCurrentCard(note)
|
|
|
|
|
return flag
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
def currentRow(self):
|
|
|
|
|
idx = self.form.tableView.selectionModel().currentIndex()
|
|
|
|
|
return idx.row()
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# Headers & sorting
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def setupHeaders(self):
|
|
|
|
|
vh = self.form.tableView.verticalHeader()
|
|
|
|
|
hh = self.form.tableView.horizontalHeader()
|
|
|
|
|
if not isWin:
|
|
|
|
|
vh.hide()
|
|
|
|
|
hh.show()
|
2014-03-14 07:29:48 +01:00
|
|
|
|
restoreHeader(hh, "editor")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
hh.setHighlightSections(False)
|
|
|
|
|
hh.setMinimumSectionSize(50)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
hh.setSectionsMovable(True)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.setColumnSizes()
|
|
|
|
|
hh.setContextMenuPolicy(Qt.CustomContextMenu)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
hh.customContextMenuRequested.connect(self.onHeaderContext)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.setSortIndicator()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
hh.sortIndicatorChanged.connect(self.onSortChanged)
|
|
|
|
|
hh.sectionMoved.connect(self.onColumnMoved)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onSortChanged(self, idx, ord):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._onSortChanged(idx, ord))
|
|
|
|
|
|
|
|
|
|
def _onSortChanged(self, idx, ord):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
type = self.model.activeCols[idx]
|
2013-09-03 20:20:20 +02:00
|
|
|
|
noSort = ("question", "answer", "template", "deck", "note", "noteTags")
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if type in noSort:
|
|
|
|
|
if type == "template":
|
|
|
|
|
showInfo(_("""\
|
2013-03-03 00:19:16 +01:00
|
|
|
|
This column can't be sorted on, but you can search for individual card types, \
|
2016-07-14 12:23:44 +02:00
|
|
|
|
such as 'card:1'."""))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
elif type == "deck":
|
|
|
|
|
showInfo(_("""\
|
2013-03-03 00:19:16 +01:00
|
|
|
|
This column can't be sorted on, but you can search for specific decks \
|
2012-12-21 08:51:59 +01:00
|
|
|
|
by clicking on one on the left."""))
|
|
|
|
|
else:
|
|
|
|
|
showInfo(_("Sorting on this column is not supported. Please "
|
|
|
|
|
"choose another."))
|
|
|
|
|
type = self.col.conf['sortType']
|
|
|
|
|
if self.col.conf['sortType'] != type:
|
|
|
|
|
self.col.conf['sortType'] = type
|
|
|
|
|
# default to descending for non-text fields
|
|
|
|
|
if type == "noteFld":
|
|
|
|
|
ord = not ord
|
|
|
|
|
self.col.conf['sortBackwards'] = ord
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
else:
|
|
|
|
|
if self.col.conf['sortBackwards'] != ord:
|
|
|
|
|
self.col.conf['sortBackwards'] = ord
|
|
|
|
|
self.model.reverse()
|
|
|
|
|
self.setSortIndicator()
|
|
|
|
|
|
|
|
|
|
def setSortIndicator(self):
|
|
|
|
|
hh = self.form.tableView.horizontalHeader()
|
|
|
|
|
type = self.col.conf['sortType']
|
|
|
|
|
if type not in self.model.activeCols:
|
|
|
|
|
hh.setSortIndicatorShown(False)
|
|
|
|
|
return
|
|
|
|
|
idx = self.model.activeCols.index(type)
|
|
|
|
|
if self.col.conf['sortBackwards']:
|
|
|
|
|
ord = Qt.DescendingOrder
|
|
|
|
|
else:
|
|
|
|
|
ord = Qt.AscendingOrder
|
|
|
|
|
hh.blockSignals(True)
|
|
|
|
|
hh.setSortIndicator(idx, ord)
|
|
|
|
|
hh.blockSignals(False)
|
|
|
|
|
hh.setSortIndicatorShown(True)
|
|
|
|
|
|
|
|
|
|
def onHeaderContext(self, pos):
|
|
|
|
|
gpos = self.form.tableView.mapToGlobal(pos)
|
|
|
|
|
m = QMenu()
|
|
|
|
|
for type, name in self.columns:
|
|
|
|
|
a = m.addAction(name)
|
|
|
|
|
a.setCheckable(True)
|
|
|
|
|
a.setChecked(type in self.model.activeCols)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
a.toggled.connect(lambda b, t=type: self.toggleField(t))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
m.exec_(gpos)
|
|
|
|
|
|
|
|
|
|
def toggleField(self, type):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._toggleField(type))
|
|
|
|
|
|
|
|
|
|
def _toggleField(self, type):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.model.beginReset()
|
|
|
|
|
if type in self.model.activeCols:
|
|
|
|
|
if len(self.model.activeCols) < 2:
|
2018-07-25 11:52:21 +02:00
|
|
|
|
self.model.endReset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return showInfo(_("You must have at least one column."))
|
|
|
|
|
self.model.activeCols.remove(type)
|
2013-10-04 01:43:46 +02:00
|
|
|
|
adding=False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
else:
|
|
|
|
|
self.model.activeCols.append(type)
|
2013-10-04 01:43:46 +02:00
|
|
|
|
adding=True
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# sorted field may have been hidden
|
|
|
|
|
self.setSortIndicator()
|
|
|
|
|
self.setColumnSizes()
|
|
|
|
|
self.model.endReset()
|
2013-10-04 01:43:46 +02:00
|
|
|
|
# if we added a column, scroll to it
|
|
|
|
|
if adding:
|
|
|
|
|
row = self.currentRow()
|
|
|
|
|
idx = self.model.index(row, len(self.model.activeCols) - 1)
|
|
|
|
|
self.form.tableView.scrollTo(idx)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def setColumnSizes(self):
|
|
|
|
|
hh = self.form.tableView.horizontalHeader()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
hh.setSectionResizeMode(QHeaderView.Interactive)
|
|
|
|
|
hh.setSectionResizeMode(hh.logicalIndex(len(self.model.activeCols)-1),
|
2014-03-14 07:41:00 +01:00
|
|
|
|
QHeaderView.Stretch)
|
2013-11-25 20:57:39 +01:00
|
|
|
|
# this must be set post-resize or it doesn't work
|
|
|
|
|
hh.setCascadingSectionResizes(False)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onColumnMoved(self, a, b, c):
|
|
|
|
|
self.setColumnSizes()
|
|
|
|
|
|
2017-08-13 11:11:40 +02:00
|
|
|
|
# Sidebar
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
class CallbackItem(QTreeWidgetItem):
|
|
|
|
|
def __init__(self, root, name, onclick, oncollapse=None, expanded=False):
|
|
|
|
|
QTreeWidgetItem.__init__(self, root, [name])
|
|
|
|
|
self.setExpanded(expanded)
|
|
|
|
|
self.onclick = onclick
|
|
|
|
|
self.oncollapse = oncollapse
|
|
|
|
|
|
|
|
|
|
class SidebarTreeWidget(QTreeWidget):
|
|
|
|
|
def __init__(self):
|
|
|
|
|
QTreeWidget.__init__(self)
|
|
|
|
|
self.itemClicked.connect(self.onTreeClick)
|
|
|
|
|
self.itemExpanded.connect(lambda item: self.onTreeCollapse(item))
|
|
|
|
|
self.itemCollapsed.connect(lambda item: self.onTreeCollapse(item))
|
|
|
|
|
|
|
|
|
|
def keyPressEvent(self, evt):
|
|
|
|
|
if evt.key() in (Qt.Key_Return, Qt.Key_Enter):
|
|
|
|
|
item = self.currentItem()
|
|
|
|
|
self.onTreeClick(item, 0)
|
|
|
|
|
else:
|
|
|
|
|
super().keyPressEvent(evt)
|
|
|
|
|
|
|
|
|
|
def onTreeClick(self, item, col):
|
|
|
|
|
if getattr(item, 'onclick', None):
|
|
|
|
|
item.onclick()
|
|
|
|
|
|
|
|
|
|
def onTreeCollapse(self, item):
|
|
|
|
|
if getattr(item, 'oncollapse', None):
|
|
|
|
|
item.oncollapse()
|
|
|
|
|
|
|
|
|
|
def setupSidebar(self):
|
|
|
|
|
dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self)
|
|
|
|
|
dw.setFeatures(QDockWidget.DockWidgetClosable)
|
|
|
|
|
dw.setObjectName("Sidebar")
|
|
|
|
|
dw.setAllowedAreas(Qt.LeftDockWidgetArea)
|
|
|
|
|
self.sidebarTree = self.SidebarTreeWidget()
|
|
|
|
|
self.sidebarTree.mw = self.mw
|
|
|
|
|
self.sidebarTree.header().setVisible(False)
|
|
|
|
|
dw.setWidget(self.sidebarTree)
|
|
|
|
|
p = QPalette()
|
|
|
|
|
p.setColor(QPalette.Base, p.window().color())
|
|
|
|
|
self.sidebarTree.setPalette(p)
|
2017-08-14 08:57:43 +02:00
|
|
|
|
self.sidebarDockWidget.setVisible(False)
|
2017-08-15 06:54:13 +02:00
|
|
|
|
self.sidebarDockWidget.setFloating(False)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
self.sidebarDockWidget.visibilityChanged.connect(self.onSidebarVisChanged)
|
2017-08-15 07:19:22 +02:00
|
|
|
|
self.sidebarDockWidget.setTitleBarWidget(QWidget())
|
2017-08-14 08:57:43 +02:00
|
|
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, dw)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
|
|
|
|
|
def onSidebarVisChanged(self, visible):
|
|
|
|
|
if visible:
|
|
|
|
|
self.buildTree()
|
|
|
|
|
else:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def focusSidebar(self):
|
|
|
|
|
self.sidebarDockWidget.setVisible(True)
|
|
|
|
|
self.sidebarTree.setFocus()
|
|
|
|
|
|
|
|
|
|
def maybeRefreshSidebar(self):
|
|
|
|
|
if self.sidebarDockWidget.isVisible():
|
|
|
|
|
self.buildTree()
|
|
|
|
|
|
|
|
|
|
def buildTree(self):
|
|
|
|
|
self.sidebarTree.clear()
|
|
|
|
|
root = self.sidebarTree
|
2017-08-28 13:43:44 +02:00
|
|
|
|
self._stdTree(root)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
self._favTree(root)
|
|
|
|
|
self._decksTree(root)
|
|
|
|
|
self._modelTree(root)
|
|
|
|
|
self._userTagTree(root)
|
|
|
|
|
self.sidebarTree.setIndentation(15)
|
|
|
|
|
|
2017-08-28 13:43:44 +02:00
|
|
|
|
def _stdTree(self, root):
|
|
|
|
|
for name, filt, icon in [[_("Whole Collection"), "", "collection"],
|
|
|
|
|
[_("Current Deck"), "deck:current", "deck"]]:
|
|
|
|
|
item = self.CallbackItem(
|
|
|
|
|
root, name, self._filterFunc(filt))
|
|
|
|
|
item.setIcon(0, QIcon(":/icons/{}.svg".format(icon)))
|
|
|
|
|
|
2017-08-13 11:11:40 +02:00
|
|
|
|
def _favTree(self, root):
|
|
|
|
|
saved = self.col.conf.get('savedFilters', {})
|
|
|
|
|
for name, filt in sorted(saved.items()):
|
|
|
|
|
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s))
|
2017-08-15 05:54:58 +02:00
|
|
|
|
item.setIcon(0, QIcon(":/icons/heart.svg"))
|
2017-08-13 11:11:40 +02:00
|
|
|
|
|
|
|
|
|
def _userTagTree(self, root):
|
|
|
|
|
for t in sorted(self.col.tags.all(), key=lambda t: t.lower()):
|
|
|
|
|
item = self.CallbackItem(
|
|
|
|
|
root, t, lambda t=t: self.setFilter("tag", t))
|
2017-08-15 05:54:58 +02:00
|
|
|
|
item.setIcon(0, QIcon(":/icons/tag.svg"))
|
2017-08-13 11:11:40 +02:00
|
|
|
|
|
|
|
|
|
def _decksTree(self, root):
|
|
|
|
|
grps = self.col.sched.deckDueTree()
|
|
|
|
|
def fillGroups(root, grps, head=""):
|
|
|
|
|
for g in grps:
|
|
|
|
|
item = self.CallbackItem(
|
|
|
|
|
root, g[0],
|
|
|
|
|
lambda g=g: self.setFilter("deck", head+g[0]),
|
|
|
|
|
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]),
|
|
|
|
|
not self.mw.col.decks.get(g[1]).get('browserCollapsed', False))
|
2017-08-15 05:54:58 +02:00
|
|
|
|
item.setIcon(0, QIcon(":/icons/deck.svg"))
|
2017-08-13 11:11:40 +02:00
|
|
|
|
newhead = head + g[0]+"::"
|
|
|
|
|
fillGroups(item, g[5], newhead)
|
|
|
|
|
fillGroups(root, grps)
|
|
|
|
|
|
|
|
|
|
def _modelTree(self, root):
|
|
|
|
|
for m in sorted(self.col.models.all(), key=itemgetter("name")):
|
|
|
|
|
mitem = self.CallbackItem(
|
2017-09-02 08:15:01 +02:00
|
|
|
|
root, m['name'], lambda m=m: self.setFilter("note", m['name']))
|
2017-08-15 05:54:58 +02:00
|
|
|
|
mitem.setIcon(0, QIcon(":/icons/notetype.svg"))
|
2017-08-13 11:11:40 +02:00
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# Filter tree
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2017-08-11 11:06:29 +02:00
|
|
|
|
def onFilterButton(self):
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml = MenuList()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addChild(self._commonFilters())
|
|
|
|
|
ml.addSeparator()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addChild(self._todayFilters())
|
|
|
|
|
ml.addChild(self._cardStateFilters())
|
|
|
|
|
ml.addChild(self._deckFilters())
|
|
|
|
|
ml.addChild(self._noteTypeFilters())
|
|
|
|
|
ml.addChild(self._tagFilters())
|
|
|
|
|
ml.addSeparator()
|
2017-08-12 08:08:10 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addChild(self.sidebarDockWidget.toggleViewAction())
|
|
|
|
|
ml.addSeparator()
|
2017-08-13 11:11:40 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addChild(self._savedSearches())
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.popupOver(self.form.filter)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def setFilter(self, *args):
|
|
|
|
|
if len(args) == 1:
|
|
|
|
|
txt = args[0]
|
|
|
|
|
else:
|
|
|
|
|
txt = ""
|
|
|
|
|
items = []
|
|
|
|
|
for c, a in enumerate(args):
|
|
|
|
|
if c % 2 == 0:
|
|
|
|
|
txt += a + ":"
|
|
|
|
|
else:
|
|
|
|
|
txt += a
|
2017-09-02 07:42:19 +02:00
|
|
|
|
for chr in " ()":
|
|
|
|
|
if chr in txt:
|
|
|
|
|
txt = '"%s"' % txt
|
|
|
|
|
break
|
2012-12-21 08:51:59 +01:00
|
|
|
|
items.append(txt)
|
|
|
|
|
txt = ""
|
|
|
|
|
txt = " ".join(items)
|
|
|
|
|
if self.mw.app.keyboardModifiers() & Qt.AltModifier:
|
|
|
|
|
txt = "-"+txt
|
|
|
|
|
if self.mw.app.keyboardModifiers() & Qt.ControlModifier:
|
2016-05-12 06:45:35 +02:00
|
|
|
|
cur = str(self.form.searchEdit.lineEdit().text())
|
2016-07-14 12:23:44 +02:00
|
|
|
|
if cur and cur != self._searchPrompt:
|
2014-06-25 20:46:21 +02:00
|
|
|
|
txt = cur + " " + txt
|
2013-04-11 10:07:08 +02:00
|
|
|
|
elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
|
2016-05-12 06:45:35 +02:00
|
|
|
|
cur = str(self.form.searchEdit.lineEdit().text())
|
2013-04-11 10:07:08 +02:00
|
|
|
|
if cur:
|
|
|
|
|
txt = cur + " or " + txt
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.form.searchEdit.lineEdit().setText(txt)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.onSearchActivated()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _simpleFilters(self, items):
|
|
|
|
|
ml = MenuList()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
for row in items:
|
|
|
|
|
if row is None:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addSeparator()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
else:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
label, filter = row
|
|
|
|
|
ml.addItem(label, self._filterFunc(filter))
|
|
|
|
|
return ml
|
|
|
|
|
|
|
|
|
|
def _filterFunc(self, *args):
|
|
|
|
|
return lambda *, f=args: self.setFilter(*f)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _commonFilters(self):
|
|
|
|
|
return self._simpleFilters((
|
2017-08-11 11:06:29 +02:00
|
|
|
|
(_("Whole Collection"), ""),
|
2017-08-15 10:41:36 +02:00
|
|
|
|
(_("Current Deck"), "deck:current")))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _todayFilters(self):
|
|
|
|
|
subm = SubMenu(_("Today"))
|
|
|
|
|
subm.addChild(self._simpleFilters((
|
2017-08-11 11:06:29 +02:00
|
|
|
|
(_("Added Today"), "added:1"),
|
|
|
|
|
(_("Studied Today"), "rated:1"),
|
2017-08-15 10:41:36 +02:00
|
|
|
|
(_("Again Today"), "rated:1:1"))))
|
|
|
|
|
return subm
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _cardStateFilters(self):
|
|
|
|
|
subm = SubMenu(_("Card State"))
|
|
|
|
|
subm.addChild(self._simpleFilters((
|
2017-08-11 11:06:29 +02:00
|
|
|
|
(_("New"), "is:new"),
|
|
|
|
|
(_("Learning"), "is:learn"),
|
|
|
|
|
(_("Review"), "is:review"),
|
|
|
|
|
(_("Due"), "is:due"),
|
|
|
|
|
None,
|
|
|
|
|
(_("Suspended"), "is:suspended"),
|
2017-08-12 08:08:10 +02:00
|
|
|
|
(_("Buried"), "is:buried"),
|
|
|
|
|
None,
|
|
|
|
|
(_("Red Flag"), "flag:1"),
|
|
|
|
|
(_("Purple Flag"), "flag:2"),
|
|
|
|
|
(_("Green Flag"), "flag:3"),
|
|
|
|
|
(_("Blue Flag"), "flag:4"),
|
|
|
|
|
(_("No Flag"), "flag:0"),
|
|
|
|
|
(_("Any Flag"), "-flag:0"),
|
2017-08-15 10:41:36 +02:00
|
|
|
|
)))
|
|
|
|
|
return subm
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _tagFilters(self):
|
|
|
|
|
m = SubMenu(_("Tags"))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
m.addItem(_("Clear Unused"), self.clearUnusedTags)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
m.addSeparator()
|
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
tagList = MenuList()
|
|
|
|
|
for t in sorted(self.col.tags.all(), key=lambda s: s.lower()):
|
|
|
|
|
tagList.addItem(t, self._filterFunc("tag", t))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
m.addChild(tagList.chunked())
|
|
|
|
|
return m
|
|
|
|
|
|
|
|
|
|
def _deckFilters(self):
|
2017-08-11 11:06:29 +02:00
|
|
|
|
def addDecks(parent, decks):
|
|
|
|
|
for head, did, rev, lrn, new, children in decks:
|
|
|
|
|
name = self.mw.col.decks.get(did)['name']
|
|
|
|
|
shortname = name.split("::")[-1]
|
|
|
|
|
if children:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
subm = parent.addMenu(shortname)
|
|
|
|
|
subm.addItem(_("Filter"), self._filterFunc("deck", name))
|
|
|
|
|
subm.addSeparator()
|
|
|
|
|
addDecks(subm, children)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
else:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
parent.addItem(shortname, self._filterFunc("deck", name))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
# fixme: could rewrite to avoid calculating due # in the future
|
|
|
|
|
alldecks = self.col.sched.deckDueTree()
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml = MenuList()
|
|
|
|
|
addDecks(ml, alldecks)
|
|
|
|
|
|
|
|
|
|
root = SubMenu(_("Decks"))
|
|
|
|
|
root.addChild(ml.chunked())
|
|
|
|
|
|
|
|
|
|
return root
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _noteTypeFilters(self):
|
|
|
|
|
m = SubMenu(_("Note Types"))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
m.addItem(_("Manage..."), self.mw.onNoteTypes)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
m.addSeparator()
|
2017-08-15 10:41:36 +02:00
|
|
|
|
|
|
|
|
|
noteTypes = MenuList()
|
|
|
|
|
for nt in sorted(self.col.models.all(), key=lambda nt: nt['name'].lower()):
|
2017-08-11 11:06:29 +02:00
|
|
|
|
# no sub menu if it's a single template
|
|
|
|
|
if len(nt['tmpls']) == 1:
|
2017-09-02 08:15:01 +02:00
|
|
|
|
noteTypes.addItem(nt['name'], self._filterFunc("note", nt['name']))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
else:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
subm = noteTypes.addMenu(nt['name'])
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-09-02 08:15:01 +02:00
|
|
|
|
subm.addItem(_("All Card Types"), self._filterFunc("note", nt['name']))
|
2017-08-11 11:06:29 +02:00
|
|
|
|
subm.addSeparator()
|
2017-08-15 10:41:36 +02:00
|
|
|
|
|
|
|
|
|
# add templates
|
2017-08-11 11:06:29 +02:00
|
|
|
|
for c, tmpl in enumerate(nt['tmpls']):
|
2017-08-15 10:41:36 +02:00
|
|
|
|
name = _("%(n)d: %(name)s") % dict(n=c+1, name=tmpl['name'])
|
|
|
|
|
subm.addItem(name, self._filterFunc(
|
2017-09-02 08:15:01 +02:00
|
|
|
|
"note", nt['name'], "card", str(c+1)))
|
2017-08-15 10:41:36 +02:00
|
|
|
|
|
|
|
|
|
m.addChild(noteTypes.chunked())
|
|
|
|
|
return m
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
# Favourites
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
def _savedSearches(self):
|
|
|
|
|
ml = MenuList()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
# make sure exists
|
|
|
|
|
if "savedFilters" not in self.col.conf:
|
|
|
|
|
self.col.conf['savedFilters'] = {}
|
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addSeparator()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
if self._currentFilterIsSaved():
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addItem(_("Remove Current Filter..."), self._onRemoveFilter)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
else:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addItem(_("Save Current Filter..."), self._onSaveFilter)
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
saved = self.col.conf['savedFilters']
|
2014-04-08 20:35:00 +02:00
|
|
|
|
if not saved:
|
2017-08-15 10:41:36 +02:00
|
|
|
|
return ml
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addSeparator()
|
2014-11-11 00:13:29 +01:00
|
|
|
|
for name, filt in sorted(saved.items()):
|
2017-08-15 10:41:36 +02:00
|
|
|
|
ml.addItem(name, self._filterFunc(filt))
|
|
|
|
|
|
|
|
|
|
return ml
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
def _onSaveFilter(self):
|
|
|
|
|
name = getOnlyText(_("Please give your filter a name:"))
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
filt = self.form.searchEdit.lineEdit().text()
|
|
|
|
|
self.col.conf['savedFilters'][name] = filt
|
|
|
|
|
self.col.setMod()
|
2017-08-28 13:43:44 +02:00
|
|
|
|
self.maybeRefreshSidebar()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
def _onRemoveFilter(self):
|
|
|
|
|
name = self._currentFilterIsSaved()
|
|
|
|
|
if not askUser(_("Remove %s from your saved searches?") % name):
|
|
|
|
|
return
|
|
|
|
|
del self.col.conf['savedFilters'][name]
|
|
|
|
|
self.col.setMod()
|
2017-08-28 13:43:44 +02:00
|
|
|
|
self.maybeRefreshSidebar()
|
2017-08-11 11:06:29 +02:00
|
|
|
|
|
|
|
|
|
# returns name if found
|
|
|
|
|
def _currentFilterIsSaved(self):
|
|
|
|
|
filt = self.form.searchEdit.lineEdit().text()
|
|
|
|
|
for k,v in self.col.conf['savedFilters'].items():
|
|
|
|
|
if filt == v:
|
|
|
|
|
return k
|
|
|
|
|
return None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Info
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def showCardInfo(self):
|
|
|
|
|
if not self.card:
|
|
|
|
|
return
|
|
|
|
|
info, cs = self._cardInfoData()
|
|
|
|
|
reps = self._revlogData(cs)
|
2018-06-25 03:56:27 +02:00
|
|
|
|
class CardInfoDialog(QDialog):
|
|
|
|
|
def reject(self):
|
|
|
|
|
saveGeom(self, "revlog")
|
|
|
|
|
return QDialog.reject(self)
|
|
|
|
|
d = CardInfoDialog(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
l = QVBoxLayout()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
l.setContentsMargins(0,0,0,0)
|
2014-09-15 08:04:14 +02:00
|
|
|
|
w = AnkiWebView()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
l.addWidget(w)
|
|
|
|
|
w.stdHtml(info + "<p>" + reps)
|
|
|
|
|
bb = QDialogButtonBox(QDialogButtonBox.Close)
|
|
|
|
|
l.addWidget(bb)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
bb.rejected.connect(d.reject)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
d.setLayout(l)
|
|
|
|
|
d.setWindowModality(Qt.WindowModal)
|
|
|
|
|
d.resize(500, 400)
|
|
|
|
|
restoreGeom(d, "revlog")
|
2016-05-31 10:51:40 +02:00
|
|
|
|
d.show()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def _cardInfoData(self):
|
|
|
|
|
from anki.stats import CardStats
|
|
|
|
|
cs = CardStats(self.col, self.card)
|
|
|
|
|
rep = cs.report()
|
|
|
|
|
m = self.card.model()
|
|
|
|
|
rep = """
|
|
|
|
|
<div style='width: 400px; margin: 0 auto 0;
|
|
|
|
|
border: 1px solid #000; padding: 3px; '>%s</div>""" % rep
|
|
|
|
|
return rep, cs
|
|
|
|
|
|
|
|
|
|
def _revlogData(self, cs):
|
|
|
|
|
entries = self.mw.col.db.all(
|
|
|
|
|
"select id/1000.0, ease, ivl, factor, time/1000.0, type "
|
|
|
|
|
"from revlog where cid = ?", self.card.id)
|
|
|
|
|
if not entries:
|
|
|
|
|
return ""
|
|
|
|
|
s = "<table width=100%%><tr><th align=left>%s</th>" % _("Date")
|
|
|
|
|
s += ("<th align=right>%s</th>" * 5) % (
|
|
|
|
|
_("Type"), _("Rating"), _("Interval"), _("Ease"), _("Time"))
|
|
|
|
|
cnt = 0
|
|
|
|
|
for (date, ease, ivl, factor, taken, type) in reversed(entries):
|
|
|
|
|
cnt += 1
|
|
|
|
|
s += "<tr><td>%s</td>" % time.strftime(_("<b>%Y-%m-%d</b> @ %H:%M"),
|
|
|
|
|
time.localtime(date))
|
|
|
|
|
tstr = [_("Learn"), _("Review"), _("Relearn"), _("Filtered"),
|
|
|
|
|
_("Resched")][type]
|
|
|
|
|
import anki.stats as st
|
|
|
|
|
fmt = "<span style='color:%s'>%s</span>"
|
|
|
|
|
if type == 0:
|
|
|
|
|
tstr = fmt % (st.colLearn, tstr)
|
|
|
|
|
elif type == 1:
|
|
|
|
|
tstr = fmt % (st.colMature, tstr)
|
|
|
|
|
elif type == 2:
|
|
|
|
|
tstr = fmt % (st.colRelearn, tstr)
|
|
|
|
|
elif type == 3:
|
|
|
|
|
tstr = fmt % (st.colCram, tstr)
|
|
|
|
|
else:
|
|
|
|
|
tstr = fmt % ("#000", tstr)
|
|
|
|
|
if ease == 1:
|
|
|
|
|
ease = fmt % (st.colRelearn, ease)
|
|
|
|
|
if ivl == 0:
|
|
|
|
|
ivl = _("0d")
|
|
|
|
|
elif ivl > 0:
|
|
|
|
|
ivl = fmtTimeSpan(ivl*86400, short=True)
|
|
|
|
|
else:
|
|
|
|
|
ivl = cs.time(-ivl)
|
|
|
|
|
s += ("<td align=right>%s</td>" * 5) % (
|
|
|
|
|
tstr,
|
|
|
|
|
ease, ivl,
|
|
|
|
|
"%d%%" % (factor/10) if factor else "",
|
|
|
|
|
cs.time(taken)) + "</tr>"
|
|
|
|
|
s += "</table>"
|
|
|
|
|
if cnt < self.card.reps:
|
|
|
|
|
s += _("""\
|
|
|
|
|
Note: Some of the history is missing. For more information, \
|
|
|
|
|
please see the browser documentation.""")
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
# Menu helpers
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def selectedCards(self):
|
|
|
|
|
return [self.model.cards[idx.row()] for idx in
|
|
|
|
|
self.form.tableView.selectionModel().selectedRows()]
|
|
|
|
|
|
|
|
|
|
def selectedNotes(self):
|
|
|
|
|
return self.col.db.list("""
|
|
|
|
|
select distinct nid from cards
|
|
|
|
|
where id in %s""" % ids2str(
|
|
|
|
|
[self.model.cards[idx.row()] for idx in
|
|
|
|
|
self.form.tableView.selectionModel().selectedRows()]))
|
|
|
|
|
|
|
|
|
|
def selectedNotesAsCards(self):
|
|
|
|
|
return self.col.db.list(
|
|
|
|
|
"select id from cards where nid in (%s)" %
|
|
|
|
|
",".join([str(s) for s in self.selectedNotes()]))
|
|
|
|
|
|
|
|
|
|
def oneModelNotes(self):
|
|
|
|
|
sf = self.selectedNotes()
|
|
|
|
|
if not sf:
|
|
|
|
|
return
|
|
|
|
|
mods = self.col.db.scalar("""
|
|
|
|
|
select count(distinct mid) from notes
|
|
|
|
|
where id in %s""" % ids2str(sf))
|
|
|
|
|
if mods > 1:
|
|
|
|
|
showInfo(_("Please select cards from only one note type."))
|
|
|
|
|
return
|
|
|
|
|
return sf
|
|
|
|
|
|
|
|
|
|
def onHelp(self):
|
|
|
|
|
openHelp("browser")
|
|
|
|
|
|
|
|
|
|
# Misc menu options
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def onChangeModel(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._onChangeModel)
|
|
|
|
|
|
|
|
|
|
def _onChangeModel(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
nids = self.oneModelNotes()
|
|
|
|
|
if nids:
|
|
|
|
|
ChangeModel(self, nids)
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
# Preview
|
|
|
|
|
######################################################################
|
|
|
|
|
|
2017-08-08 08:28:53 +02:00
|
|
|
|
_previewTimer = None
|
|
|
|
|
_lastPreviewRender = 0
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
def onTogglePreview(self):
|
|
|
|
|
if self._previewWindow:
|
|
|
|
|
self._closePreview()
|
|
|
|
|
else:
|
|
|
|
|
self._openPreview()
|
|
|
|
|
|
|
|
|
|
def _openPreview(self):
|
2013-05-09 08:32:30 +02:00
|
|
|
|
self._previewState = "question"
|
2013-05-22 03:08:39 +02:00
|
|
|
|
self._previewWindow = QDialog(None, Qt.Window)
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._previewWindow.setWindowTitle(_("Preview"))
|
2016-05-05 00:21:04 +02:00
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self._previewWindow.finished.connect(self._onPreviewFinished)
|
2017-09-08 11:44:13 +02:00
|
|
|
|
self._previewWindow.silentlyClose = True
|
2013-05-03 10:52:46 +02:00
|
|
|
|
vbox = QVBoxLayout()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
vbox.setContentsMargins(0,0,0,0)
|
2014-09-15 08:04:14 +02:00
|
|
|
|
self._previewWeb = AnkiWebView()
|
2013-05-03 10:52:46 +02:00
|
|
|
|
vbox.addWidget(self._previewWeb)
|
|
|
|
|
bbox = QDialogButtonBox()
|
2016-05-05 00:21:04 +02:00
|
|
|
|
|
2014-06-21 22:35:45 +02:00
|
|
|
|
self._previewReplay = bbox.addButton(_("Replay Audio"), QDialogButtonBox.ActionRole)
|
|
|
|
|
self._previewReplay.setAutoDefault(False)
|
|
|
|
|
self._previewReplay.setShortcut(QKeySequence("R"))
|
|
|
|
|
self._previewReplay.setToolTip(_("Shortcut key: %s" % "R"))
|
2016-05-05 00:21:04 +02:00
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
|
|
|
|
|
self._previewPrev.setAutoDefault(False)
|
|
|
|
|
self._previewPrev.setShortcut(QKeySequence("Left"))
|
2016-05-05 00:21:04 +02:00
|
|
|
|
self._previewPrev.setToolTip(_("Shortcut key: Left arrow"))
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._previewNext = bbox.addButton(">", QDialogButtonBox.ActionRole)
|
2016-05-05 00:21:04 +02:00
|
|
|
|
self._previewNext.setAutoDefault(True)
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._previewNext.setShortcut(QKeySequence("Right"))
|
2016-05-05 00:26:13 +02:00
|
|
|
|
self._previewNext.setToolTip(_("Shortcut key: Right arrow or Enter"))
|
2016-05-05 00:21:04 +02:00
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self._previewPrev.clicked.connect(self._onPreviewPrev)
|
|
|
|
|
self._previewNext.clicked.connect(self._onPreviewNext)
|
|
|
|
|
self._previewReplay.clicked.connect(self._onReplayAudio)
|
2016-05-05 00:21:04 +02:00
|
|
|
|
|
2017-08-28 14:16:05 +02:00
|
|
|
|
self.previewShowBothSides = QCheckBox(_("Show Both Sides"))
|
2017-08-29 16:41:01 +02:00
|
|
|
|
self.previewShowBothSides.setShortcut(QKeySequence("B"))
|
|
|
|
|
self.previewShowBothSides.setToolTip(_("Shortcut key: %s" % "B"))
|
2017-08-28 14:16:05 +02:00
|
|
|
|
bbox.addButton(self.previewShowBothSides, QDialogButtonBox.ActionRole)
|
|
|
|
|
self.previewShowBothSides.toggled.connect(self._onPreviewShowBothSides)
|
|
|
|
|
self._previewBothSides = self.col.conf.get("previewBothSides", False)
|
|
|
|
|
self.previewShowBothSides.setChecked(self._previewBothSides)
|
|
|
|
|
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._setupPreviewWebview()
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
vbox.addWidget(bbox)
|
|
|
|
|
self._previewWindow.setLayout(vbox)
|
|
|
|
|
restoreGeom(self._previewWindow, "preview")
|
|
|
|
|
self._previewWindow.show()
|
|
|
|
|
self._renderPreview(True)
|
|
|
|
|
|
|
|
|
|
def _onPreviewFinished(self, ok):
|
|
|
|
|
saveGeom(self._previewWindow, "preview")
|
|
|
|
|
self.mw.progress.timer(100, self._onClosePreview, False)
|
|
|
|
|
self.form.previewButton.setChecked(False)
|
|
|
|
|
|
|
|
|
|
def _onPreviewPrev(self):
|
2017-08-28 14:16:05 +02:00
|
|
|
|
if self._previewState == "answer" and not self._previewBothSides:
|
2017-03-24 17:09:16 +01:00
|
|
|
|
self._previewState = "question"
|
2013-05-03 10:52:46 +02:00
|
|
|
|
self._renderPreview()
|
|
|
|
|
else:
|
2017-08-01 09:46:08 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._moveCur(QAbstractItemView.MoveUp))
|
2013-05-03 10:52:46 +02:00
|
|
|
|
|
|
|
|
|
def _onPreviewNext(self):
|
|
|
|
|
if self._previewState == "question":
|
|
|
|
|
self._previewState = "answer"
|
|
|
|
|
self._renderPreview()
|
|
|
|
|
else:
|
2017-08-01 09:46:08 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._moveCur(QAbstractItemView.MoveDown))
|
2013-05-03 10:52:46 +02:00
|
|
|
|
|
2014-06-21 22:35:45 +02:00
|
|
|
|
def _onReplayAudio(self):
|
|
|
|
|
self.mw.reviewer.replayAudio(self)
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
def _updatePreviewButtons(self):
|
|
|
|
|
if not self._previewWindow:
|
|
|
|
|
return
|
2017-03-24 17:05:33 +01:00
|
|
|
|
current = self.currentRow()
|
2017-08-28 14:16:05 +02:00
|
|
|
|
canBack = (current > 0 or (current == 0 and self._previewState == "answer"
|
|
|
|
|
and not self._previewBothSides))
|
2013-05-10 08:02:58 +02:00
|
|
|
|
self._previewPrev.setEnabled(not not (self.singleCard and canBack))
|
2013-05-03 10:52:46 +02:00
|
|
|
|
canForward = self.currentRow() < self.model.rowCount(None) - 1 or \
|
|
|
|
|
self._previewState == "question"
|
2013-05-10 08:02:58 +02:00
|
|
|
|
self._previewNext.setEnabled(not not (self.singleCard and canForward))
|
2013-05-03 10:52:46 +02:00
|
|
|
|
|
|
|
|
|
def _closePreview(self):
|
|
|
|
|
if self._previewWindow:
|
|
|
|
|
self._previewWindow.close()
|
|
|
|
|
self._onClosePreview()
|
|
|
|
|
|
|
|
|
|
def _onClosePreview(self):
|
|
|
|
|
self._previewWindow = self._previewPrev = self._previewNext = None
|
|
|
|
|
|
2017-08-08 08:28:53 +02:00
|
|
|
|
def _setupPreviewWebview(self):
|
|
|
|
|
jsinc = ["jquery.js","browsersel.js",
|
|
|
|
|
"mathjax/conf.js", "mathjax/MathJax.js",
|
|
|
|
|
"reviewer.js"]
|
2017-08-10 13:39:04 +02:00
|
|
|
|
self._previewWeb.stdHtml(self.mw.reviewer.revHtml(),
|
2017-08-10 11:02:32 +02:00
|
|
|
|
css=["reviewer.css"],
|
2017-08-11 12:59:15 +02:00
|
|
|
|
js=jsinc)
|
2017-08-08 08:28:53 +02:00
|
|
|
|
|
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
def _renderPreview(self, cardChanged=False):
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._cancelPreviewTimer()
|
|
|
|
|
# avoid rendering in quick succession
|
|
|
|
|
elapMS = int((time.time() - self._lastPreviewRender)*1000)
|
|
|
|
|
if elapMS < 500:
|
|
|
|
|
self._previewTimer = self.mw.progress.timer(
|
|
|
|
|
500-elapMS, lambda: self._renderScheduledPreview(cardChanged), False)
|
|
|
|
|
else:
|
|
|
|
|
self._renderScheduledPreview(cardChanged)
|
|
|
|
|
|
|
|
|
|
def _cancelPreviewTimer(self):
|
|
|
|
|
if self._previewTimer:
|
|
|
|
|
self._previewTimer.stop()
|
|
|
|
|
self._previewTimer = None
|
|
|
|
|
|
|
|
|
|
def _renderScheduledPreview(self, cardChanged=False):
|
2017-08-08 12:02:37 +02:00
|
|
|
|
self._cancelPreviewTimer()
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._lastPreviewRender = time.time()
|
2017-08-08 12:02:37 +02:00
|
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
|
if not self._previewWindow:
|
|
|
|
|
return
|
|
|
|
|
c = self.card
|
2017-08-10 13:39:04 +02:00
|
|
|
|
func = "_showQuestion"
|
2017-08-10 11:32:19 +02:00
|
|
|
|
if not c or not self.singleCard:
|
2013-05-03 10:52:46 +02:00
|
|
|
|
txt = _("(please select 1 card)")
|
2017-08-08 08:28:53 +02:00
|
|
|
|
bodyclass = ""
|
|
|
|
|
else:
|
2017-12-04 03:45:22 +01:00
|
|
|
|
# need to force reload even if answer
|
|
|
|
|
txt = c.q(reload=True)
|
|
|
|
|
|
|
|
|
|
questionAudio = []
|
2017-08-28 14:16:05 +02:00
|
|
|
|
if self._previewBothSides:
|
|
|
|
|
self._previewState = "answer"
|
2017-12-04 03:45:22 +01:00
|
|
|
|
questionAudio = allSounds(txt)
|
2017-08-28 14:16:05 +02:00
|
|
|
|
elif cardChanged:
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._previewState = "question"
|
|
|
|
|
if self._previewState == "answer":
|
2017-08-10 13:39:04 +02:00
|
|
|
|
func = "_showAnswer"
|
2017-08-08 08:28:53 +02:00
|
|
|
|
txt = c.a()
|
2017-12-13 05:34:54 +01:00
|
|
|
|
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
|
2017-08-08 08:28:53 +02:00
|
|
|
|
|
2017-12-04 03:20:56 +01:00
|
|
|
|
bodyclass = bodyClass(self.mw.col, c)
|
2017-08-08 08:28:53 +02:00
|
|
|
|
|
|
|
|
|
clearAudioQueue()
|
|
|
|
|
if self.mw.reviewer.autoplay(c):
|
2017-12-04 03:45:22 +01:00
|
|
|
|
# if we're showing both sides at once, play question audio first
|
|
|
|
|
for audio in questionAudio:
|
|
|
|
|
play(audio)
|
|
|
|
|
# then play any audio that hasn't already been played
|
|
|
|
|
for audio in allSounds(txt):
|
|
|
|
|
if audio not in questionAudio:
|
|
|
|
|
play(audio)
|
2017-08-08 08:28:53 +02:00
|
|
|
|
|
2017-08-10 13:11:38 +02:00
|
|
|
|
txt = mungeQA(self.col, txt)
|
2017-09-06 05:02:00 +02:00
|
|
|
|
txt = runFilter("prepareQA", txt, c,
|
|
|
|
|
"preview"+self._previewState.capitalize())
|
2017-08-10 13:11:38 +02:00
|
|
|
|
|
2017-09-08 11:43:35 +02:00
|
|
|
|
self._updatePreviewButtons()
|
2017-08-08 08:28:53 +02:00
|
|
|
|
self._previewWeb.eval(
|
2018-01-16 07:07:30 +01:00
|
|
|
|
"{}({},'{}');".format(func, json.dumps(txt), bodyclass))
|
2013-05-03 10:52:46 +02:00
|
|
|
|
|
2017-08-28 14:16:05 +02:00
|
|
|
|
def _onPreviewShowBothSides(self, toggle):
|
|
|
|
|
self._previewBothSides = toggle
|
|
|
|
|
self.col.conf["previewBothSides"] = toggle
|
|
|
|
|
self.col.setMod()
|
2017-08-29 16:41:01 +02:00
|
|
|
|
if self._previewState == "answer" and not toggle:
|
|
|
|
|
self._previewState = "question"
|
|
|
|
|
self._renderPreview()
|
2017-08-28 14:16:05 +02:00
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# Card deletion
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def deleteNotes(self):
|
2018-03-01 04:31:52 +01:00
|
|
|
|
focus = self.focusWidget()
|
|
|
|
|
if focus != self.form.tableView:
|
|
|
|
|
return
|
|
|
|
|
self._deleteNotes()
|
2016-07-14 12:23:44 +02:00
|
|
|
|
|
|
|
|
|
def _deleteNotes(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
nids = self.selectedNotes()
|
|
|
|
|
if not nids:
|
|
|
|
|
return
|
|
|
|
|
self.mw.checkpoint(_("Delete Notes"))
|
|
|
|
|
self.model.beginReset()
|
2014-08-11 21:32:59 +02:00
|
|
|
|
# figure out where to place the cursor after the deletion
|
|
|
|
|
curRow = self.form.tableView.selectionModel().currentIndex().row()
|
|
|
|
|
selectedRows = [i.row() for i in
|
|
|
|
|
self.form.tableView.selectionModel().selectedRows()]
|
|
|
|
|
if min(selectedRows) < curRow < max(selectedRows):
|
|
|
|
|
# last selection in middle; place one below last selected item
|
|
|
|
|
move = sum(1 for i in selectedRows if i > curRow)
|
|
|
|
|
newRow = curRow - move
|
|
|
|
|
elif max(selectedRows) <= curRow:
|
|
|
|
|
# last selection at bottom; place one below bottommost selection
|
|
|
|
|
newRow = max(selectedRows) - len(nids) + 1
|
|
|
|
|
else:
|
|
|
|
|
# last selection at top; place one above topmost selection
|
|
|
|
|
newRow = min(selectedRows) - 1
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.col.remNotes(nids)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if len(self.model.cards):
|
2014-08-11 21:32:59 +02:00
|
|
|
|
newRow = min(newRow, len(self.model.cards) - 1)
|
|
|
|
|
newRow = max(newRow, 0)
|
|
|
|
|
self.model.focusedCard = self.model.cards[newRow]
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.model.endReset()
|
|
|
|
|
self.mw.requireReset()
|
2014-08-26 09:25:13 +02:00
|
|
|
|
tooltip(ngettext("%d note deleted.", "%d notes deleted.", len(nids)) % len(nids))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Deck change
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def setDeck(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._setDeck)
|
|
|
|
|
|
|
|
|
|
def _setDeck(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
from aqt.studydeck import StudyDeck
|
2013-02-20 07:25:59 +01:00
|
|
|
|
cids = self.selectedCards()
|
|
|
|
|
if not cids:
|
|
|
|
|
return
|
|
|
|
|
did = self.mw.col.db.scalar(
|
|
|
|
|
"select did from cards where id = ?", cids[0])
|
|
|
|
|
current=self.mw.col.decks.get(did)['name']
|
2012-12-21 08:51:59 +01:00
|
|
|
|
ret = StudyDeck(
|
2013-02-20 07:25:59 +01:00
|
|
|
|
self.mw, current=current, accept=_("Move Cards"),
|
2012-12-21 08:51:59 +01:00
|
|
|
|
title=_("Change Deck"), help="browse", parent=self)
|
|
|
|
|
if not ret.name:
|
|
|
|
|
return
|
|
|
|
|
did = self.col.decks.id(ret.name)
|
|
|
|
|
deck = self.col.decks.get(did)
|
|
|
|
|
if deck['dyn']:
|
|
|
|
|
showWarning(_("Cards can't be manually moved into a filtered deck."))
|
|
|
|
|
return
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
self.mw.checkpoint(_("Change Deck"))
|
|
|
|
|
mod = intTime()
|
|
|
|
|
usn = self.col.usn()
|
|
|
|
|
# normal cards
|
|
|
|
|
scids = ids2str(cids)
|
|
|
|
|
# remove any cards from filtered deck first
|
|
|
|
|
self.col.sched.remFromDyn(cids)
|
|
|
|
|
# then move into new deck
|
|
|
|
|
self.col.db.execute("""
|
|
|
|
|
update cards set usn=?, mod=?, did=? where id in """ + scids,
|
|
|
|
|
usn, mod, did)
|
|
|
|
|
self.model.endReset()
|
2012-12-22 01:44:52 +01:00
|
|
|
|
self.mw.requireReset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
# Tags
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def addTags(self, tags=None, label=None, prompt=None, func=None):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func))
|
|
|
|
|
|
|
|
|
|
def _addTags(self, tags, label, prompt, func):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if prompt is None:
|
|
|
|
|
prompt = _("Enter tags to add:")
|
|
|
|
|
if tags is None:
|
|
|
|
|
(tags, r) = getTag(self, self.col, prompt)
|
|
|
|
|
else:
|
|
|
|
|
r = True
|
|
|
|
|
if not r:
|
|
|
|
|
return
|
|
|
|
|
if func is None:
|
|
|
|
|
func = self.col.tags.bulkAdd
|
|
|
|
|
if label is None:
|
|
|
|
|
label = _("Add Tags")
|
|
|
|
|
if label:
|
|
|
|
|
self.mw.checkpoint(label)
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
func(self.selectedNotes(), tags)
|
|
|
|
|
self.model.endReset()
|
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
|
|
|
|
|
def deleteTags(self, tags=None, label=None):
|
|
|
|
|
if label is None:
|
|
|
|
|
label = _("Delete Tags")
|
|
|
|
|
self.addTags(tags, label, _("Enter tags to delete:"),
|
|
|
|
|
func=self.col.tags.bulkRem)
|
|
|
|
|
|
2017-08-11 06:40:51 +02:00
|
|
|
|
def clearUnusedTags(self):
|
|
|
|
|
self.editor.saveNow(self._clearUnusedTags)
|
|
|
|
|
|
|
|
|
|
def _clearUnusedTags(self):
|
|
|
|
|
self.col.tags.registerNotes()
|
|
|
|
|
|
2017-08-12 08:08:10 +02:00
|
|
|
|
# Suspending
|
2012-12-21 08:51:59 +01:00
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def isSuspended(self):
|
|
|
|
|
return not not (self.card and self.card.queue == -1)
|
|
|
|
|
|
2016-07-14 12:23:44 +02:00
|
|
|
|
def onSuspend(self):
|
|
|
|
|
self.editor.saveNow(self._onSuspend)
|
|
|
|
|
|
|
|
|
|
def _onSuspend(self):
|
|
|
|
|
sus = not self.isSuspended()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
c = self.selectedCards()
|
|
|
|
|
if sus:
|
|
|
|
|
self.col.sched.suspendCards(c)
|
|
|
|
|
else:
|
|
|
|
|
self.col.sched.unsuspendCards(c)
|
|
|
|
|
self.model.reset()
|
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
|
2017-08-16 12:30:29 +02:00
|
|
|
|
# Flags & Marking
|
2017-08-12 08:08:10 +02:00
|
|
|
|
######################################################################
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2017-08-12 08:08:10 +02:00
|
|
|
|
def onSetFlag(self, n):
|
|
|
|
|
self.col.setUserFlag(n, self.selectedCards())
|
|
|
|
|
self.model.reset()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
2017-08-16 12:30:29 +02:00
|
|
|
|
def onMark(self, mark=None):
|
|
|
|
|
if mark is None:
|
|
|
|
|
mark = not self.isMarked()
|
|
|
|
|
if mark:
|
|
|
|
|
self.addTags(tags="marked", label=False)
|
|
|
|
|
else:
|
|
|
|
|
self.deleteTags(tags="marked", label=False)
|
|
|
|
|
|
|
|
|
|
def isMarked(self):
|
|
|
|
|
return not not (self.card and self.card.note().hasTag("Marked"))
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# Repositioning
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def reposition(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._reposition)
|
|
|
|
|
|
|
|
|
|
def _reposition(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
cids = self.selectedCards()
|
|
|
|
|
cids2 = self.col.db.list(
|
|
|
|
|
"select id from cards where type = 0 and id in " + ids2str(cids))
|
|
|
|
|
if not cids2:
|
|
|
|
|
return showInfo(_("Only new cards can be repositioned."))
|
|
|
|
|
d = QDialog(self)
|
|
|
|
|
d.setWindowModality(Qt.WindowModal)
|
|
|
|
|
frm = aqt.forms.reposition.Ui_Dialog()
|
|
|
|
|
frm.setupUi(d)
|
|
|
|
|
(pmin, pmax) = self.col.db.first(
|
2014-01-14 07:06:22 +01:00
|
|
|
|
"select min(due), max(due) from cards where type=0 and odid=0")
|
|
|
|
|
pmin = pmin or 0
|
|
|
|
|
pmax = pmax or 0
|
2012-12-21 08:51:59 +01:00
|
|
|
|
txt = _("Queue top: %d") % pmin
|
|
|
|
|
txt += "\n" + _("Queue bottom: %d") % pmax
|
|
|
|
|
frm.label.setText(txt)
|
|
|
|
|
if not d.exec_():
|
|
|
|
|
return
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
self.mw.checkpoint(_("Reposition"))
|
|
|
|
|
self.col.sched.sortCards(
|
|
|
|
|
cids, start=frm.start.value(), step=frm.step.value(),
|
|
|
|
|
shuffle=frm.randomize.isChecked(), shift=frm.shift.isChecked())
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
self.model.endReset()
|
|
|
|
|
|
|
|
|
|
# Rescheduling
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def reschedule(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._reschedule)
|
|
|
|
|
|
|
|
|
|
def _reschedule(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
d = QDialog(self)
|
|
|
|
|
d.setWindowModality(Qt.WindowModal)
|
|
|
|
|
frm = aqt.forms.reschedule.Ui_Dialog()
|
|
|
|
|
frm.setupUi(d)
|
|
|
|
|
if not d.exec_():
|
|
|
|
|
return
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
self.mw.checkpoint(_("Reschedule"))
|
|
|
|
|
if frm.asNew.isChecked():
|
|
|
|
|
self.col.sched.forgetCards(self.selectedCards())
|
|
|
|
|
else:
|
2013-05-17 09:27:58 +02:00
|
|
|
|
fmin = frm.min.value()
|
|
|
|
|
fmax = frm.max.value()
|
|
|
|
|
fmax = max(fmin, fmax)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.col.sched.reschedCards(
|
2013-05-17 09:27:58 +02:00
|
|
|
|
self.selectedCards(), fmin, fmax)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
self.model.endReset()
|
|
|
|
|
|
|
|
|
|
# Edit: selection
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def selectNotes(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._selectNotes)
|
|
|
|
|
|
|
|
|
|
def _selectNotes(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
nids = self.selectedNotes()
|
2016-07-14 12:23:44 +02:00
|
|
|
|
# bypass search history
|
|
|
|
|
self._lastSearchTxt = "nid:"+",".join([str(x) for x in nids])
|
|
|
|
|
self.form.searchEdit.lineEdit().setText(self._lastSearchTxt)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# clear the selection so we don't waste energy preserving it
|
|
|
|
|
tv = self.form.tableView
|
|
|
|
|
tv.selectionModel().clear()
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
tv.selectAll()
|
|
|
|
|
|
|
|
|
|
def invertSelection(self):
|
|
|
|
|
sm = self.form.tableView.selectionModel()
|
|
|
|
|
items = sm.selection()
|
|
|
|
|
self.form.tableView.selectAll()
|
|
|
|
|
sm.select(items, QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
|
|
|
|
|
|
|
|
|
|
# Edit: undo
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def setupHooks(self):
|
|
|
|
|
addHook("undoState", self.onUndoState)
|
|
|
|
|
addHook("reset", self.onReset)
|
|
|
|
|
addHook("editTimer", self.refreshCurrentCard)
|
|
|
|
|
addHook("editFocusLost", self.refreshCurrentCardFilter)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
for t in "newTag", "newModel", "newDeck":
|
|
|
|
|
addHook(t, self.maybeRefreshSidebar)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def teardownHooks(self):
|
|
|
|
|
remHook("reset", self.onReset)
|
|
|
|
|
remHook("editTimer", self.refreshCurrentCard)
|
2013-04-09 23:58:04 +02:00
|
|
|
|
remHook("editFocusLost", self.refreshCurrentCardFilter)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
remHook("undoState", self.onUndoState)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
for t in "newTag", "newModel", "newDeck":
|
|
|
|
|
remHook(t, self.maybeRefreshSidebar)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onUndoState(self, on):
|
|
|
|
|
self.form.actionUndo.setEnabled(on)
|
|
|
|
|
if on:
|
|
|
|
|
self.form.actionUndo.setText(self.mw.form.actionUndo.text())
|
|
|
|
|
|
|
|
|
|
# Edit: replacing
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def onFindReplace(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._onFindReplace)
|
|
|
|
|
|
|
|
|
|
def _onFindReplace(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
sf = self.selectedNotes()
|
|
|
|
|
if not sf:
|
|
|
|
|
return
|
|
|
|
|
import anki.find
|
|
|
|
|
fields = sorted(anki.find.fieldNames(self.col, downcase=False))
|
|
|
|
|
d = QDialog(self)
|
|
|
|
|
frm = aqt.forms.findreplace.Ui_Dialog()
|
|
|
|
|
frm.setupUi(d)
|
|
|
|
|
d.setWindowModality(Qt.WindowModal)
|
|
|
|
|
frm.field.addItems([_("All Fields")] + fields)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
frm.buttonBox.helpRequested.connect(self.onFindReplaceHelp)
|
2014-06-18 20:47:45 +02:00
|
|
|
|
restoreGeom(d, "findreplace")
|
|
|
|
|
r = d.exec_()
|
|
|
|
|
saveGeom(d, "findreplace")
|
|
|
|
|
if not r:
|
2012-12-21 08:51:59 +01:00
|
|
|
|
return
|
|
|
|
|
if frm.field.currentIndex() == 0:
|
|
|
|
|
field = None
|
|
|
|
|
else:
|
|
|
|
|
field = fields[frm.field.currentIndex()-1]
|
|
|
|
|
self.mw.checkpoint(_("Find and Replace"))
|
|
|
|
|
self.mw.progress.start()
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
try:
|
|
|
|
|
changed = self.col.findReplace(sf,
|
2016-05-12 06:45:35 +02:00
|
|
|
|
str(frm.find.text()),
|
|
|
|
|
str(frm.replace.text()),
|
2012-12-21 08:51:59 +01:00
|
|
|
|
frm.re.isChecked(),
|
|
|
|
|
field,
|
|
|
|
|
frm.ignoreCase.isChecked())
|
|
|
|
|
except sre_constants.error:
|
|
|
|
|
showInfo(_("Invalid regular expression."), parent=self)
|
|
|
|
|
return
|
|
|
|
|
else:
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
finally:
|
|
|
|
|
self.model.endReset()
|
|
|
|
|
self.mw.progress.finish()
|
|
|
|
|
showInfo(ngettext(
|
|
|
|
|
"%(a)d of %(b)d note updated",
|
|
|
|
|
"%(a)d of %(b)d notes updated", len(sf)) % {
|
|
|
|
|
'a': changed,
|
|
|
|
|
'b': len(sf),
|
2018-07-25 12:00:08 +02:00
|
|
|
|
}, parent=self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onFindReplaceHelp(self):
|
|
|
|
|
openHelp("findreplace")
|
|
|
|
|
|
|
|
|
|
# Edit: finding dupes
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def onFindDupes(self):
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._onFindDupes)
|
|
|
|
|
|
|
|
|
|
def _onFindDupes(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
d = QDialog(self)
|
2016-07-04 05:22:35 +02:00
|
|
|
|
self.mw.setupDialogGC(d)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
frm = aqt.forms.finddupes.Ui_Dialog()
|
|
|
|
|
frm.setupUi(d)
|
|
|
|
|
restoreGeom(d, "findDupes")
|
|
|
|
|
fields = sorted(anki.find.fieldNames(self.col, downcase=False))
|
|
|
|
|
frm.fields.addItems(fields)
|
2013-10-03 17:07:11 +02:00
|
|
|
|
self._dupesButton = None
|
2012-12-21 08:51:59 +01:00
|
|
|
|
# links
|
2016-06-06 07:50:03 +02:00
|
|
|
|
frm.webView.onBridgeCmd = self.dupeLinkClicked
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def onFin(code):
|
|
|
|
|
saveGeom(d, "findDupes")
|
2016-05-31 10:51:40 +02:00
|
|
|
|
d.finished.connect(onFin)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def onClick():
|
|
|
|
|
field = fields[frm.fields.currentIndex()]
|
2013-10-03 17:07:11 +02:00
|
|
|
|
self.duplicatesReport(frm.webView, field, frm.search.text(), frm)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
search = frm.buttonBox.addButton(
|
|
|
|
|
_("Search"), QDialogButtonBox.ActionRole)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
search.clicked.connect(onClick)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
d.show()
|
|
|
|
|
|
2013-10-03 17:07:11 +02:00
|
|
|
|
def duplicatesReport(self, web, fname, search, frm):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.mw.progress.start()
|
|
|
|
|
res = self.mw.col.findDupes(fname, search)
|
2013-10-03 17:07:11 +02:00
|
|
|
|
if not self._dupesButton:
|
|
|
|
|
self._dupesButton = b = frm.buttonBox.addButton(
|
|
|
|
|
_("Tag Duplicates"), QDialogButtonBox.ActionRole)
|
2016-05-31 10:51:40 +02:00
|
|
|
|
b.clicked.connect(lambda: self._onTagDupes(res))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
t = "<html><body>"
|
|
|
|
|
groups = len(res)
|
|
|
|
|
notes = sum(len(r[1]) for r in res)
|
|
|
|
|
part1 = ngettext("%d group", "%d groups", groups) % groups
|
|
|
|
|
part2 = ngettext("%d note", "%d notes", notes) % notes
|
|
|
|
|
t += _("Found %(a)s across %(b)s.") % dict(a=part1, b=part2)
|
|
|
|
|
t += "<p><ol>"
|
|
|
|
|
for val, nids in res:
|
2018-02-03 08:11:32 +01:00
|
|
|
|
t += '''<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>''' % (
|
2012-12-21 08:51:59 +01:00
|
|
|
|
"nid:" + ",".join(str(id) for id in nids),
|
|
|
|
|
ngettext("%d note", "%d notes", len(nids)) % len(nids),
|
2017-12-11 07:20:00 +01:00
|
|
|
|
html.escape(val))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
t += "</ol>"
|
|
|
|
|
t += "</body></html>"
|
|
|
|
|
web.setHtml(t)
|
|
|
|
|
self.mw.progress.finish()
|
|
|
|
|
|
2013-10-03 17:07:11 +02:00
|
|
|
|
def _onTagDupes(self, res):
|
|
|
|
|
if not res:
|
|
|
|
|
return
|
|
|
|
|
self.model.beginReset()
|
|
|
|
|
self.mw.checkpoint(_("Tag Duplicates"))
|
|
|
|
|
nids = set()
|
|
|
|
|
for s, nidlist in res:
|
|
|
|
|
nids.update(nidlist)
|
|
|
|
|
self.col.tags.bulkAdd(nids, _("duplicate"))
|
|
|
|
|
self.mw.progress.finish()
|
|
|
|
|
self.model.endReset()
|
|
|
|
|
self.mw.requireReset()
|
|
|
|
|
tooltip(_("Notes tagged."))
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
def dupeLinkClicked(self, link):
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.form.searchEdit.lineEdit().setText(link)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
# manually, because we've already saved
|
|
|
|
|
self._lastSearchTxt = link
|
|
|
|
|
self.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.onNote()
|
|
|
|
|
|
|
|
|
|
# Jumping
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
def _moveCur(self, dir=None, idx=None):
|
|
|
|
|
if not self.model.cards:
|
|
|
|
|
return
|
|
|
|
|
tv = self.form.tableView
|
|
|
|
|
if idx is None:
|
|
|
|
|
idx = tv.moveCursor(dir, self.mw.app.keyboardModifiers())
|
2017-08-05 07:15:19 +02:00
|
|
|
|
tv.selectionModel().setCurrentIndex(
|
|
|
|
|
idx,
|
|
|
|
|
QItemSelectionModel.Clear|
|
|
|
|
|
QItemSelectionModel.Select|
|
|
|
|
|
QItemSelectionModel.Rows)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onPreviousCard(self):
|
2017-08-05 07:15:19 +02:00
|
|
|
|
self.focusTo = self.editor.currentField
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._onPreviousCard)
|
|
|
|
|
|
|
|
|
|
def _onPreviousCard(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self._moveCur(QAbstractItemView.MoveUp)
|
|
|
|
|
|
|
|
|
|
def onNextCard(self):
|
2017-08-05 07:15:19 +02:00
|
|
|
|
self.focusTo = self.editor.currentField
|
2016-07-14 12:23:44 +02:00
|
|
|
|
self.editor.saveNow(self._onNextCard)
|
|
|
|
|
|
|
|
|
|
def _onNextCard(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self._moveCur(QAbstractItemView.MoveDown)
|
|
|
|
|
|
|
|
|
|
def onFirstCard(self):
|
|
|
|
|
sm = self.form.tableView.selectionModel()
|
|
|
|
|
idx = sm.currentIndex()
|
|
|
|
|
self._moveCur(None, self.model.index(0, 0))
|
|
|
|
|
if not self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
|
|
|
|
|
return
|
|
|
|
|
idx2 = sm.currentIndex()
|
|
|
|
|
item = QItemSelection(idx2, idx)
|
|
|
|
|
sm.select(item, QItemSelectionModel.SelectCurrent|
|
|
|
|
|
QItemSelectionModel.Rows)
|
|
|
|
|
|
|
|
|
|
def onLastCard(self):
|
|
|
|
|
sm = self.form.tableView.selectionModel()
|
|
|
|
|
idx = sm.currentIndex()
|
|
|
|
|
self._moveCur(
|
|
|
|
|
None, self.model.index(len(self.model.cards) - 1, 0))
|
|
|
|
|
if not self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
|
|
|
|
|
return
|
|
|
|
|
idx2 = sm.currentIndex()
|
|
|
|
|
item = QItemSelection(idx, idx2)
|
|
|
|
|
sm.select(item, QItemSelectionModel.SelectCurrent|
|
|
|
|
|
QItemSelectionModel.Rows)
|
|
|
|
|
|
|
|
|
|
def onFind(self):
|
|
|
|
|
self.form.searchEdit.setFocus()
|
|
|
|
|
self.form.searchEdit.lineEdit().selectAll()
|
|
|
|
|
|
|
|
|
|
def onNote(self):
|
2013-01-29 01:49:04 +01:00
|
|
|
|
self.editor.web.setFocus()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
|
|
def onCardList(self):
|
|
|
|
|
self.form.tableView.setFocus()
|
|
|
|
|
|
|
|
|
|
def focusCid(self, cid):
|
|
|
|
|
try:
|
|
|
|
|
row = self.model.cards.index(cid)
|
|
|
|
|
except:
|
|
|
|
|
return
|
|
|
|
|
self.form.tableView.selectRow(row)
|
|
|
|
|
|
|
|
|
|
# Change model dialog
|
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
|
|
class ChangeModel(QDialog):
|
|
|
|
|
|
|
|
|
|
def __init__(self, browser, nids):
|
|
|
|
|
QDialog.__init__(self, browser)
|
|
|
|
|
self.browser = browser
|
|
|
|
|
self.nids = nids
|
|
|
|
|
self.oldModel = browser.card.note().model()
|
|
|
|
|
self.form = aqt.forms.changemodel.Ui_Dialog()
|
|
|
|
|
self.form.setupUi(self)
|
|
|
|
|
self.setWindowModality(Qt.WindowModal)
|
|
|
|
|
self.setup()
|
|
|
|
|
restoreGeom(self, "changeModel")
|
|
|
|
|
addHook("reset", self.onReset)
|
|
|
|
|
addHook("currentModelChanged", self.onReset)
|
|
|
|
|
self.exec_()
|
|
|
|
|
|
|
|
|
|
def setup(self):
|
|
|
|
|
# maps
|
|
|
|
|
self.flayout = QHBoxLayout()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.flayout.setContentsMargins(0,0,0,0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.fwidg = None
|
|
|
|
|
self.form.fieldMap.setLayout(self.flayout)
|
|
|
|
|
self.tlayout = QHBoxLayout()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.tlayout.setContentsMargins(0,0,0,0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.twidg = None
|
|
|
|
|
self.form.templateMap.setLayout(self.tlayout)
|
|
|
|
|
if self.style().objectName() == "gtk+":
|
|
|
|
|
# gtk+ requires margins in inner layout
|
|
|
|
|
self.form.verticalLayout_2.setContentsMargins(0, 11, 0, 0)
|
|
|
|
|
self.form.verticalLayout_3.setContentsMargins(0, 11, 0, 0)
|
|
|
|
|
# model chooser
|
|
|
|
|
import aqt.modelchooser
|
|
|
|
|
self.oldModel = self.browser.col.models.get(
|
|
|
|
|
self.browser.col.db.scalar(
|
|
|
|
|
"select mid from notes where id = ?", self.nids[0]))
|
|
|
|
|
self.form.oldModelLabel.setText(self.oldModel['name'])
|
|
|
|
|
self.modelChooser = aqt.modelchooser.ModelChooser(
|
|
|
|
|
self.browser.mw, self.form.modelChooserWidget, label=False)
|
|
|
|
|
self.modelChooser.models.setFocus()
|
2016-05-31 10:51:40 +02:00
|
|
|
|
self.form.buttonBox.helpRequested.connect(self.onHelp)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
self.modelChanged(self.browser.mw.col.models.current())
|
|
|
|
|
self.pauseUpdate = False
|
|
|
|
|
|
|
|
|
|
def onReset(self):
|
|
|
|
|
self.modelChanged(self.browser.col.models.current())
|
|
|
|
|
|
|
|
|
|
def modelChanged(self, model):
|
|
|
|
|
self.targetModel = model
|
|
|
|
|
self.rebuildTemplateMap()
|
|
|
|
|
self.rebuildFieldMap()
|
|
|
|
|
|
|
|
|
|
def rebuildTemplateMap(self, key=None, attr=None):
|
|
|
|
|
if not key:
|
|
|
|
|
key = "t"
|
|
|
|
|
attr = "tmpls"
|
|
|
|
|
map = getattr(self, key + "widg")
|
|
|
|
|
lay = getattr(self, key + "layout")
|
|
|
|
|
src = self.oldModel[attr]
|
|
|
|
|
dst = 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")]
|
|
|
|
|
indices = {}
|
|
|
|
|
for i, x in enumerate(src):
|
|
|
|
|
l.addWidget(QLabel(_("Change %s to:") % x['name']), i, 0)
|
|
|
|
|
cb = QComboBox()
|
|
|
|
|
cb.addItems(targets)
|
|
|
|
|
idx = min(i, len(targets)-1)
|
|
|
|
|
cb.setCurrentIndex(idx)
|
|
|
|
|
indices[cb] = idx
|
2016-05-31 10:51:40 +02:00
|
|
|
|
cb.currentIndexChanged.connect(
|
|
|
|
|
lambda i, cb=cb, key=key: self.onComboChanged(i, cb, key))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
combos.append(cb)
|
|
|
|
|
l.addWidget(cb, i, 1)
|
|
|
|
|
map.setLayout(l)
|
|
|
|
|
lay.addWidget(map)
|
|
|
|
|
setattr(self, key + "widg", map)
|
|
|
|
|
setattr(self, key + "layout", lay)
|
|
|
|
|
setattr(self, key + "combos", combos)
|
|
|
|
|
setattr(self, key + "indices", indices)
|
|
|
|
|
|
|
|
|
|
def rebuildFieldMap(self):
|
|
|
|
|
return self.rebuildTemplateMap(key="f", attr="flds")
|
|
|
|
|
|
|
|
|
|
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['tmpls']
|
|
|
|
|
combos = self.tcombos
|
|
|
|
|
new = self.targetModel['tmpls']
|
|
|
|
|
map = {}
|
|
|
|
|
for i, f in enumerate(old):
|
|
|
|
|
idx = combos[i].currentIndex()
|
|
|
|
|
if idx == len(new):
|
|
|
|
|
# ignore
|
|
|
|
|
map[f['ord']] = None
|
|
|
|
|
else:
|
|
|
|
|
f2 = new[idx]
|
|
|
|
|
map[f['ord']] = f2['ord']
|
|
|
|
|
return map
|
|
|
|
|
|
|
|
|
|
def getFieldMap(self):
|
|
|
|
|
return self.getTemplateMap(
|
|
|
|
|
old=self.oldModel['flds'],
|
|
|
|
|
combos=self.fcombos,
|
|
|
|
|
new=self.targetModel['flds'])
|
|
|
|
|
|
|
|
|
|
def cleanup(self):
|
|
|
|
|
remHook("reset", self.onReset)
|
|
|
|
|
remHook("currentModelChanged", self.onReset)
|
|
|
|
|
self.modelChooser.cleanup()
|
|
|
|
|
saveGeom(self, "changeModel")
|
|
|
|
|
|
|
|
|
|
def reject(self):
|
|
|
|
|
self.cleanup()
|
|
|
|
|
return QDialog.reject(self)
|
|
|
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
|
# check maps
|
|
|
|
|
fmap = self.getFieldMap()
|
|
|
|
|
cmap = self.getTemplateMap()
|
2016-05-12 06:45:35 +02:00
|
|
|
|
if any(True for c in list(cmap.values()) if c is None):
|
2012-12-21 08:51:59 +01:00
|
|
|
|
if not askUser(_("""\
|
|
|
|
|
Any cards mapped to nothing will be deleted. \
|
|
|
|
|
If a note has no remaining cards, it will be lost. \
|
|
|
|
|
Are you sure you want to continue?""")):
|
|
|
|
|
return
|
|
|
|
|
QDialog.accept(self)
|
|
|
|
|
self.browser.mw.checkpoint(_("Change Note Type"))
|
|
|
|
|
b = self.browser
|
2016-07-14 13:49:40 +02:00
|
|
|
|
b.mw.col.modSchema(check=True)
|
|
|
|
|
b.mw.progress.start()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
b.model.beginReset()
|
|
|
|
|
mm = b.mw.col.models
|
|
|
|
|
mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
b.search()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
b.model.endReset()
|
|
|
|
|
b.mw.progress.finish()
|
|
|
|
|
b.mw.reset()
|
|
|
|
|
self.cleanup()
|
|
|
|
|
|
|
|
|
|
def onHelp(self):
|
|
|
|
|
openHelp("browsermisc")
|
|
|
|
|
|