anki/qt/aqt/browser.py

2431 lines
81 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2017-12-11 07:20:00 +01:00
import html
2019-12-20 10:19:03 +01:00
import json
2013-05-03 10:52:46 +02:00
import re
2019-12-20 10:19:03 +01:00
import sre_constants
import time
import unicodedata
2019-12-20 10:19:03 +01:00
from operator import itemgetter
from typing import Callable, List, Optional, Union
2019-12-20 10:19:03 +01:00
import anki
import aqt.forms
2020-01-15 04:49:26 +01:00
from anki import hooks
from anki.cards import Card
2019-12-20 08:55:19 +01:00
from anki.collection import _Collection
2019-12-20 10:19:03 +01:00
from anki.consts import *
from anki.lang import _, ngettext
from anki.models import NoteType
from anki.notes import Note
from anki.utils import fmtTimeSpan, htmlToTextLine, ids2str, intTime, isMac, isWin
from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor
2013-05-03 10:52:46 +02:00
from aqt.qt import *
2020-01-20 11:10:38 +01:00
from aqt.sound import av_player
from aqt.theme import theme_manager
2019-12-23 01:34:10 +01:00
from aqt.utils import (
MenuList,
SubMenu,
askUser,
getOnlyText,
getTag,
openHelp,
qtMenuShortcutWorkaround,
restoreGeom,
restoreHeader,
restoreSplitter,
restoreState,
saveGeom,
saveHeader,
saveSplitter,
saveState,
shortcut,
showInfo,
showWarning,
tooltip,
)
from aqt.webview import AnkiWebView
# Data model
##########################################################################
2019-12-23 01:34:10 +01:00
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(
2019-12-23 01:34:10 +01:00
"activeCols", ["noteFld", "template", "cardDue", "deck"]
)
self.cards = []
self.cardObjs = {}
def getCard(self, index):
id = self.cards[index.row()]
if not id in self.cardObjs:
self.cardObjs[id] = self.col.getCard(id)
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:
self.layoutChanged.emit()
# Model interface
######################################################################
def rowCount(self, parent):
2017-02-23 07:21:00 +01:00
if parent and parent.isValid():
return 0
return len(self.cards)
def columnCount(self, parent):
2017-02-23 07:21:00 +01:00
if parent and parent.isValid():
return 0
return len(self.activeCols)
def data(self, index, role):
if not index.isValid():
return
if role == Qt.FontRole:
2019-12-23 01:34:10 +01:00
if self.activeCols[index.column()] not in ("question", "answer", "noteFld"):
return
row = index.row()
c = self.getCard(index)
t = c.template()
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))
return f
elif role == Qt.TextAlignmentRole:
align = Qt.AlignVCenter
2019-12-23 01:34:10 +01:00
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)
txt = None
for stype, name in self.browser.columns:
if type == stype:
txt = name
break
# give the user a hint an invalid column was added by an add-on
if not txt:
txt = _("Add-on")
return txt
else:
return
def flags(self, index):
2019-12-23 01:34:10 +01:00
return Qt.ItemFlag(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
# Filtering
######################################################################
def search(self, txt):
self.beginReset()
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
2019-12-23 01:34:10 +01:00
# print "fetch cards in %dms" % ((time.time() - t)*1000)
self.endReset()
2017-02-02 12:00:58 +01:00
if invalid:
showWarning(_("Invalid search - please check for typing mistakes."))
def reset(self):
self.beginReset()
self.endReset()
# caller must have called editor.saveNow() before calling this or .reset()
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):
self.browser.editor.saveNow(self._reverse)
def _reverse(self):
self.beginReset()
self.cards.reverse()
self.endReset()
def saveSelection(self):
cards = self.browser.selectedCards()
self.selectedCards = dict([(id, True) for id in cards])
2019-12-23 01:34:10 +01:00
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())
# scroll if the selection count has changed
if count != len(self.selectedCards):
# we save and then restore the horizontal scroll position because
# scrollTo() also scrolls horizontally which is confusing
h = tv.horizontalScrollBar().value()
tv.scrollTo(idx, tv.PositionAtCenter)
tv.horizontalScrollBar().setValue(h)
if count < 500:
# discard large selections; they're too slow
2019-12-23 01:34:10 +01:00
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())])
elif type == "template":
2019-12-23 01:34:10 +01:00
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":
2019-12-23 01:34:10 +01:00
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)
elif type == "note":
2019-12-23 01:34:10 +01:00
return c.model()["name"]
elif type == "cardIvl":
if c.type == 0:
return _("(new)")
elif c.type == 1:
return _("(learning)")
2019-12-23 01:34:10 +01:00
return fmtTimeSpan(c.ivl * 86400)
elif type == "cardEase":
if c.type == 0:
return _("(new)")
2019-12-23 01:34:10 +01:00
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),
2019-12-23 01:34:10 +01:00
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))
def answer(self, c):
2019-12-23 01:34:10 +01:00
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())
# need to strip question from answer
q = self.question(c)
2017-03-14 07:48:40 +01:00
a = htmlToTextLine(c.a())
if a.startswith(q):
2019-12-23 01:34:10 +01:00
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)
2019-12-23 01:34:10 +01:00
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))
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()
2019-12-23 01:34:10 +01:00
return nt["flds"][self.col.models.sortIdx(nt)]["rtl"]
# Line painter
######################################################################
2019-12-23 01:34:10 +01:00
class StatusDelegate(QItemDelegate):
def __init__(self, browser, model):
QItemDelegate.__init__(self, browser)
self.browser = browser
self.model = model
def paint(self, painter, option, index):
self.browser.mw.progress.blockUpdates = True
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
finally:
self.browser.mw.progress.blockUpdates = True
if self.model.isRTL(index):
option.direction = Qt.RightToLeft
col = None
if c.userFlag() > 0:
col = theme_manager.qcolor(f"flag{c.userFlag()}-bg")
elif c.note().hasTag("Marked"):
col = theme_manager.qcolor("marked-bg")
elif c.queue == -1:
col = theme_manager.qcolor("suspended-bg")
if col:
brush = QBrush(col)
painter.save()
painter.fillRect(option.rect, brush)
painter.restore()
return QItemDelegate.paint(self, painter, option, index)
2019-12-23 01:34:10 +01:00
# Sidebar
######################################################################
2019-12-23 01:34:10 +01:00
class SidebarItem:
2019-12-23 01:34:10 +01:00
def __init__(
self,
name: str,
icon: str,
onClick: Callable[[], None] = None,
onExpanded: Callable[[bool], None] = None,
expanded: bool = False,
) -> None:
self.name = name
self.icon = icon
2019-12-20 00:30:35 +01:00
self.onClick = onClick
self.onExpanded = onExpanded
self.expanded = expanded
self.children: List["SidebarItem"] = []
self.parentItem: Optional[SidebarItem] = None
2019-12-21 07:44:38 +01:00
self.tooltip: Optional[str] = None
def addChild(self, cb: "SidebarItem") -> None:
self.children.append(cb)
cb.parentItem = self
def rowForChild(self, child: "SidebarItem") -> Optional[int]:
try:
return self.children.index(child)
except ValueError:
return None
2019-12-23 01:34:10 +01:00
class SidebarModel(QAbstractItemModel):
def __init__(self, root: SidebarItem) -> None:
super().__init__()
self.root = root
2019-12-20 00:27:14 +01:00
# Qt API
######################################################################
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if not parent.isValid():
return len(self.root.children)
else:
item: SidebarItem = parent.internalPointer()
return len(item.children)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
return 1
2019-12-23 01:34:10 +01:00
def index(
self, row: int, column: int, parent: QModelIndex = QModelIndex()
) -> QModelIndex:
if not self.hasIndex(row, column, parent):
return QModelIndex()
parentItem: SidebarItem
if not parent.isValid():
parentItem = self.root
else:
parentItem = parent.internalPointer()
item = parentItem.children[row]
return self.createIndex(row, column, item)
2019-12-23 01:34:10 +01:00
def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
if not child.isValid():
return QModelIndex()
childItem: SidebarItem = child.internalPointer()
parentItem = childItem.parentItem
if parentItem is None or parentItem == self.root:
return QModelIndex()
row = parentItem.rowForChild(childItem)
if row is None:
return QModelIndex()
return self.createIndex(row, 0, parentItem)
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant:
if not index.isValid():
return QVariant()
if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole):
return QVariant()
item: SidebarItem = index.internalPointer()
if role == Qt.DisplayRole:
return QVariant(item.name)
elif role == Qt.ToolTipRole:
return QVariant(item.tooltip)
else:
return QVariant(theme_manager.icon_from_resources(item.icon))
2019-12-20 00:27:14 +01:00
# Helpers
######################################################################
2020-01-23 07:36:50 +01:00
def iconFromRef(self, iconRef: str) -> QIcon:
print("iconFromRef() deprecated")
return theme_manager.icon_from_resources(iconRef)
2019-12-20 00:27:14 +01:00
def expandWhereNeccessary(self, tree: QTreeView) -> None:
for row, child in enumerate(self.root.children):
if child.expanded:
idx = self.index(row, 0, QModelIndex())
self._expandWhereNeccessary(idx, tree)
def _expandWhereNeccessary(self, parent: QModelIndex, tree: QTreeView) -> None:
2019-12-20 01:21:20 +01:00
parentItem: SidebarItem
2019-12-20 00:27:14 +01:00
if not parent.isValid():
parentItem = self.root
else:
2019-12-20 01:21:20 +01:00
parentItem = parent.internalPointer()
2019-12-20 00:27:14 +01:00
# nothing to do?
if not parentItem.expanded:
return
# expand children
for row, child in enumerate(parentItem.children):
if not child.expanded:
continue
childIdx = self.index(row, 0, parent)
self._expandWhereNeccessary(childIdx, tree)
# then ourselves
tree.setExpanded(parent, True)
2019-12-23 01:34:10 +01:00
# Browser window
######################################################################
# fixme: respond to reset+edit hooks
2019-12-23 01:34:10 +01:00
class Browser(QMainWindow):
2019-12-20 08:55:19 +01:00
model: DataModel
mw: AnkiQt
col: _Collection
editor: Optional[Editor]
def __init__(self, mw: AnkiQt) -> None:
QMainWindow.__init__(self, None, Qt.Window)
self.mw = mw
self.col = self.mw.col
self.lastFilter = ""
self.focusTo = None
2013-05-03 10:52:46 +02:00
self._previewWindow = None
self._closeEventHasCleanedUp = False
self.form = aqt.forms.browser.Ui_Dialog()
self.form.setupUi(self)
self.setupSidebar()
restoreGeom(self, "editor", 0)
restoreState(self, "editor")
restoreSplitter(self.form.splitter, "editor3")
self.form.splitter.setChildrenCollapsible(False)
self.card: Optional[Card] = None
self.setupColumns()
self.setupTable()
self.setupMenus()
self.setupHeaders()
self.setupHooks()
self.setupEditor()
self.updateFont()
self.onUndoState(self.mw.form.actionUndo.isEnabled())
self.setupSearch()
self.show()
def setupMenus(self) -> None:
2019-03-04 07:01:10 +01:00
# pylint: disable=unnecessary-lambda
# actions
f = self.form
f.previewButton.clicked.connect(self.onTogglePreview)
2019-12-23 01:34:10 +01:00
f.previewButton.setToolTip(
_("Preview Selected Card (%s)") % shortcut(_("Ctrl+Shift+P"))
)
f.previewButton.setShortcut("Ctrl+Shift+P")
f.filter.clicked.connect(self.onFilterButton)
# edit
f.actionUndo.triggered.connect(self.mw.onUndo)
f.actionInvertSelection.triggered.connect(self.invertSelection)
f.actionSelectNotes.triggered.connect(self.selectNotes)
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())
f.actionClear_Unused_Tags.triggered.connect(self.clearUnusedTags)
f.actionToggle_Mark.triggered.connect(lambda: self.onMark())
f.actionChangeModel.triggered.connect(self.onChangeModel)
f.actionFindDuplicates.triggered.connect(self.onFindDupes)
f.actionFindReplace.triggered.connect(self.onFindReplace)
2017-08-15 10:49:41 +02:00
f.actionManage_Note_Types.triggered.connect(self.mw.onNoteTypes)
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)
f.actionRed_Flag.triggered.connect(lambda: self.onSetFlag(1))
2018-11-01 05:58:41 +01:00
f.actionOrange_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))
# jumps
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)
f.actionTags.triggered.connect(self.onFilterButton)
f.actionSidebar.triggered.connect(self.focusSidebar)
f.actionCardList.triggered.connect(self.onCardList)
# help
f.actionGuide.triggered.connect(self.onHelp)
# keyboard shortcut for shift+home/end
self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self)
qconnect(self.pgUpCut.activated, self.onFirstCard)
self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self)
qconnect(self.pgDownCut.activated, self.onLastCard)
# add-on hook
gui_hooks.browser_menus_did_init(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) -> None:
2017-12-14 05:49:51 +01:00
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)
gui_hooks.browser_will_show_context_menu(self, m)
qtMenuShortcutWorkaround(m)
2017-12-14 05:49:51 +01:00
m.exec_(QCursor.pos())
def updateFont(self):
# 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():
2019-12-23 01:34:10 +01:00
for t in m["tmpls"]:
bsize = t.get("bsize", 0)
if bsize > curmax:
curmax = bsize
2019-12-23 01:34:10 +01:00
self.form.tableView.verticalHeader().setDefaultSectionSize(curmax + 6)
def closeEvent(self, evt):
if self._closeEventHasCleanedUp:
evt.accept()
return
self.editor.saveNow(self._closeWindow)
evt.ignore()
def _closeWindow(self):
self._cancelPreviewTimer()
self.editor.cleanup()
saveSplitter(self.form.splitter, "editor3")
saveGeom(self, "editor")
saveState(self, "editor")
saveHeader(self.form.tableView.horizontalHeader(), "editor")
2019-12-23 01:34:10 +01:00
self.col.conf["activeCols"] = self.model.activeCols
self.col.setMod()
self.teardownHooks()
self.mw.maybeReset()
aqt.dialogs.markClosed("Browser")
self._closeEventHasCleanedUp = True
self.mw.gcWindow(self)
self.close()
def closeWithCallback(self, onsuccess):
def callback():
self._closeWindow()
onsuccess()
2019-12-23 01:34:10 +01:00
self.editor.saveNow(callback)
def keyPressEvent(self, evt):
if evt.key() == Qt.Key_Escape:
self.close()
else:
super().keyPressEvent(evt)
def setupColumns(self):
self.columns = [
2019-12-23 01:34:10 +01:00
("question", _("Question")),
("answer", _("Answer")),
("template", _("Card")),
("deck", _("Deck")),
("noteFld", _("Sort Field")),
("noteCrt", _("Created")),
("noteMod", _("Edited")),
("cardMod", _("Changed")),
("cardDue", _("Due")),
("cardIvl", _("Interval")),
("cardEase", _("Ease")),
("cardReps", _("Reviews")),
("cardLapses", _("Lapses")),
("noteTags", _("Tags")),
("note", _("Note")),
]
self.columns.sort(key=itemgetter(1))
# Searching
######################################################################
def setupSearch(self):
self.form.searchButton.clicked.connect(self.onSearchActivated)
self.form.searchEdit.lineEdit().returnPressed.connect(self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
self._searchPrompt = _("<type here to search; hit enter to show current deck>")
2019-12-23 01:34:10 +01:00
self.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
)
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)
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 ")
# grab search text and normalize
txt = self.form.searchEdit.lineEdit().text()
txt = unicodedata.normalize("NFC", txt)
# update history
2019-12-23 01:34:10 +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)
2019-12-23 01:34:10 +01:00
self.mw.pm.profile["searchHistory"] = sh
# 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) -> None:
if "is:current" in self._lastSearchTxt:
# show current card if there is one
c = self.mw.reviewer.card
self.card = self.mw.reviewer.card
nid = c and c.nid or 0
2019-12-23 01:34:10 +01:00
self.model.search("nid:%d" % nid)
else:
self.model.search(self._lastSearchTxt)
if not self.model.cards:
# no row change will fire
self._onRowChanged(None, None)
def updateTitle(self):
selected = len(self.form.tableView.selectionModel().selectedRows())
cur = len(self.model.cards)
2019-12-23 01:34:10 +01:00
self.setWindowTitle(
ngettext(
"Browse (%(cur)d card shown; %(sel)s)",
"Browse (%(cur)d cards shown; %(sel)s)",
cur,
)
% {
"cur": cur,
"sel": ngettext("%d selected", "%d selected", selected) % selected,
}
)
return selected
def onReset(self):
self.editor.setNote(None)
self.search()
# 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))
self.form.tableView.selectionModel().selectionChanged.connect(self.onRowChanged)
if not theme_manager.night_mode:
self.form.tableView.setStyleSheet(
"QTableView{ selection-background-color: rgba(150, 150, 150, 50); "
"selection-color: black; }"
)
self.singleCard = False
def setupEditor(self):
2019-12-23 01:34:10 +01:00
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
def onRowChanged(self, current, previous):
"Update current note and hide/show editor."
self.editor.saveNow(lambda: self._onRowChanged(current, previous))
def _onRowChanged(self, current, previous) -> None:
update = self.updateTitle()
show = self.model.cards and update == 1
self.form.splitter.widget(1).setVisible(not not show)
idx = self.form.tableView.selectionModel().currentIndex()
if idx.isValid():
self.card = self.model.getCard(idx)
if not show:
self.editor.setNote(None)
self.singleCard = False
else:
self.editor.setNote(self.card.note(reload=True), focusTo=self.focusTo)
self.focusTo = None
self.editor.card = self.card # type: ignore
self.singleCard = True
2018-11-12 03:10:50 +01:00
self._updateFlagsMenu()
gui_hooks.browser_did_change_row(self)
2013-05-03 10:52:46 +02:00
self._renderPreview(True)
def refreshCurrentCard(self, note: Note) -> None:
self.model.refreshNote(note)
2013-05-03 10:52:46 +02:00
self._renderPreview(False)
def onLoadNote(self, editor):
self.refreshCurrentCard(editor.note)
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()
# Headers & sorting
######################################################################
def setupHeaders(self):
vh = self.form.tableView.verticalHeader()
hh = self.form.tableView.horizontalHeader()
if not isWin:
vh.hide()
hh.show()
restoreHeader(hh, "editor")
hh.setHighlightSections(False)
hh.setMinimumSectionSize(50)
hh.setSectionsMovable(True)
self.setColumnSizes()
hh.setContextMenuPolicy(Qt.CustomContextMenu)
hh.customContextMenuRequested.connect(self.onHeaderContext)
self.setSortIndicator()
hh.sortIndicatorChanged.connect(self.onSortChanged)
hh.sectionMoved.connect(self.onColumnMoved)
def onSortChanged(self, idx, ord):
ord = bool(ord)
self.editor.saveNow(lambda: self._onSortChanged(idx, ord))
def _onSortChanged(self, idx, ord):
type = self.model.activeCols[idx]
2013-09-03 20:20:20 +02:00
noSort = ("question", "answer", "template", "deck", "note", "noteTags")
if type in noSort:
if type == "template":
2019-12-23 01:34:10 +01:00
showInfo(
_(
"""\
2013-03-03 00:19:16 +01:00
This column can't be sorted on, but you can search for individual card types, \
2019-12-23 01:34:10 +01:00
such as 'card:1'."""
)
)
elif type == "deck":
2019-12-23 01:34:10 +01:00
showInfo(
_(
"""\
2013-03-03 00:19:16 +01:00
This column can't be sorted on, but you can search for specific decks \
2019-12-23 01:34:10 +01:00
by clicking on one on the left."""
)
)
else:
2019-12-23 01:34:10 +01:00
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
2019-12-23 01:34:10 +01:00
self.col.conf["sortBackwards"] = ord
self.search()
else:
2019-12-23 01:34:10 +01:00
if self.col.conf["sortBackwards"] != ord:
self.col.conf["sortBackwards"] = ord
self.model.reverse()
self.setSortIndicator()
def setSortIndicator(self):
hh = self.form.tableView.horizontalHeader()
2019-12-23 01:34:10 +01:00
type = self.col.conf["sortType"]
if type not in self.model.activeCols:
hh.setSortIndicatorShown(False)
return
idx = self.model.activeCols.index(type)
2019-12-23 01:34:10 +01:00
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)
a.toggled.connect(lambda b, t=type: self.toggleField(t))
m.exec_(gpos)
def toggleField(self, type):
self.editor.saveNow(lambda: self._toggleField(type))
def _toggleField(self, type):
self.model.beginReset()
if type in self.model.activeCols:
if len(self.model.activeCols) < 2:
self.model.endReset()
return showInfo(_("You must have at least one column."))
self.model.activeCols.remove(type)
2019-12-23 01:34:10 +01:00
adding = False
else:
self.model.activeCols.append(type)
2019-12-23 01:34:10 +01:00
adding = True
# sorted field may have been hidden
self.setSortIndicator()
self.setColumnSizes()
self.model.endReset()
# 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)
def setColumnSizes(self):
hh = self.form.tableView.horizontalHeader()
hh.setSectionResizeMode(QHeaderView.Interactive)
2019-12-23 01:34:10 +01:00
hh.setSectionResizeMode(
hh.logicalIndex(len(self.model.activeCols) - 1), QHeaderView.Stretch
)
# this must be set post-resize or it doesn't work
hh.setCascadingSectionResizes(False)
def onColumnMoved(self, a, b, c):
self.setColumnSizes()
# Sidebar
######################################################################
class SidebarTreeView(QTreeView):
2019-12-20 00:30:35 +01:00
def __init__(self):
super().__init__()
self.expanded.connect(self.onExpansion)
self.collapsed.connect(self.onCollapse)
def onClickCurrent(self) -> None:
idx = self.currentIndex()
if idx.isValid():
item: SidebarItem = idx.internalPointer()
2019-12-20 00:30:35 +01:00
if item.onClick:
item.onClick()
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
2019-12-19 23:52:03 +01:00
super().mouseReleaseEvent(event)
self.onClickCurrent()
def keyPressEvent(self, event: QKeyEvent) -> None:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.onClickCurrent()
else:
super().keyPressEvent(event)
2019-12-20 00:30:35 +01:00
def onExpansion(self, idx: QModelIndex) -> None:
self._onExpansionChange(idx, True)
def onCollapse(self, idx: QModelIndex) -> None:
self._onExpansionChange(idx, False)
def _onExpansionChange(self, idx: QModelIndex, expanded: bool) -> None:
item: SidebarItem = idx.internalPointer()
if item.expanded != expanded:
item.expanded = expanded
if item.onExpanded:
item.onExpanded(expanded)
def setupSidebar(self) -> None:
dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self)
dw.setFeatures(QDockWidget.DockWidgetClosable)
dw.setObjectName("Sidebar")
dw.setAllowedAreas(Qt.LeftDockWidgetArea)
self.sidebarTree = self.SidebarTreeView()
self.sidebarTree.mw = self.mw
self.sidebarTree.setUniformRowHeights(True)
self.sidebarTree.setHeaderHidden(True)
self.sidebarTree.setIndentation(15)
2019-12-23 01:34:10 +01:00
self.sidebarTree.expanded.connect(self.onSidebarItemExpanded) # type: ignore
dw.setWidget(self.sidebarTree)
# match window background color
bgcolor = QPalette().window().color().name()
self.sidebarTree.setStyleSheet("QTreeView { background: '%s'; }" % bgcolor)
self.sidebarDockWidget.setFloating(False)
2019-12-23 01:34:10 +01:00
self.sidebarDockWidget.visibilityChanged.connect(self.onSidebarVisChanged) # type: ignore
self.sidebarDockWidget.setTitleBarWidget(QWidget())
2017-08-14 08:57:43 +02:00
self.addDockWidget(Qt.LeftDockWidgetArea, dw)
2019-12-20 00:27:14 +01:00
def onSidebarItemExpanded(self, idx: QModelIndex) -> None:
item: SidebarItem = idx.internalPointer()
2019-12-23 01:34:10 +01:00
# item.on
2019-12-20 00:27:14 +01:00
def onSidebarVisChanged(self, _visible: bool) -> None:
self.maybeRefreshSidebar()
def focusSidebar(self) -> None:
self.sidebarDockWidget.setVisible(True)
self.sidebarTree.setFocus()
def maybeRefreshSidebar(self) -> None:
if self.sidebarDockWidget.isVisible():
# add slight delay to allow browser window to appear first
def deferredDisplay():
root = self.buildTree()
model = SidebarModel(root)
self.sidebarTree.setModel(model)
2019-12-20 00:27:14 +01:00
model.expandWhereNeccessary(self.sidebarTree)
2019-12-23 01:34:10 +01:00
self.mw.progress.timer(10, deferredDisplay, False)
def buildTree(self) -> SidebarItem:
root = SidebarItem("", "")
self._stdTree(root)
self._favTree(root)
self._decksTree(root)
self._modelTree(root)
self._userTagTree(root)
return root
def _stdTree(self, root) -> None:
2019-12-23 01:34:10 +01:00
for name, filt, icon in [
[_("Whole Collection"), "", "collection"],
[_("Current Deck"), "deck:current", "deck"],
]:
item = SidebarItem(
2019-12-23 01:34:10 +01:00
name, ":/icons/{}.svg".format(icon), self._filterFunc(filt)
)
root.addChild(item)
def _favTree(self, root) -> None:
assert self.col
2019-12-23 01:34:10 +01:00
saved = self.col.conf.get("savedFilters", {})
for name, filt in sorted(saved.items()):
2019-12-23 01:34:10 +01:00
item = SidebarItem(
name,
":/icons/heart.svg",
lambda s=filt: self.setFilter(s), # type: ignore
)
root.addChild(item)
def _userTagTree(self, root) -> None:
assert self.col
for t in sorted(self.col.tags.all(), key=lambda t: t.lower()):
item = SidebarItem(
2019-12-23 02:31:42 +01:00
t, ":/icons/tag.svg", lambda t=t: self.setFilter("tag", t) # type: ignore
)
root.addChild(item)
def _decksTree(self, root) -> None:
assert self.col
grps = self.col.sched.deckDueTree()
2019-12-23 01:34:10 +01:00
def fillGroups(root, grps, head=""):
for g in grps:
item = SidebarItem(
g[0],
":/icons/deck.svg",
2019-12-23 01:34:10 +01:00
lambda g=g: self.setFilter("deck", head + g[0]),
2019-12-20 00:30:35 +01:00
lambda expanded, g=g: self.mw.col.decks.collapseBrowser(g[1]),
2019-12-23 01:34:10 +01:00
not self.mw.col.decks.get(g[1]).get("browserCollapsed", False),
)
root.addChild(item)
2019-12-23 01:34:10 +01:00
newhead = head + g[0] + "::"
fillGroups(item, g[5], newhead)
2019-12-23 01:34:10 +01:00
fillGroups(root, grps)
def _modelTree(self, root) -> None:
assert self.col
for m in sorted(self.col.models.all(), key=itemgetter("name")):
item = SidebarItem(
2019-12-23 01:34:10 +01:00
m["name"],
":/icons/notetype.svg",
2019-12-23 02:31:42 +01:00
lambda m=m: self.setFilter("note", m["name"]), # type: ignore
)
root.addChild(item)
# Filter tree
######################################################################
def onFilterButton(self):
ml = MenuList()
ml.addChild(self._commonFilters())
ml.addSeparator()
ml.addChild(self._todayFilters())
ml.addChild(self._cardStateFilters())
ml.addChild(self._deckFilters())
ml.addChild(self._noteTypeFilters())
ml.addChild(self._tagFilters())
ml.addSeparator()
ml.addChild(self.sidebarDockWidget.toggleViewAction())
ml.addSeparator()
ml.addChild(self._savedSearches())
ml.popupOver(self.form.filter)
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
items.append(txt)
txt = ""
txt = " ".join(items)
if self.mw.app.keyboardModifiers() & Qt.AltModifier:
2019-12-23 01:34:10 +01:00
txt = "-" + txt
if self.mw.app.keyboardModifiers() & Qt.ControlModifier:
cur = str(self.form.searchEdit.lineEdit().text())
if cur and cur != self._searchPrompt:
2019-03-04 07:54:22 +01:00
txt = cur + " " + txt
2013-04-11 10:07:08 +02:00
elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
cur = str(self.form.searchEdit.lineEdit().text())
2013-04-11 10:07:08 +02:00
if cur:
txt = cur + " or " + txt
self.form.searchEdit.lineEdit().setText(txt)
self.onSearchActivated()
def _simpleFilters(self, items):
ml = MenuList()
for row in items:
if row is None:
ml.addSeparator()
else:
label, filter = row
ml.addItem(label, self._filterFunc(filter))
return ml
def _filterFunc(self, *args):
return lambda *, f=args: self.setFilter(*f)
def _commonFilters(self):
2019-12-23 01:34:10 +01:00
return self._simpleFilters(
((_("Whole Collection"), ""), (_("Current Deck"), "deck:current"))
)
def _todayFilters(self):
subm = SubMenu(_("Today"))
2019-12-23 01:34:10 +01:00
subm.addChild(
self._simpleFilters(
(
(_("Added Today"), "added:1"),
(_("Studied Today"), "rated:1"),
(_("Again Today"), "rated:1:1"),
)
)
)
return subm
def _cardStateFilters(self):
subm = SubMenu(_("Card State"))
2019-12-23 01:34:10 +01:00
subm.addChild(
self._simpleFilters(
(
(_("New"), "is:new"),
(_("Learning"), "is:learn"),
(_("Review"), "is:review"),
(_("Due"), "is:due"),
None,
(_("Suspended"), "is:suspended"),
(_("Buried"), "is:buried"),
None,
(_("Red Flag"), "flag:1"),
(_("Orange Flag"), "flag:2"),
(_("Green Flag"), "flag:3"),
(_("Blue Flag"), "flag:4"),
(_("No Flag"), "flag:0"),
(_("Any Flag"), "-flag:0"),
)
)
)
return subm
def _tagFilters(self):
m = SubMenu(_("Tags"))
m.addItem(_("Clear Unused"), self.clearUnusedTags)
m.addSeparator()
tagList = MenuList()
for t in sorted(self.col.tags.all(), key=lambda s: s.lower()):
tagList.addItem(t, self._filterFunc("tag", t))
m.addChild(tagList.chunked())
return m
def _deckFilters(self):
def addDecks(parent, decks):
for head, did, rev, lrn, new, children in decks:
2019-12-23 01:34:10 +01:00
name = self.mw.col.decks.get(did)["name"]
shortname = name.split("::")[-1]
if children:
subm = parent.addMenu(shortname)
subm.addItem(_("Filter"), self._filterFunc("deck", name))
subm.addSeparator()
addDecks(subm, children)
else:
parent.addItem(shortname, self._filterFunc("deck", name))
# fixme: could rewrite to avoid calculating due # in the future
alldecks = self.col.sched.deckDueTree()
ml = MenuList()
addDecks(ml, alldecks)
root = SubMenu(_("Decks"))
root.addChild(ml.chunked())
return root
def _noteTypeFilters(self):
m = SubMenu(_("Note Types"))
m.addItem(_("Manage..."), self.mw.onNoteTypes)
m.addSeparator()
noteTypes = MenuList()
2019-12-23 01:34:10 +01:00
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
# no sub menu if it's a single template
2019-12-23 01:34:10 +01:00
if len(nt["tmpls"]) == 1:
noteTypes.addItem(nt["name"], self._filterFunc("note", nt["name"]))
else:
2019-12-23 01:34:10 +01:00
subm = noteTypes.addMenu(nt["name"])
2019-12-23 01:34:10 +01:00
subm.addItem(_("All Card Types"), self._filterFunc("note", nt["name"]))
subm.addSeparator()
# add templates
2019-12-23 01:34:10 +01:00
for c, tmpl in enumerate(nt["tmpls"]):
# T: name is a card type name. n it's order in the list of card type.
# T: this is shown in browser's filter, when seeing the list of card type of a note type.
name = _("%(n)d: %(name)s") % dict(n=c + 1, name=tmpl["name"])
subm.addItem(
name, self._filterFunc("note", nt["name"], "card", str(c + 1))
)
m.addChild(noteTypes.chunked())
return m
# Favourites
######################################################################
def _savedSearches(self):
ml = MenuList()
# make sure exists
if "savedFilters" not in self.col.conf:
2019-12-23 01:34:10 +01:00
self.col.conf["savedFilters"] = {}
ml.addSeparator()
if self._currentFilterIsSaved():
ml.addItem(_("Remove Current Filter..."), self._onRemoveFilter)
else:
ml.addItem(_("Save Current Filter..."), self._onSaveFilter)
2019-12-23 01:34:10 +01:00
saved = self.col.conf["savedFilters"]
if not saved:
return ml
ml.addSeparator()
for name, filt in sorted(saved.items()):
ml.addItem(name, self._filterFunc(filt))
return ml
def _onSaveFilter(self):
name = getOnlyText(_("Please give your filter a name:"))
if not name:
return
filt = self.form.searchEdit.lineEdit().text()
2019-12-23 01:34:10 +01:00
self.col.conf["savedFilters"][name] = filt
self.col.setMod()
self.maybeRefreshSidebar()
def _onRemoveFilter(self):
name = self._currentFilterIsSaved()
if not askUser(_("Remove %s from your saved searches?") % name):
return
2019-12-23 01:34:10 +01:00
del self.col.conf["savedFilters"][name]
self.col.setMod()
self.maybeRefreshSidebar()
# returns name if found
def _currentFilterIsSaved(self):
filt = self.form.searchEdit.lineEdit().text()
2019-12-23 01:34:10 +01:00
for k, v in self.col.conf["savedFilters"].items():
if filt == v:
return k
return None
# Info
######################################################################
def showCardInfo(self):
if not self.card:
return
info, cs = self._cardInfoData()
reps = self._revlogData(cs)
2019-12-23 01:34:10 +01:00
class CardInfoDialog(QDialog):
2019-04-09 08:21:43 +02:00
silentlyClose = True
def reject(self):
saveGeom(self, "revlog")
return QDialog.reject(self)
2019-12-23 01:34:10 +01:00
d = CardInfoDialog(self)
l = QVBoxLayout()
2019-12-23 01:34:10 +01:00
l.setContentsMargins(0, 0, 0, 0)
w = AnkiWebView()
l.addWidget(w)
w.stdHtml(info + "<p>" + reps)
bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(bb)
bb.rejected.connect(d.reject)
d.setLayout(l)
d.setWindowModality(Qt.WindowModal)
d.resize(500, 400)
restoreGeom(d, "revlog")
d.show()
def _cardInfoData(self):
from anki.stats import CardStats
2019-12-23 01:34:10 +01:00
cs = CardStats(self.col, self.card)
rep = cs.report()
m = self.card.model()
2019-12-23 01:34:10 +01:00
rep = (
"""
<div style='width: 400px; margin: 0 auto 0;
2019-12-23 01:34:10 +01:00
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 "
2019-12-23 01:34:10 +01:00
"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) % (
2019-12-23 01:34:10 +01:00
_("Type"),
_("Rating"),
_("Interval"),
_("Ease"),
_("Time"),
)
cnt = 0
for (date, ease, ivl, factor, taken, type) in reversed(entries):
cnt += 1
2019-12-23 01:34:10 +01:00
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
2019-12-23 01:34:10 +01:00
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:
2019-12-23 01:34:10 +01:00
ivl = fmtTimeSpan(ivl * 86400, short=True)
else:
ivl = cs.time(-ivl)
s += ("<td align=right>%s</td>" * 5) % (
tstr,
2019-12-23 01:34:10 +01:00
ease,
ivl,
"%d%%" % (factor / 10) if factor else "",
cs.time(taken),
) + "</tr>"
s += "</table>"
if cnt < self.card.reps:
2019-12-23 01:34:10 +01:00
s += _(
"""\
Note: Some of the history is missing. For more information, \
2019-12-23 01:34:10 +01:00
please see the browser documentation."""
)
return s
# Menu helpers
######################################################################
def selectedCards(self):
2019-12-23 01:34:10 +01:00
return [
self.model.cards[idx.row()]
for idx in self.form.tableView.selectionModel().selectedRows()
]
def selectedNotes(self):
2019-12-23 01:34:10 +01:00
return self.col.db.list(
"""
select distinct nid from cards
2019-12-23 01:34:10 +01:00
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(
2019-12-23 01:34:10 +01:00
"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
2019-12-23 01:34:10 +01:00
mods = self.col.db.scalar(
"""
select count(distinct mid) from notes
2019-12-23 01:34:10 +01:00
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):
self.editor.saveNow(self._onChangeModel)
def _onChangeModel(self):
nids = self.oneModelNotes()
if nids:
ChangeModel(self, nids)
2013-05-03 10:52:46 +02:00
# Preview
######################################################################
_previewTimer = None
_lastPreviewRender: Union[int, float] = 0
_lastPreviewState = None
_previewCardChanged = False
2013-05-03 10:52:46 +02:00
def onTogglePreview(self):
if self._previewWindow:
self._closePreview()
else:
self._openPreview()
def _openPreview(self):
self._previewState = "question"
self._lastPreviewState = None
self._previewWindow = QDialog(None, Qt.Window)
2013-05-03 10:52:46 +02:00
self._previewWindow.setWindowTitle(_("Preview"))
self._previewWindow.finished.connect(self._onPreviewFinished)
self._previewWindow.silentlyClose = True
2013-05-03 10:52:46 +02:00
vbox = QVBoxLayout()
2019-12-23 01:34:10 +01:00
vbox.setContentsMargins(0, 0, 0, 0)
self._previewWeb = AnkiWebView()
2013-05-03 10:52:46 +02:00
vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox()
2019-12-23 01:34:10 +01:00
self._previewReplay = bbox.addButton(
_("Replay Audio"), QDialogButtonBox.ActionRole
)
2014-06-21 22:35:45 +02:00
self._previewReplay.setAutoDefault(False)
self._previewReplay.setShortcut(QKeySequence("R"))
self._previewReplay.setToolTip(_("Shortcut key: %s" % "R"))
2013-05-03 10:52:46 +02:00
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
self._previewPrev.setAutoDefault(False)
self._previewPrev.setShortcut(QKeySequence("Left"))
self._previewPrev.setToolTip(_("Shortcut key: Left arrow"))
2013-05-03 10:52:46 +02:00
self._previewNext = bbox.addButton(">", QDialogButtonBox.ActionRole)
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"))
self._previewPrev.clicked.connect(self._onPreviewPrev)
self._previewNext.clicked.connect(self._onPreviewNext)
self._previewReplay.clicked.connect(self._onReplayAudio)
self.previewShowBothSides = QCheckBox(_("Show Both Sides"))
self.previewShowBothSides.setShortcut(QKeySequence("B"))
self.previewShowBothSides.setToolTip(_("Shortcut key: %s" % "B"))
bbox.addButton(self.previewShowBothSides, QDialogButtonBox.ActionRole)
self._previewBothSides = self.col.conf.get("previewBothSides", False)
self.previewShowBothSides.setChecked(self._previewBothSides)
self.previewShowBothSides.toggled.connect(self._onPreviewShowBothSides)
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):
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:
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:
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
current = self.currentRow()
2019-12-23 01:34:10 +01:00
canBack = current > 0 or (
current == 0
and self._previewState == "answer"
and not self._previewBothSides
)
self._previewPrev.setEnabled(not not (self.singleCard and canBack))
2019-12-23 01:34:10 +01:00
canForward = (
self.currentRow() < self.model.rowCount(None) - 1
or self._previewState == "question"
)
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
def _setupPreviewWebview(self):
2019-12-23 01:34:10 +01:00
jsinc = [
"jquery.js",
"browsersel.js",
"mathjax/conf.js",
"mathjax/MathJax.js",
"reviewer.js",
]
self._previewWeb.stdHtml(
self.mw.reviewer.revHtml(), css=["reviewer.css"], js=jsinc
)
2013-05-03 10:52:46 +02:00
def _renderPreview(self, cardChanged=False):
self._cancelPreviewTimer()
# Keep track of whether _renderPreview() has ever been called
# with cardChanged=True since the last successful render
self._previewCardChanged |= cardChanged
# avoid rendering in quick succession
2019-12-23 01:34:10 +01:00
elapMS = int((time.time() - self._lastPreviewRender) * 1000)
delay = 300
if elapMS < delay:
self._previewTimer = self.mw.progress.timer(
2019-12-23 01:34:10 +01:00
delay - elapMS, self._renderScheduledPreview, False
)
else:
self._renderScheduledPreview()
def _cancelPreviewTimer(self):
if self._previewTimer:
self._previewTimer.stop()
self._previewTimer = None
def _renderScheduledPreview(self) -> None:
2017-08-08 12:02:37 +02:00
self._cancelPreviewTimer()
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
func = "_showQuestion"
if not c or not self.singleCard:
2013-05-03 10:52:46 +02:00
txt = _("(please select 1 card)")
bodyclass = ""
self._lastPreviewState = None
else:
if self._previewBothSides:
self._previewState = "answer"
elif self._previewCardChanged:
self._previewState = "question"
currentState = self._previewStateAndMod()
if currentState == self._lastPreviewState:
# nothing has changed, avoid refreshing
return
# need to force reload even if answer
txt = c.q(reload=True)
question_audio = []
if self._previewBothSides:
question_audio = c.question_av_tags()
if self._previewState == "answer":
func = "_showAnswer"
txt = c.a()
2017-12-13 05:34:54 +01:00
txt = re.sub(r"\[\[type:[^]]+\]\]", "", txt)
bodyclass = theme_manager.body_classes_for_card_ord(c.ord)
if self.mw.reviewer.autoplay(c):
# if we're showing both sides at once, play question audio first
av_player.play_tags(question_audio)
# then play any audio that hasn't already been played
answer_audio = c.answer_av_tags()
unplayed_audio = [x for x in answer_audio if x not in question_audio]
2020-01-20 11:10:38 +01:00
av_player.extend_and_play(unplayed_audio)
txt = self.mw.prepare_card_text_for_display(txt)
gui_hooks.card_will_show(
txt, c, "preview" + self._previewState.capitalize()
)
self._lastPreviewState = self._previewStateAndMod()
self._updatePreviewButtons()
2019-12-23 01:34:10 +01:00
self._previewWeb.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
self._previewCardChanged = False
2013-05-03 10:52:46 +02:00
def _onPreviewShowBothSides(self, toggle):
self._previewBothSides = toggle
self.col.conf["previewBothSides"] = toggle
self.col.setMod()
if self._previewState == "answer" and not toggle:
self._previewState = "question"
self._renderPreview()
def _previewStateAndMod(self):
c = self.card
n = c.note()
n.load()
return (self._previewState, c.id, n.mod)
# Card deletion
######################################################################
def deleteNotes(self):
focus = self.focusWidget()
if focus != self.form.tableView:
return
self._deleteNotes()
def _deleteNotes(self):
nids = self.selectedNotes()
if not nids:
return
self.mw.checkpoint(_("Delete Notes"))
self.model.beginReset()
# figure out where to place the cursor after the deletion
curRow = self.form.tableView.selectionModel().currentIndex().row()
2019-12-23 01:34:10 +01:00
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
self.col.remNotes(nids)
self.search()
if len(self.model.cards):
newRow = min(newRow, len(self.model.cards) - 1)
newRow = max(newRow, 0)
self.model.focusedCard = self.model.cards[newRow]
self.model.endReset()
self.mw.requireReset()
2019-12-23 01:34:10 +01:00
tooltip(
ngettext("%d note deleted.", "%d notes deleted.", len(nids)) % len(nids)
)
# Deck change
######################################################################
def setDeck(self):
self.editor.saveNow(self._setDeck)
def _setDeck(self):
from aqt.studydeck import StudyDeck
2019-12-23 01:34:10 +01:00
cids = self.selectedCards()
if not cids:
return
2019-12-23 01:34:10 +01:00
did = self.mw.col.db.scalar("select did from cards where id = ?", cids[0])
current = self.mw.col.decks.get(did)["name"]
ret = StudyDeck(
2019-12-23 01:34:10 +01:00
self.mw,
current=current,
accept=_("Move Cards"),
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)
2019-12-23 01:34:10 +01:00
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
2019-12-23 01:34:10 +01:00
self.col.db.execute(
"""
update cards set usn=?, mod=?, did=? where id in """
+ scids,
usn,
mod,
did,
)
self.model.endReset()
self.mw.requireReset()
# Tags
######################################################################
def addTags(self, tags=None, label=None, prompt=None, func=None):
self.editor.saveNow(lambda: self._addTags(tags, label, prompt, func))
def _addTags(self, tags, label, prompt, func):
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")
2019-12-23 01:34:10 +01:00
self.addTags(
tags, label, _("Enter tags to delete:"), func=self.col.tags.bulkRem
)
def clearUnusedTags(self):
self.editor.saveNow(self._clearUnusedTags)
def _clearUnusedTags(self):
self.col.tags.registerNotes()
# Suspending
######################################################################
def isSuspended(self):
return not not (self.card and self.card.queue == -1)
def onSuspend(self):
self.editor.saveNow(self._onSuspend)
def _onSuspend(self):
sus = not self.isSuspended()
c = self.selectedCards()
if sus:
self.col.sched.suspendCards(c)
else:
self.col.sched.unsuspendCards(c)
self.model.reset()
self.mw.requireReset()
# Flags & Marking
######################################################################
def onSetFlag(self, n):
if not self.card:
return
# flag needs toggling off?
if n == self.card.userFlag():
n = 0
self.col.setUserFlag(n, self.selectedCards())
self.model.reset()
2018-11-12 03:10:50 +01:00
def _updateFlagsMenu(self):
flag = self.card and self.card.userFlag()
flag = flag or 0
f = self.form
2019-12-23 01:34:10 +01:00
flagActions = [
f.actionRed_Flag,
f.actionOrange_Flag,
f.actionGreen_Flag,
f.actionBlue_Flag,
]
2018-11-12 03:10:50 +01:00
for c, act in enumerate(flagActions):
2019-12-23 01:34:10 +01:00
act.setChecked(flag == c + 1)
qtMenuShortcutWorkaround(self.form.menuFlag)
2018-11-12 03:10:50 +01: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"))
# Repositioning
######################################################################
def reposition(self):
self.editor.saveNow(self._reposition)
def _reposition(self):
cids = self.selectedCards()
cids2 = self.col.db.list(
2019-12-23 01:34:10 +01:00
"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(
2019-12-23 01:34:10 +01:00
"select min(due), max(due) from cards where type=0 and odid=0"
)
pmin = pmin or 0
pmax = pmax or 0
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(
2019-12-23 01:34:10 +01:00
cids,
start=frm.start.value(),
step=frm.step.value(),
shuffle=frm.randomize.isChecked(),
shift=frm.shift.isChecked(),
)
self.search()
self.mw.requireReset()
self.model.endReset()
# Rescheduling
######################################################################
def reschedule(self):
self.editor.saveNow(self._reschedule)
def _reschedule(self):
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)
2019-12-23 01:34:10 +01:00
self.col.sched.reschedCards(self.selectedCards(), fmin, fmax)
self.search()
self.mw.requireReset()
self.model.endReset()
# Edit: selection
######################################################################
def selectNotes(self):
self.editor.saveNow(self._selectNotes)
def _selectNotes(self):
nids = self.selectedNotes()
# bypass search history
2019-12-23 01:34:10 +01:00
self._lastSearchTxt = "nid:" + ",".join([str(x) for x in nids])
self.form.searchEdit.lineEdit().setText(self._lastSearchTxt)
# clear the selection so we don't waste energy preserving it
tv = self.form.tableView
tv.selectionModel().clear()
self.search()
tv.selectAll()
def invertSelection(self):
sm = self.form.tableView.selectionModel()
items = sm.selection()
self.form.tableView.selectAll()
sm.select(items, QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
# Hooks
######################################################################
def setupHooks(self) -> None:
2020-01-15 07:53:24 +01:00
gui_hooks.undo_state_did_change.append(self.onUndoState)
gui_hooks.state_did_reset.append(self.onReset)
gui_hooks.editor_did_fire_typing_timer.append(self.refreshCurrentCard)
gui_hooks.editor_did_load_note.append(self.onLoadNote)
gui_hooks.editor_did_unfocus_field.append(self.on_unfocus_field)
hooks.tag_added.append(self.on_item_added)
hooks.note_type_added.append(self.on_item_added)
hooks.deck_added.append(self.on_item_added)
def teardownHooks(self) -> None:
2020-01-15 07:53:24 +01:00
gui_hooks.undo_state_did_change.remove(self.onUndoState)
gui_hooks.state_did_reset.remove(self.onReset)
gui_hooks.editor_did_fire_typing_timer.remove(self.refreshCurrentCard)
gui_hooks.editor_did_load_note.remove(self.onLoadNote)
gui_hooks.editor_did_unfocus_field.remove(self.on_unfocus_field)
hooks.tag_added.remove(self.on_item_added)
hooks.note_type_added.remove(self.on_item_added)
hooks.deck_added.remove(self.on_item_added)
def on_unfocus_field(self, changed: bool, note: Note, field_idx: int) -> None:
self.refreshCurrentCard(note)
# covers the tag, note and deck case
def on_item_added(self, item: Any) -> None:
self.maybeRefreshSidebar()
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):
self.editor.saveNow(self._onFindReplace)
def _onFindReplace(self):
sf = self.selectedNotes()
if not sf:
return
import anki.find
2019-12-23 01:34:10 +01:00
fields = anki.find.fieldNamesForNotes(self.mw.col, sf)
d = QDialog(self)
frm = aqt.forms.findreplace.Ui_Dialog()
frm.setupUi(d)
d.setWindowModality(Qt.WindowModal)
frm.field.addItems([_("All Fields")] + fields)
frm.buttonBox.helpRequested.connect(self.onFindReplaceHelp)
restoreGeom(d, "findreplace")
r = d.exec_()
saveGeom(d, "findreplace")
if not r:
return
if frm.field.currentIndex() == 0:
field = None
else:
2019-12-23 01:34:10 +01:00
field = fields[frm.field.currentIndex() - 1]
self.mw.checkpoint(_("Find and Replace"))
self.mw.progress.start()
self.model.beginReset()
try:
2019-12-23 01:34:10 +01:00
changed = self.col.findReplace(
sf,
str(frm.find.text()),
str(frm.replace.text()),
frm.re.isChecked(),
field,
frm.ignoreCase.isChecked(),
)
except sre_constants.error:
showInfo(_("Invalid regular expression."), parent=self)
return
else:
self.search()
self.mw.requireReset()
finally:
self.model.endReset()
self.mw.progress.finish()
2019-12-23 01:34:10 +01:00
showInfo(
ngettext(
"%(a)d of %(b)d note updated", "%(a)d of %(b)d notes updated", len(sf)
)
% {"a": changed, "b": len(sf),},
parent=self,
)
def onFindReplaceHelp(self):
openHelp("findreplace")
# Edit: finding dupes
######################################################################
def onFindDupes(self):
self.editor.saveNow(self._onFindDupes)
def _onFindDupes(self):
d = QDialog(self)
self.mw.setupDialogGC(d)
frm = aqt.forms.finddupes.Ui_Dialog()
frm.setupUi(d)
restoreGeom(d, "findDupes")
2019-12-23 01:34:10 +01:00
fields = sorted(
anki.find.fieldNames(self.col, downcase=False), key=lambda x: x.lower()
)
frm.fields.addItems(fields)
self._dupesButton = None
# links
frm.webView.set_bridge_command(self.dupeLinkClicked, "find_dupes")
2019-12-23 01:34:10 +01:00
def onFin(code):
saveGeom(d, "findDupes")
2019-12-23 01:34:10 +01:00
d.finished.connect(onFin)
2019-12-23 01:34:10 +01:00
def onClick():
field = fields[frm.fields.currentIndex()]
self.duplicatesReport(frm.webView, field, frm.search.text(), frm)
2019-12-23 01:34:10 +01:00
search = frm.buttonBox.addButton(_("Search"), QDialogButtonBox.ActionRole)
search.clicked.connect(onClick)
d.show()
def duplicatesReport(self, web, fname, search, frm):
self.mw.progress.start()
res = self.mw.col.findDupes(fname, search)
if not self._dupesButton:
self._dupesButton = b = frm.buttonBox.addButton(
2019-12-23 01:34:10 +01:00
_("Tag Duplicates"), QDialogButtonBox.ActionRole
)
b.clicked.connect(lambda: self._onTagDupes(res))
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:
2019-12-23 01:34:10 +01:00
t += (
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
% (
"nid:" + ",".join(str(id) for id in nids),
ngettext("%d note", "%d notes", len(nids)) % len(nids),
html.escape(val),
)
)
t += "</ol>"
t += "</body></html>"
web.setHtml(t)
self.mw.progress.finish()
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."))
def dupeLinkClicked(self, link):
self.form.searchEdit.lineEdit().setText(link)
# manually, because we've already saved
self._lastSearchTxt = link
self.search()
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())
tv.selectionModel().setCurrentIndex(
idx,
2019-12-23 01:34:10 +01:00
QItemSelectionModel.Clear
| QItemSelectionModel.Select
| QItemSelectionModel.Rows,
)
def onPreviousCard(self):
self.focusTo = self.editor.currentField
self.editor.saveNow(self._onPreviousCard)
def _onPreviousCard(self):
self._moveCur(QAbstractItemView.MoveUp)
def onNextCard(self):
self.focusTo = self.editor.currentField
self.editor.saveNow(self._onNextCard)
def _onNextCard(self):
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)
2019-12-23 01:34:10 +01:00
sm.select(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows)
def onLastCard(self):
sm = self.form.tableView.selectionModel()
idx = sm.currentIndex()
2019-12-23 01:34:10 +01:00
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)
2019-12-23 01:34:10 +01:00
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()
self.editor.loadNote(focusTo=0)
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)
2019-12-23 01:34:10 +01:00
# Change model dialog
######################################################################
2019-12-23 01:34:10 +01:00
class ChangeModel(QDialog):
def __init__(self, browser, nids) -> None:
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")
2020-01-15 07:53:24 +01:00
gui_hooks.state_did_reset.append(self.onReset)
gui_hooks.current_note_type_did_change.append(self.on_note_type_change)
self.exec_()
def on_note_type_change(self, notetype: NoteType) -> None:
self.onReset()
def setup(self):
# maps
self.flayout = QHBoxLayout()
2019-12-23 01:34:10 +01:00
self.flayout.setContentsMargins(0, 0, 0, 0)
self.fwidg = None
self.form.fieldMap.setLayout(self.flayout)
self.tlayout = QHBoxLayout()
2019-12-23 01:34:10 +01:00
self.tlayout.setContentsMargins(0, 0, 0, 0)
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
2019-12-23 01:34:10 +01:00
self.oldModel = self.browser.col.models.get(
self.browser.col.db.scalar(
2019-12-23 01:34:10 +01:00
"select mid from notes where id = ?", self.nids[0]
)
)
self.form.oldModelLabel.setText(self.oldModel["name"])
self.modelChooser = aqt.modelchooser.ModelChooser(
2019-12-23 01:34:10 +01:00
self.browser.mw, self.form.modelChooserWidget, label=False
)
self.modelChooser.models.setFocus()
self.form.buttonBox.helpRequested.connect(self.onHelp)
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 = []
2019-12-23 01:34:10 +01:00
targets = [x["name"] for x in dst] + [_("Nothing")]
indices = {}
for i, x in enumerate(src):
2019-12-23 01:34:10 +01:00
l.addWidget(QLabel(_("Change %s to:") % x["name"]), i, 0)
cb = QComboBox()
cb.addItems(targets)
2019-12-23 01:34:10 +01:00
idx = min(i, len(targets) - 1)
cb.setCurrentIndex(idx)
indices[cb] = idx
cb.currentIndexChanged.connect(
2019-12-23 01:34:10 +01:00
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 + "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:
2019-12-23 01:34:10 +01:00
old = self.oldModel["tmpls"]
combos = self.tcombos
2019-12-23 01:34:10 +01:00
new = self.targetModel["tmpls"]
map = {}
for i, f in enumerate(old):
idx = combos[i].currentIndex()
if idx == len(new):
# ignore
2019-12-23 01:34:10 +01:00
map[f["ord"]] = None
else:
f2 = new[idx]
2019-12-23 01:34:10 +01:00
map[f["ord"]] = f2["ord"]
return map
def getFieldMap(self):
return self.getTemplateMap(
2019-12-23 01:34:10 +01:00
old=self.oldModel["flds"], combos=self.fcombos, new=self.targetModel["flds"]
)
def cleanup(self) -> None:
2020-01-15 07:53:24 +01:00
gui_hooks.state_did_reset.remove(self.onReset)
gui_hooks.current_note_type_did_change.remove(self.on_note_type_change)
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()
if any(True for c in list(cmap.values()) if c is None):
2019-12-23 01:34:10 +01:00
if not askUser(
_(
"""\
Any cards mapped to nothing will be deleted. \
If a note has no remaining cards, it will be lost. \
2019-12-23 01:34:10 +01:00
Are you sure you want to continue?"""
)
):
return
self.browser.mw.checkpoint(_("Change Note Type"))
b = self.browser
b.mw.col.modSchema(check=True)
b.mw.progress.start()
b.model.beginReset()
mm = b.mw.col.models
mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap)
b.search()
b.model.endReset()
b.mw.progress.finish()
b.mw.reset()
self.cleanup()
QDialog.accept(self)
def onHelp(self):
openHelp("browsermisc")