parent
0a5222c400
commit
0d8b1c9d0b
@ -7,6 +7,7 @@ browsing-all-fields = All Fields
|
|||||||
browsing-answer = Answer
|
browsing-answer = Answer
|
||||||
browsing-any-cards-mapped-to-nothing-will = 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?
|
browsing-any-cards-mapped-to-nothing-will = 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?
|
||||||
browsing-any-flag = Any Flag
|
browsing-any-flag = Any Flag
|
||||||
|
browsing-average-ease = Average Ease
|
||||||
browsing-browser-appearance = Browser Appearance
|
browsing-browser-appearance = Browser Appearance
|
||||||
browsing-browser-options = Browser Options
|
browsing-browser-options = Browser Options
|
||||||
browsing-buried = Buried
|
browsing-buried = Buried
|
||||||
@ -100,6 +101,7 @@ browsing-toggle-suspend = Toggle Suspend
|
|||||||
browsing-treat-input-as-regular-expression = Treat input as regular expression
|
browsing-treat-input-as-regular-expression = Treat input as regular expression
|
||||||
browsing-update-saved-search = Update with Current Search
|
browsing-update-saved-search = Update with Current Search
|
||||||
browsing-whole-collection = Whole Collection
|
browsing-whole-collection = Whole Collection
|
||||||
|
browsing-window-title-notes = Browse ({ $selected } of { $total } notes selected)
|
||||||
browsing-you-must-have-at-least-one = You must have at least one column.
|
browsing-you-must-have-at-least-one = You must have at least one column.
|
||||||
browsing-group =
|
browsing-group =
|
||||||
{ $count ->
|
{ $count ->
|
||||||
|
@ -495,55 +495,26 @@ class Collection:
|
|||||||
query: str,
|
query: str,
|
||||||
order: Union[bool, str, BuiltinSort.Kind.V] = False,
|
order: Union[bool, str, BuiltinSort.Kind.V] = False,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
) -> Sequence[CardId]:
|
) -> List[CardId]:
|
||||||
"""Return card ids matching the provided search.
|
"""Return card ids matching the provided search.
|
||||||
|
|
||||||
To programmatically construct a search string, see .build_search_string().
|
To programmatically construct a search string, see .build_search_string().
|
||||||
|
To define a sort order, see _build_sort_mode().
|
||||||
If order=True, use the sort order stored in the collection config
|
|
||||||
If order=False, do no ordering
|
|
||||||
|
|
||||||
If order is a string, that text is added after 'order by' in the sql statement.
|
|
||||||
You must add ' asc' or ' desc' to the order, as Anki will replace asc with
|
|
||||||
desc and vice versa when reverse is set in the collection config, eg
|
|
||||||
order="c.ivl asc, c.due desc".
|
|
||||||
|
|
||||||
If order is a BuiltinSort.Kind value, sort using that builtin sort, eg
|
|
||||||
col.find_cards("", order=BuiltinSort.Kind.CARD_DUE)
|
|
||||||
|
|
||||||
The reverse argument only applies when a BuiltinSort.Kind is provided;
|
|
||||||
otherwise the collection config defines whether reverse is set or not.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(order, str):
|
mode = _build_sort_mode(order, reverse)
|
||||||
mode = _pb.SortOrder(custom=order)
|
return list(map(CardId, self._backend.search_cards(search=query, order=mode)))
|
||||||
elif isinstance(order, bool):
|
|
||||||
if order is True:
|
|
||||||
mode = _pb.SortOrder(from_config=_pb.Empty())
|
|
||||||
else:
|
|
||||||
mode = _pb.SortOrder(none=_pb.Empty())
|
|
||||||
else:
|
|
||||||
mode = _pb.SortOrder(
|
|
||||||
builtin=_pb.SortOrder.Builtin(kind=order, reverse=reverse)
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
CardId(id) for id in self._backend.search_cards(search=query, order=mode)
|
|
||||||
]
|
|
||||||
|
|
||||||
def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[NoteId]:
|
def find_notes(
|
||||||
"""Return note ids matching the provided search or searches.
|
self,
|
||||||
|
query: str,
|
||||||
If more than one search is provided, they will be ANDed together.
|
order: Union[bool, str, BuiltinSort.Kind.V] = False,
|
||||||
|
reverse: bool = False,
|
||||||
Eg: col.find_notes("test", "another") will search for "test AND another"
|
) -> List[NoteId]:
|
||||||
and return matching note ids.
|
"""Return note ids matching the provided search.
|
||||||
|
To programmatically construct a search string, see .build_search_string().
|
||||||
Eg: col.find_notes(SearchNode(deck="test"), "foo") will return notes
|
To define a sort order, see _build_sort_mode().
|
||||||
that have a card in deck called "test", and have the text "foo".
|
|
||||||
"""
|
"""
|
||||||
return [
|
mode = _build_sort_mode(order, reverse)
|
||||||
NoteId(did)
|
return list(map(NoteId, self._backend.search_notes(search=query, order=mode)))
|
||||||
for did in self._backend.search_notes(self.build_search_string(*terms))
|
|
||||||
]
|
|
||||||
|
|
||||||
def find_and_replace(
|
def find_and_replace(
|
||||||
self,
|
self,
|
||||||
@ -570,7 +541,9 @@ class Collection:
|
|||||||
|
|
||||||
# returns array of ("dupestr", [nids])
|
# returns array of ("dupestr", [nids])
|
||||||
def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]:
|
def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]:
|
||||||
nids = self.findNotes(search, SearchNode(field_name=fieldName))
|
nids = self.find_notes(
|
||||||
|
self.build_search_string(search, SearchNode(field_name=fieldName))
|
||||||
|
)
|
||||||
# go through notes
|
# go through notes
|
||||||
vals: Dict[str, List[int]] = {}
|
vals: Dict[str, List[int]] = {}
|
||||||
dupes = []
|
dupes = []
|
||||||
@ -692,10 +665,10 @@ class Collection:
|
|||||||
# Browser rows
|
# Browser rows
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def browser_row_for_card(
|
def browser_row_for_id(
|
||||||
self, cid: int
|
self, id_: int
|
||||||
) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
|
) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]:
|
||||||
row = self._backend.browser_row_for_card(cid)
|
row = self._backend.browser_row_for_id(id_)
|
||||||
return (
|
return (
|
||||||
((cell.text, cell.is_rtl) for cell in row.cells),
|
((cell.text, cell.is_rtl) for cell in row.cells),
|
||||||
row.color,
|
row.color,
|
||||||
@ -1089,3 +1062,34 @@ class _ReviewsUndo:
|
|||||||
|
|
||||||
|
|
||||||
_UndoInfo = Union[_ReviewsUndo, Checkpoint, None]
|
_UndoInfo = Union[_ReviewsUndo, Checkpoint, None]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sort_mode(
|
||||||
|
order: Union[bool, str, BuiltinSort.Kind.V],
|
||||||
|
reverse: bool,
|
||||||
|
) -> _pb.SortOrder:
|
||||||
|
"""Return a SortOrder object for use in find_cards() or find_notes().
|
||||||
|
|
||||||
|
If order=True, use the sort order stored in the collection config
|
||||||
|
If order=False, do no ordering
|
||||||
|
|
||||||
|
If order is a string, that text is added after 'order by' in the sql statement.
|
||||||
|
You must add ' asc' or ' desc' to the order, as Anki will replace asc with
|
||||||
|
desc and vice versa when reverse is set in the collection config, eg
|
||||||
|
order="c.ivl asc, c.due desc".
|
||||||
|
|
||||||
|
If order is a BuiltinSort.Kind value, sort using that builtin sort, eg
|
||||||
|
col.find_cards("", order=BuiltinSort.Kind.CARD_DUE)
|
||||||
|
|
||||||
|
The reverse argument only applies when a BuiltinSort.Kind is provided;
|
||||||
|
otherwise the collection config defines whether reverse is set or not.
|
||||||
|
"""
|
||||||
|
if isinstance(order, str):
|
||||||
|
return _pb.SortOrder(custom=order)
|
||||||
|
elif isinstance(order, bool):
|
||||||
|
if order is True:
|
||||||
|
return _pb.SortOrder(from_config=_pb.Empty())
|
||||||
|
else:
|
||||||
|
return _pb.SortOrder(none=_pb.Empty())
|
||||||
|
else:
|
||||||
|
return _pb.SortOrder(builtin=_pb.SortOrder.Builtin(kind=order, reverse=reverse))
|
||||||
|
@ -38,6 +38,7 @@ class AddCards(QDialog):
|
|||||||
QDialog.__init__(self, None, Qt.Window)
|
QDialog.__init__(self, None, Qt.Window)
|
||||||
mw.garbage_collect_on_dialog_finish(self)
|
mw.garbage_collect_on_dialog_finish(self)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
self.col = mw.col
|
||||||
self.form = aqt.forms.addcards.Ui_Dialog()
|
self.form = aqt.forms.addcards.Ui_Dialog()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
self.setWindowTitle(tr.actions_add())
|
self.setWindowTitle(tr.actions_add())
|
||||||
@ -59,7 +60,7 @@ class AddCards(QDialog):
|
|||||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True)
|
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True)
|
||||||
|
|
||||||
def setup_choosers(self) -> None:
|
def setup_choosers(self) -> None:
|
||||||
defaults = self.mw.col.defaults_for_adding(
|
defaults = self.col.defaults_for_adding(
|
||||||
current_review_card=self.mw.reviewer.card
|
current_review_card=self.mw.reviewer.card
|
||||||
)
|
)
|
||||||
self.notetype_chooser = NotetypeChooser(
|
self.notetype_chooser = NotetypeChooser(
|
||||||
@ -112,7 +113,7 @@ class AddCards(QDialog):
|
|||||||
|
|
||||||
def on_notetype_change(self, notetype_id: NotetypeId) -> None:
|
def on_notetype_change(self, notetype_id: NotetypeId) -> None:
|
||||||
# need to adjust current deck?
|
# need to adjust current deck?
|
||||||
if deck_id := self.mw.col.default_deck_for_notetype(notetype_id):
|
if deck_id := self.col.default_deck_for_notetype(notetype_id):
|
||||||
self.deck_chooser.selected_deck_id = deck_id
|
self.deck_chooser.selected_deck_id = deck_id
|
||||||
|
|
||||||
# only used for detecting changed sticky fields on close
|
# only used for detecting changed sticky fields on close
|
||||||
@ -151,8 +152,8 @@ class AddCards(QDialog):
|
|||||||
self.setAndFocusNote(note)
|
self.setAndFocusNote(note)
|
||||||
|
|
||||||
def _new_note(self) -> Note:
|
def _new_note(self) -> Note:
|
||||||
return self.mw.col.new_note(
|
return self.col.new_note(
|
||||||
self.mw.col.models.get(self.notetype_chooser.selected_notetype_id)
|
self.col.models.get(self.notetype_chooser.selected_notetype_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
def addHistory(self, note: Note) -> None:
|
def addHistory(self, note: Note) -> None:
|
||||||
@ -163,8 +164,8 @@ class AddCards(QDialog):
|
|||||||
def onHistory(self) -> None:
|
def onHistory(self) -> None:
|
||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
for nid in self.history:
|
for nid in self.history:
|
||||||
if self.mw.col.findNotes(SearchNode(nid=nid)):
|
if self.col.find_notes(self.col.build_search_string(SearchNode(nid=nid))):
|
||||||
note = self.mw.col.get_note(nid)
|
note = self.col.get_note(nid)
|
||||||
fields = note.fields
|
fields = note.fields
|
||||||
txt = htmlToTextLine(", ".join(fields))
|
txt = htmlToTextLine(", ".join(fields))
|
||||||
if len(txt) > 30:
|
if len(txt) > 30:
|
||||||
|
@ -4,26 +4,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import itemgetter
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Generator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from anki.cards import Card, CardId
|
from anki.cards import Card, CardId
|
||||||
from anki.collection import BrowserRow, Collection, Config, OpChanges, SearchNode
|
from anki.collection import Collection, Config, OpChanges, SearchNode
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
@ -31,8 +18,8 @@ from anki.models import NotetypeDict
|
|||||||
from anki.notes import NoteId
|
from anki.notes import NoteId
|
||||||
from anki.stats import CardStats
|
from anki.stats import CardStats
|
||||||
from anki.tags import MARKED_TAG
|
from anki.tags import MARKED_TAG
|
||||||
from anki.utils import ids2str, isMac, isWin
|
from anki.utils import ids2str, isMac
|
||||||
from aqt import AnkiQt, colors, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.card_ops import set_card_deck, set_card_flag
|
from aqt.card_ops import set_card_deck, set_card_flag
|
||||||
from aqt.editor import Editor
|
from aqt.editor import Editor
|
||||||
from aqt.exporting import ExportDialog
|
from aqt.exporting import ExportDialog
|
||||||
@ -50,8 +37,8 @@ from aqt.scheduling_ops import (
|
|||||||
unsuspend_cards,
|
unsuspend_cards,
|
||||||
)
|
)
|
||||||
from aqt.sidebar import SidebarTreeView
|
from aqt.sidebar import SidebarTreeView
|
||||||
|
from aqt.table import Table
|
||||||
from aqt.tag_ops import add_tags, clear_unused_tags, remove_tags_for_notes
|
from aqt.tag_ops import add_tags, clear_unused_tags, remove_tags_for_notes
|
||||||
from aqt.theme import theme_manager
|
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
HelpPage,
|
HelpPage,
|
||||||
KeyboardModifiersPressed,
|
KeyboardModifiersPressed,
|
||||||
@ -66,7 +53,6 @@ from aqt.utils import (
|
|||||||
restore_combo_history,
|
restore_combo_history,
|
||||||
restore_combo_index_for_session,
|
restore_combo_index_for_session,
|
||||||
restoreGeom,
|
restoreGeom,
|
||||||
restoreHeader,
|
|
||||||
restoreSplitter,
|
restoreSplitter,
|
||||||
restoreState,
|
restoreState,
|
||||||
save_combo_history,
|
save_combo_history,
|
||||||
@ -90,353 +76,15 @@ class FindDupesDialog:
|
|||||||
browser: Browser
|
browser: Browser
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SearchContext:
|
|
||||||
search: str
|
|
||||||
browser: Browser
|
|
||||||
order: Union[bool, str] = True
|
|
||||||
# if set, provided card ids will be used instead of the regular search
|
|
||||||
card_ids: Optional[Sequence[CardId]] = None
|
|
||||||
|
|
||||||
|
|
||||||
# Data model
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Cell:
|
|
||||||
text: str
|
|
||||||
is_rtl: bool
|
|
||||||
|
|
||||||
|
|
||||||
class CellRow:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
cells: Generator[Tuple[str, bool], None, None],
|
|
||||||
color: BrowserRow.Color.V,
|
|
||||||
font_name: str,
|
|
||||||
font_size: int,
|
|
||||||
) -> None:
|
|
||||||
self.refreshed_at: float = time.time()
|
|
||||||
self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells)
|
|
||||||
self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color)
|
|
||||||
self.font_name: str = font_name or "arial"
|
|
||||||
self.font_size: int = font_size if font_size > 0 else 12
|
|
||||||
|
|
||||||
def is_stale(self, threshold: float) -> bool:
|
|
||||||
return self.refreshed_at < threshold
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generic(length: int, cell_text: str) -> CellRow:
|
|
||||||
return CellRow(
|
|
||||||
((cell_text, False) for cell in range(length)),
|
|
||||||
BrowserRow.COLOR_DEFAULT,
|
|
||||||
"arial",
|
|
||||||
12,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def placeholder(length: int) -> CellRow:
|
|
||||||
return CellRow.generic(length, "...")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def deleted(length: int) -> CellRow:
|
|
||||||
return CellRow.generic(length, tr.browsing_row_deleted())
|
|
||||||
|
|
||||||
|
|
||||||
def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]:
|
|
||||||
if color == BrowserRow.COLOR_MARKED:
|
|
||||||
return colors.MARKED_BG
|
|
||||||
if color == BrowserRow.COLOR_SUSPENDED:
|
|
||||||
return colors.SUSPENDED_BG
|
|
||||||
if color == BrowserRow.COLOR_FLAG_RED:
|
|
||||||
return colors.FLAG1_BG
|
|
||||||
if color == BrowserRow.COLOR_FLAG_ORANGE:
|
|
||||||
return colors.FLAG2_BG
|
|
||||||
if color == BrowserRow.COLOR_FLAG_GREEN:
|
|
||||||
return colors.FLAG3_BG
|
|
||||||
if color == BrowserRow.COLOR_FLAG_BLUE:
|
|
||||||
return colors.FLAG4_BG
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class DataModel(QAbstractTableModel):
|
|
||||||
def __init__(self, browser: Browser) -> None:
|
|
||||||
QAbstractTableModel.__init__(self)
|
|
||||||
self.browser = browser
|
|
||||||
self.col = browser.col
|
|
||||||
self.sortKey = None
|
|
||||||
self.activeCols: List[str] = self.col.get_config(
|
|
||||||
"activeCols", ["noteFld", "template", "cardDue", "deck"]
|
|
||||||
)
|
|
||||||
self.cards: Sequence[CardId] = []
|
|
||||||
self._rows: Dict[int, CellRow] = {}
|
|
||||||
self._last_refresh = 0.0
|
|
||||||
# serve stale content to avoid hitting the DB?
|
|
||||||
self.block_updates = False
|
|
||||||
|
|
||||||
def get_id(self, index: QModelIndex) -> CardId:
|
|
||||||
return self.cards[index.row()]
|
|
||||||
|
|
||||||
def get_cell(self, index: QModelIndex) -> Cell:
|
|
||||||
return self.get_row(index).cells[index.column()]
|
|
||||||
|
|
||||||
def get_row(self, index: QModelIndex) -> CellRow:
|
|
||||||
cid = self.get_id(index)
|
|
||||||
if row := self._rows.get(cid):
|
|
||||||
if not self.block_updates and row.is_stale(self._last_refresh):
|
|
||||||
# need to refresh
|
|
||||||
self._rows[cid] = self._fetch_row_from_backend(cid)
|
|
||||||
return self._rows[cid]
|
|
||||||
# return row, even if it's stale
|
|
||||||
return row
|
|
||||||
if self.block_updates:
|
|
||||||
# blank row until we unblock
|
|
||||||
return CellRow.placeholder(len(self.activeCols))
|
|
||||||
# missing row, need to build
|
|
||||||
self._rows[cid] = self._fetch_row_from_backend(cid)
|
|
||||||
return self._rows[cid]
|
|
||||||
|
|
||||||
def _fetch_row_from_backend(self, cid: CardId) -> CellRow:
|
|
||||||
try:
|
|
||||||
row = CellRow(*self.col.browser_row_for_card(cid))
|
|
||||||
except NotFoundError:
|
|
||||||
return CellRow.deleted(len(self.activeCols))
|
|
||||||
except Exception as e:
|
|
||||||
return CellRow.generic(len(self.activeCols), str(e))
|
|
||||||
|
|
||||||
gui_hooks.browser_did_fetch_row(cid, row, self.activeCols)
|
|
||||||
return row
|
|
||||||
|
|
||||||
def getCard(self, index: QModelIndex) -> Optional[Card]:
|
|
||||||
"""Try to return the indicated, possibly deleted card."""
|
|
||||||
try:
|
|
||||||
return self.col.getCard(self.get_id(index))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Model interface
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
||||||
if parent and parent.isValid():
|
|
||||||
return 0
|
|
||||||
return len(self.cards)
|
|
||||||
|
|
||||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
|
||||||
if parent and parent.isValid():
|
|
||||||
return 0
|
|
||||||
return len(self.activeCols)
|
|
||||||
|
|
||||||
def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any:
|
|
||||||
if not index.isValid():
|
|
||||||
return QVariant()
|
|
||||||
if role == Qt.FontRole:
|
|
||||||
if self.activeCols[index.column()] not in ("question", "answer", "noteFld"):
|
|
||||||
return QVariant()
|
|
||||||
qfont = QFont()
|
|
||||||
row = self.get_row(index)
|
|
||||||
qfont.setFamily(row.font_name)
|
|
||||||
qfont.setPixelSize(row.font_size)
|
|
||||||
return qfont
|
|
||||||
if role == Qt.TextAlignmentRole:
|
|
||||||
align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter
|
|
||||||
if self.activeCols[index.column()] not in (
|
|
||||||
"question",
|
|
||||||
"answer",
|
|
||||||
"template",
|
|
||||||
"deck",
|
|
||||||
"noteFld",
|
|
||||||
"note",
|
|
||||||
"noteTags",
|
|
||||||
):
|
|
||||||
align |= Qt.AlignHCenter
|
|
||||||
return align
|
|
||||||
if role in (Qt.DisplayRole, Qt.ToolTipRole):
|
|
||||||
return self.get_cell(index).text
|
|
||||||
return QVariant()
|
|
||||||
|
|
||||||
def headerData(
|
|
||||||
self, section: int, orientation: Qt.Orientation, role: int = 0
|
|
||||||
) -> Optional[str]:
|
|
||||||
if orientation == Qt.Vertical:
|
|
||||||
return None
|
|
||||||
elif role == Qt.DisplayRole and section < len(self.activeCols):
|
|
||||||
type = self.activeCols[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 = tr.browsing_addon()
|
|
||||||
return txt
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
|
||||||
return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
||||||
|
|
||||||
# Filtering
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def search(self, txt: str) -> None:
|
|
||||||
self.beginReset()
|
|
||||||
self.cards = []
|
|
||||||
try:
|
|
||||||
ctx = SearchContext(search=txt, browser=self.browser)
|
|
||||||
gui_hooks.browser_will_search(ctx)
|
|
||||||
if ctx.card_ids is None:
|
|
||||||
ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order)
|
|
||||||
gui_hooks.browser_did_search(ctx)
|
|
||||||
self.cards = ctx.card_ids
|
|
||||||
except Exception as err:
|
|
||||||
raise err
|
|
||||||
finally:
|
|
||||||
self.endReset()
|
|
||||||
|
|
||||||
def redraw_cells(self) -> None:
|
|
||||||
"Update cell contents, without changing search count/columns/sorting."
|
|
||||||
if not self.cards:
|
|
||||||
return
|
|
||||||
top_left = self.index(0, 0)
|
|
||||||
bottom_right = self.index(len(self.cards) - 1, len(self.activeCols) - 1)
|
|
||||||
self._last_refresh = time.time()
|
|
||||||
self.dataChanged.emit(top_left, bottom_right) # type: ignore
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self.beginReset()
|
|
||||||
self.endReset()
|
|
||||||
|
|
||||||
# caller must have called editor.saveNow() before calling this or .reset()
|
|
||||||
def beginReset(self) -> None:
|
|
||||||
self.browser.editor.set_note(None, hide=False)
|
|
||||||
self.browser.mw.progress.start()
|
|
||||||
self.saveSelection()
|
|
||||||
self.beginResetModel()
|
|
||||||
self._rows = {}
|
|
||||||
|
|
||||||
def endReset(self) -> None:
|
|
||||||
self.endResetModel()
|
|
||||||
self.restoreSelection()
|
|
||||||
self.browser.mw.progress.finish()
|
|
||||||
|
|
||||||
def reverse(self) -> None:
|
|
||||||
self.browser.editor.call_after_note_saved(self._reverse)
|
|
||||||
|
|
||||||
def _reverse(self) -> None:
|
|
||||||
self.beginReset()
|
|
||||||
self.cards = list(reversed(self.cards))
|
|
||||||
self.endReset()
|
|
||||||
|
|
||||||
def saveSelection(self) -> None:
|
|
||||||
cards = self.browser.selected_cards()
|
|
||||||
self.selectedCards = {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) -> None:
|
|
||||||
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:
|
|
||||||
row = idx.row()
|
|
||||||
pos = tv.rowViewportPosition(row)
|
|
||||||
visible = pos >= 0 and pos < tv.viewport().height()
|
|
||||||
tv.selectRow(row)
|
|
||||||
|
|
||||||
# we save and then restore the horizontal scroll position because
|
|
||||||
# scrollTo() also scrolls horizontally which is confusing
|
|
||||||
if not visible:
|
|
||||||
h = tv.horizontalScrollBar().value()
|
|
||||||
tv.scrollTo(idx, tv.PositionAtCenter)
|
|
||||||
tv.horizontalScrollBar().setValue(h)
|
|
||||||
if count < 500:
|
|
||||||
# discard large selections; they're too slow
|
|
||||||
sm.select(
|
|
||||||
items,
|
|
||||||
cast(
|
|
||||||
QItemSelectionModel.SelectionFlags,
|
|
||||||
QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tv.selectRow(0)
|
|
||||||
|
|
||||||
def op_executed(self, op: OpChanges, focused: bool) -> None:
|
|
||||||
print("op executed")
|
|
||||||
if op.card or op.note or op.deck or op.notetype:
|
|
||||||
self._rows = {}
|
|
||||||
if focused:
|
|
||||||
self.redraw_cells()
|
|
||||||
|
|
||||||
def begin_blocking(self) -> None:
|
|
||||||
self.block_updates = True
|
|
||||||
|
|
||||||
def end_blocking(self) -> None:
|
|
||||||
self.block_updates = False
|
|
||||||
self.redraw_cells()
|
|
||||||
|
|
||||||
|
|
||||||
# Line painter
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class StatusDelegate(QItemDelegate):
|
|
||||||
def __init__(self, browser: Browser, model: DataModel) -> None:
|
|
||||||
QItemDelegate.__init__(self, browser)
|
|
||||||
self.browser = browser
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
def paint(
|
|
||||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
|
||||||
) -> None:
|
|
||||||
if self.model.get_cell(index).is_rtl:
|
|
||||||
option.direction = Qt.RightToLeft
|
|
||||||
if row_color := self.model.get_row(index).color:
|
|
||||||
brush = QBrush(theme_manager.qcolor(row_color))
|
|
||||||
painter.save()
|
|
||||||
painter.fillRect(option.rect, brush)
|
|
||||||
painter.restore()
|
|
||||||
return QItemDelegate.paint(self, painter, option, index)
|
|
||||||
|
|
||||||
|
|
||||||
# Browser window
|
# Browser window
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
class Browser(QMainWindow):
|
class Browser(QMainWindow):
|
||||||
model: DataModel
|
|
||||||
mw: AnkiQt
|
mw: AnkiQt
|
||||||
col: Collection
|
col: Collection
|
||||||
editor: Optional[Editor]
|
editor: Optional[Editor]
|
||||||
|
table: Table
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -464,29 +112,18 @@ class Browser(QMainWindow):
|
|||||||
restoreSplitter(self.form.splitter, "editor3")
|
restoreSplitter(self.form.splitter, "editor3")
|
||||||
self.form.splitter.setChildrenCollapsible(False)
|
self.form.splitter.setChildrenCollapsible(False)
|
||||||
self.card: Optional[Card] = None
|
self.card: Optional[Card] = None
|
||||||
self.setupColumns()
|
self.setup_table()
|
||||||
self.setupTable()
|
|
||||||
self.setupMenus()
|
self.setupMenus()
|
||||||
self.setupHeaders()
|
|
||||||
self.setupHooks()
|
self.setupHooks()
|
||||||
self.setupEditor()
|
self.setupEditor()
|
||||||
self.updateFont()
|
|
||||||
self.onUndoState(self.mw.form.actionUndo.isEnabled())
|
self.onUndoState(self.mw.form.actionUndo.isEnabled())
|
||||||
self.setupSearch(card, search)
|
self.setupSearch(card, search)
|
||||||
gui_hooks.browser_will_show(self)
|
gui_hooks.browser_will_show(self)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def on_backend_will_block(self) -> None:
|
|
||||||
# make sure the card list doesn't try to refresh itself during the operation,
|
|
||||||
# as that will block the UI
|
|
||||||
self.model.begin_blocking()
|
|
||||||
|
|
||||||
def on_backend_did_block(self) -> None:
|
|
||||||
self.model.end_blocking()
|
|
||||||
|
|
||||||
def on_operation_did_execute(self, changes: OpChanges) -> None:
|
def on_operation_did_execute(self, changes: OpChanges) -> None:
|
||||||
focused = current_top_level_widget() == self
|
focused = current_top_level_widget() == self
|
||||||
self.model.op_executed(changes, focused)
|
self.table.op_executed(changes, focused)
|
||||||
self.sidebar.op_executed(changes, focused)
|
self.sidebar.op_executed(changes, focused)
|
||||||
if changes.note or changes.notetype:
|
if changes.note or changes.notetype:
|
||||||
if not self.editor.is_updating_note():
|
if not self.editor.is_updating_note():
|
||||||
@ -506,7 +143,7 @@ class Browser(QMainWindow):
|
|||||||
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None:
|
||||||
if current_top_level_widget() == self:
|
if current_top_level_widget() == self:
|
||||||
self.setUpdatesEnabled(True)
|
self.setUpdatesEnabled(True)
|
||||||
self.model.redraw_cells()
|
self.table.redraw_cells()
|
||||||
self.sidebar.refresh_if_needed()
|
self.sidebar.refresh_if_needed()
|
||||||
|
|
||||||
def setupMenus(self) -> None:
|
def setupMenus(self) -> None:
|
||||||
@ -515,7 +152,7 @@ class Browser(QMainWindow):
|
|||||||
f = self.form
|
f = self.form
|
||||||
# edit
|
# edit
|
||||||
qconnect(f.actionUndo.triggered, self.undo)
|
qconnect(f.actionUndo.triggered, self.undo)
|
||||||
qconnect(f.actionInvertSelection.triggered, self.invertSelection)
|
qconnect(f.actionInvertSelection.triggered, self.table.invert_selection)
|
||||||
qconnect(f.actionSelectNotes.triggered, self.selectNotes)
|
qconnect(f.actionSelectNotes.triggered, self.selectNotes)
|
||||||
if not isMac:
|
if not isMac:
|
||||||
f.actionClose.setVisible(False)
|
f.actionClose.setVisible(False)
|
||||||
@ -575,32 +212,6 @@ class Browser(QMainWindow):
|
|||||||
gui_hooks.browser_menus_did_init(self)
|
gui_hooks.browser_menus_did_init(self)
|
||||||
self.mw.maybeHideAccelerators(self)
|
self.mw.maybeHideAccelerators(self)
|
||||||
|
|
||||||
# context menu
|
|
||||||
self.form.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
|
|
||||||
qconnect(self.form.tableView.customContextMenuRequested, self.onContextMenu)
|
|
||||||
|
|
||||||
def onContextMenu(self, _point: QPoint) -> None:
|
|
||||||
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)
|
|
||||||
m.exec_(QCursor.pos())
|
|
||||||
|
|
||||||
def updateFont(self) -> None:
|
|
||||||
# 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
|
|
||||||
self.form.tableView.verticalHeader().setDefaultSectionSize(curmax + 6)
|
|
||||||
|
|
||||||
def closeEvent(self, evt: QCloseEvent) -> None:
|
def closeEvent(self, evt: QCloseEvent) -> None:
|
||||||
if self._closeEventHasCleanedUp:
|
if self._closeEventHasCleanedUp:
|
||||||
evt.accept()
|
evt.accept()
|
||||||
@ -633,26 +244,6 @@ class Browser(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
super().keyPressEvent(evt)
|
super().keyPressEvent(evt)
|
||||||
|
|
||||||
def setupColumns(self) -> None:
|
|
||||||
self.columns = [
|
|
||||||
("question", tr.browsing_question()),
|
|
||||||
("answer", tr.browsing_answer()),
|
|
||||||
("template", tr.browsing_card()),
|
|
||||||
("deck", tr.decks_deck()),
|
|
||||||
("noteFld", tr.browsing_sort_field()),
|
|
||||||
("noteCrt", tr.browsing_created()),
|
|
||||||
("noteMod", tr.search_note_modified()),
|
|
||||||
("cardMod", tr.search_card_modified()),
|
|
||||||
("cardDue", tr.statistics_due_date()),
|
|
||||||
("cardIvl", tr.browsing_interval()),
|
|
||||||
("cardEase", tr.browsing_ease()),
|
|
||||||
("cardReps", tr.scheduling_reviews()),
|
|
||||||
("cardLapses", tr.scheduling_lapses()),
|
|
||||||
("noteTags", tr.editing_tags()),
|
|
||||||
("note", tr.browsing_note()),
|
|
||||||
]
|
|
||||||
self.columns.sort(key=itemgetter(1))
|
|
||||||
|
|
||||||
def reopen(
|
def reopen(
|
||||||
self,
|
self,
|
||||||
_mw: AnkiQt,
|
_mw: AnkiQt,
|
||||||
@ -720,12 +311,9 @@ class Browser(QMainWindow):
|
|||||||
"""Search triggered programmatically. Caller must have saved note first."""
|
"""Search triggered programmatically. Caller must have saved note first."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.model.search(self._lastSearchTxt)
|
self.table.search(self._lastSearchTxt)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
showWarning(str(err))
|
showWarning(str(err))
|
||||||
if not self.model.cards:
|
|
||||||
# no row change will fire
|
|
||||||
self.onRowChanged(None, None)
|
|
||||||
|
|
||||||
def update_history(self) -> None:
|
def update_history(self) -> None:
|
||||||
sh = self.mw.pm.profile["searchHistory"]
|
sh = self.mw.pm.profile["searchHistory"]
|
||||||
@ -737,15 +325,17 @@ class Browser(QMainWindow):
|
|||||||
self.form.searchEdit.addItems(sh)
|
self.form.searchEdit.addItems(sh)
|
||||||
self.mw.pm.profile["searchHistory"] = sh
|
self.mw.pm.profile["searchHistory"] = sh
|
||||||
|
|
||||||
def updateTitle(self) -> int:
|
def updateTitle(self) -> None:
|
||||||
selected = len(self.form.tableView.selectionModel().selectedRows())
|
selected = self.table.len_selection()
|
||||||
cur = len(self.model.cards)
|
cur = self.table.len()
|
||||||
self.setWindowTitle(
|
tr_title = (
|
||||||
without_unicode_isolation(
|
tr.browsing_window_title
|
||||||
tr.browsing_window_title(total=cur, selected=selected)
|
if self.table.is_card_state()
|
||||||
)
|
else tr.browsing_window_title_notes
|
||||||
|
)
|
||||||
|
self.setWindowTitle(
|
||||||
|
without_unicode_isolation(tr_title(total=cur, selected=selected))
|
||||||
)
|
)
|
||||||
return selected
|
|
||||||
|
|
||||||
def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None:
|
def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None:
|
||||||
search = self.col.build_search_string(*search_terms)
|
search = self.col.build_search_string(*search_terms)
|
||||||
@ -760,40 +350,34 @@ class Browser(QMainWindow):
|
|||||||
search = self.col.build_search_string(SearchNode(nid=card.nid))
|
search = self.col.build_search_string(SearchNode(nid=card.nid))
|
||||||
search = gui_hooks.default_search(search, card)
|
search = gui_hooks.default_search(search, card)
|
||||||
self.search_for(search, "")
|
self.search_for(search, "")
|
||||||
self.focusCid(card.id)
|
self.table.select_single_card(card.id)
|
||||||
|
|
||||||
self.editor.call_after_note_saved(on_show_single_card)
|
self.editor.call_after_note_saved(on_show_single_card)
|
||||||
|
|
||||||
def onReset(self) -> None:
|
def onReset(self) -> None:
|
||||||
self.sidebar.refresh()
|
self.sidebar.refresh()
|
||||||
self.model.reset()
|
self.begin_reset()
|
||||||
|
self.end_reset()
|
||||||
|
|
||||||
# Table view & editor
|
# caller must have called editor.saveNow() before calling this or .reset()
|
||||||
|
def begin_reset(self) -> None:
|
||||||
|
self.editor.set_note(None, hide=False)
|
||||||
|
self.mw.progress.start()
|
||||||
|
self.table.begin_reset()
|
||||||
|
|
||||||
|
def end_reset(self) -> None:
|
||||||
|
self.table.end_reset()
|
||||||
|
self.mw.progress.finish()
|
||||||
|
|
||||||
|
# Table & Editor
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupTable(self) -> None:
|
def setup_table(self) -> None:
|
||||||
self.model = DataModel(self)
|
self.table = Table(self)
|
||||||
self.form.tableView.setSortingEnabled(True)
|
self.form.radio_cards.setChecked(self.table.is_card_state())
|
||||||
self.form.tableView.setModel(self.model)
|
self.form.radio_notes.setChecked(not self.table.is_card_state())
|
||||||
self.form.tableView.selectionModel()
|
self.table.set_view(self.form.tableView)
|
||||||
self.form.tableView.setItemDelegate(StatusDelegate(self, self.model))
|
qconnect(self.form.radio_cards.toggled, self.on_table_state_changed)
|
||||||
qconnect(
|
|
||||||
self.form.tableView.selectionModel().selectionChanged, self.onRowChanged
|
|
||||||
)
|
|
||||||
self.form.tableView.setWordWrap(False)
|
|
||||||
if not theme_manager.night_mode:
|
|
||||||
self.form.tableView.setStyleSheet(
|
|
||||||
"QTableView{ selection-background-color: rgba(150, 150, 150, 50); "
|
|
||||||
"selection-color: black; }"
|
|
||||||
)
|
|
||||||
elif theme_manager.macos_dark_mode():
|
|
||||||
grid = colors.FRAME_BG
|
|
||||||
self.form.tableView.setStyleSheet(
|
|
||||||
f"""
|
|
||||||
QTableView {{ gridline-color: {grid} }}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
self.singleCard = False
|
|
||||||
|
|
||||||
def setupEditor(self) -> None:
|
def setupEditor(self) -> None:
|
||||||
def add_preview_button(leftbuttons: List[str], editor: Editor) -> None:
|
def add_preview_button(leftbuttons: List[str], editor: Editor) -> None:
|
||||||
@ -824,138 +408,36 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
def onRowChanged(
|
def onRowChanged(
|
||||||
self, current: Optional[QItemSelection], previous: Optional[QItemSelection]
|
self, current: Optional[QItemSelection], previous: Optional[QItemSelection]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update current note and hide/show editor."""
|
"""Update current note and hide/show editor. """
|
||||||
if self._closeEventHasCleanedUp:
|
if self._closeEventHasCleanedUp:
|
||||||
return
|
return
|
||||||
update = self.updateTitle()
|
|
||||||
show = self.model.cards and update == 1
|
|
||||||
idx = self.form.tableView.selectionModel().currentIndex()
|
|
||||||
if idx.isValid():
|
|
||||||
self.card = self.model.getCard(idx)
|
|
||||||
show = show and self.card is not None
|
|
||||||
self.form.splitter.widget(1).setVisible(bool(show))
|
|
||||||
|
|
||||||
if not show:
|
self.updateTitle()
|
||||||
self.editor.set_note(None)
|
# the current card is used for context actions
|
||||||
self.singleCard = False
|
self.card = self.table.get_current_card()
|
||||||
self._renderPreview()
|
# if there is only one selected card, use it in the editor
|
||||||
else:
|
# it might differ from the current card
|
||||||
self.editor.set_note(self.card.note(reload=True), focusTo=self.focusTo)
|
card = self.table.get_single_selected_card()
|
||||||
|
self.singleCard = bool(card)
|
||||||
|
self.form.splitter.widget(1).setVisible(self.singleCard)
|
||||||
|
if self.singleCard:
|
||||||
|
self.editor.set_note(card.note(), focusTo=self.focusTo)
|
||||||
self.focusTo = None
|
self.focusTo = None
|
||||||
self.editor.card = self.card
|
self.editor.card = card
|
||||||
self.singleCard = True
|
else:
|
||||||
|
self.editor.set_note(None)
|
||||||
|
self._renderPreview()
|
||||||
self._update_flags_menu()
|
self._update_flags_menu()
|
||||||
gui_hooks.browser_did_change_row(self)
|
gui_hooks.browser_did_change_row(self)
|
||||||
|
|
||||||
def currentRow(self) -> int:
|
|
||||||
idx = self.form.tableView.selectionModel().currentIndex()
|
|
||||||
return idx.row()
|
|
||||||
|
|
||||||
# Headers & sorting
|
|
||||||
######################################################################
|
|
||||||
|
|
||||||
def setupHeaders(self) -> None:
|
|
||||||
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)
|
|
||||||
qconnect(hh.customContextMenuRequested, self.onHeaderContext)
|
|
||||||
self.setSortIndicator()
|
|
||||||
qconnect(hh.sortIndicatorChanged, self.onSortChanged)
|
|
||||||
qconnect(hh.sectionMoved, self.onColumnMoved)
|
|
||||||
|
|
||||||
@ensure_editor_saved
|
|
||||||
def onSortChanged(self, idx: int, ord: int) -> None:
|
|
||||||
ord = bool(ord)
|
|
||||||
type = self.model.activeCols[idx]
|
|
||||||
noSort = ("question", "answer")
|
|
||||||
if type in noSort:
|
|
||||||
showInfo(tr.browsing_sorting_on_this_column_is_not())
|
|
||||||
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.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord)
|
|
||||||
self.col.save()
|
|
||||||
self.search()
|
|
||||||
else:
|
|
||||||
if self.col.get_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS) != ord:
|
|
||||||
self.col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord)
|
|
||||||
self.col.save()
|
|
||||||
self.model.reverse()
|
|
||||||
self.setSortIndicator()
|
|
||||||
|
|
||||||
def setSortIndicator(self) -> None:
|
|
||||||
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.get_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS):
|
|
||||||
ord = Qt.DescendingOrder
|
|
||||||
else:
|
|
||||||
ord = Qt.AscendingOrder
|
|
||||||
hh.blockSignals(True)
|
|
||||||
hh.setSortIndicator(idx, ord)
|
|
||||||
hh.blockSignals(False)
|
|
||||||
hh.setSortIndicatorShown(True)
|
|
||||||
|
|
||||||
def onHeaderContext(self, pos: QPoint) -> None:
|
|
||||||
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)
|
|
||||||
qconnect(a.toggled, lambda b, t=type: self.toggleField(t))
|
|
||||||
gui_hooks.browser_header_will_show_context_menu(self, m)
|
|
||||||
m.exec_(gpos)
|
|
||||||
|
|
||||||
@ensure_editor_saved_on_trigger
|
@ensure_editor_saved_on_trigger
|
||||||
def toggleField(self, type: str) -> None:
|
def on_table_state_changed(self) -> None:
|
||||||
self.model.beginReset()
|
self.mw.progress.start()
|
||||||
if type in self.model.activeCols:
|
self.table.toggle_state(self.form.radio_cards.isChecked(), self._lastSearchTxt)
|
||||||
if len(self.model.activeCols) < 2:
|
self.mw.progress.finish()
|
||||||
self.model.endReset()
|
|
||||||
showInfo(tr.browsing_you_must_have_at_least_one())
|
|
||||||
return
|
|
||||||
self.model.activeCols.remove(type)
|
|
||||||
adding = False
|
|
||||||
else:
|
|
||||||
self.model.activeCols.append(type)
|
|
||||||
adding = True
|
|
||||||
self.col.conf["activeCols"] = self.model.activeCols
|
|
||||||
# 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) -> None:
|
# Sidebar
|
||||||
hh = self.form.tableView.horizontalHeader()
|
######################################################################
|
||||||
hh.setSectionResizeMode(QHeaderView.Interactive)
|
|
||||||
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, *args: Any) -> None:
|
|
||||||
self.setColumnSizes()
|
|
||||||
|
|
||||||
def setupSidebar(self) -> None:
|
def setupSidebar(self) -> None:
|
||||||
dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self)
|
dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self)
|
||||||
@ -1052,29 +534,13 @@ QTableView {{ gridline-color: {grid} }}
|
|||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def selected_cards(self) -> List[CardId]:
|
def selected_cards(self) -> List[CardId]:
|
||||||
return [
|
return self.table.get_selected_card_ids()
|
||||||
self.model.cards[idx.row()]
|
|
||||||
for idx in self.form.tableView.selectionModel().selectedRows()
|
|
||||||
]
|
|
||||||
|
|
||||||
def selected_notes(self) -> List[NoteId]:
|
def selected_notes(self) -> List[NoteId]:
|
||||||
return self.col.db.list(
|
return self.table.get_selected_note_ids()
|
||||||
"""
|
|
||||||
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) -> List[CardId]:
|
def selectedNotesAsCards(self) -> List[CardId]:
|
||||||
return self.col.db.list(
|
return self.table.get_card_ids_from_selected_note_ids()
|
||||||
"select id from cards where nid in (%s)"
|
|
||||||
% ",".join([str(s) for s in self.selected_notes()])
|
|
||||||
)
|
|
||||||
|
|
||||||
def oneModelNotes(self) -> List[NoteId]:
|
def oneModelNotes(self) -> List[NoteId]:
|
||||||
sf = self.selected_notes()
|
sf = self.selected_notes()
|
||||||
@ -1154,12 +620,13 @@ where id in %s"""
|
|||||||
return
|
return
|
||||||
|
|
||||||
# nothing selected?
|
# nothing selected?
|
||||||
nids = self.selected_notes()
|
nids = self.table.get_selected_note_ids()
|
||||||
if not nids:
|
if not nids:
|
||||||
return
|
return
|
||||||
|
|
||||||
# select the next card if there is one
|
# select the next card if there is one
|
||||||
self._onNextCard()
|
self.focusTo = self.editor.currentField
|
||||||
|
self.table.to_next_row()
|
||||||
|
|
||||||
remove_notes(
|
remove_notes(
|
||||||
mw=self.mw,
|
mw=self.mw,
|
||||||
@ -1178,7 +645,7 @@ where id in %s"""
|
|||||||
def set_deck_of_selected_cards(self) -> None:
|
def set_deck_of_selected_cards(self) -> None:
|
||||||
from aqt.studydeck import StudyDeck
|
from aqt.studydeck import StudyDeck
|
||||||
|
|
||||||
cids = self.selected_cards()
|
cids = self.table.get_selected_card_ids()
|
||||||
if not cids:
|
if not cids:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1351,27 +818,12 @@ where id in %s"""
|
|||||||
def selectNotes(self) -> None:
|
def selectNotes(self) -> None:
|
||||||
nids = self.selected_notes()
|
nids = self.selected_notes()
|
||||||
# clear the selection so we don't waste energy preserving it
|
# clear the selection so we don't waste energy preserving it
|
||||||
tv = self.form.tableView
|
self.table.clear_selection()
|
||||||
tv.selectionModel().clear()
|
|
||||||
|
|
||||||
search = self.col.build_search_string(
|
search = self.col.build_search_string(
|
||||||
SearchNode(nids=SearchNode.IdList(ids=nids))
|
SearchNode(nids=SearchNode.IdList(ids=nids))
|
||||||
)
|
)
|
||||||
self.search_for(search)
|
self.search_for(search)
|
||||||
|
self.table.select_all()
|
||||||
tv.selectAll()
|
|
||||||
|
|
||||||
def invertSelection(self) -> None:
|
|
||||||
sm = self.form.tableView.selectionModel()
|
|
||||||
items = sm.selection()
|
|
||||||
self.form.tableView.selectAll()
|
|
||||||
sm.select(
|
|
||||||
items,
|
|
||||||
cast(
|
|
||||||
QItemSelectionModel.SelectionFlags,
|
|
||||||
QItemSelectionModel.Deselect | QItemSelectionModel.Rows,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -1380,16 +832,16 @@ where id in %s"""
|
|||||||
gui_hooks.undo_state_did_change.append(self.onUndoState)
|
gui_hooks.undo_state_did_change.append(self.onUndoState)
|
||||||
# fixme: remove this once all items are using `operation_did_execute`
|
# fixme: remove this once all items are using `operation_did_execute`
|
||||||
gui_hooks.sidebar_should_refresh_notetypes.append(self.on_item_added)
|
gui_hooks.sidebar_should_refresh_notetypes.append(self.on_item_added)
|
||||||
gui_hooks.backend_will_block.append(self.on_backend_will_block)
|
gui_hooks.backend_will_block.append(self.table.on_backend_will_block)
|
||||||
gui_hooks.backend_did_block.append(self.on_backend_did_block)
|
gui_hooks.backend_did_block.append(self.table.on_backend_did_block)
|
||||||
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
||||||
gui_hooks.focus_did_change.append(self.on_focus_change)
|
gui_hooks.focus_did_change.append(self.on_focus_change)
|
||||||
|
|
||||||
def teardownHooks(self) -> None:
|
def teardownHooks(self) -> None:
|
||||||
gui_hooks.undo_state_did_change.remove(self.onUndoState)
|
gui_hooks.undo_state_did_change.remove(self.onUndoState)
|
||||||
gui_hooks.sidebar_should_refresh_notetypes.remove(self.on_item_added)
|
gui_hooks.sidebar_should_refresh_notetypes.remove(self.on_item_added)
|
||||||
gui_hooks.backend_will_block.remove(self.on_backend_will_block)
|
gui_hooks.backend_will_block.remove(self.table.on_backend_will_block)
|
||||||
gui_hooks.backend_did_block.remove(self.on_backend_will_block)
|
gui_hooks.backend_did_block.remove(self.table.on_backend_will_block)
|
||||||
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
|
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
|
||||||
gui_hooks.focus_did_change.remove(self.on_focus_change)
|
gui_hooks.focus_did_change.remove(self.on_focus_change)
|
||||||
|
|
||||||
@ -1514,14 +966,14 @@ where id in %s"""
|
|||||||
def _onTagDupes(self, res: List[Any]) -> None:
|
def _onTagDupes(self, res: List[Any]) -> None:
|
||||||
if not res:
|
if not res:
|
||||||
return
|
return
|
||||||
self.model.beginReset()
|
self.begin_reset()
|
||||||
self.mw.checkpoint(tr.browsing_tag_duplicates())
|
self.mw.checkpoint(tr.browsing_tag_duplicates())
|
||||||
nids = set()
|
nids = set()
|
||||||
for _, nidlist in res:
|
for _, nidlist in res:
|
||||||
nids.update(nidlist)
|
nids.update(nidlist)
|
||||||
self.col.tags.bulk_add(list(nids), tr.browsing_duplicate())
|
self.col.tags.bulk_add(list(nids), tr.browsing_duplicate())
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
self.model.endReset()
|
self.end_reset()
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserTagDupes, context=self)
|
||||||
tooltip(tr.browsing_notes_tagged())
|
tooltip(tr.browsing_notes_tagged())
|
||||||
|
|
||||||
@ -1532,69 +984,25 @@ where id in %s"""
|
|||||||
# Jumping
|
# Jumping
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _moveCur(
|
def has_previous_card(self) -> bool:
|
||||||
self, dir: Optional[QTableView.CursorAction], idx: QModelIndex = None
|
return self.table.has_previous()
|
||||||
) -> None:
|
|
||||||
if not self.model.cards:
|
def has_next_card(self) -> bool:
|
||||||
return
|
return self.table.has_next()
|
||||||
tv = self.form.tableView
|
|
||||||
if dir is not None:
|
|
||||||
idx = tv.moveCursor(dir, self.mw.app.keyboardModifiers())
|
|
||||||
tv.selectionModel().setCurrentIndex(
|
|
||||||
idx,
|
|
||||||
cast(
|
|
||||||
QItemSelectionModel.SelectionFlags,
|
|
||||||
QItemSelectionModel.Clear
|
|
||||||
| QItemSelectionModel.Select
|
|
||||||
| QItemSelectionModel.Rows,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def onPreviousCard(self) -> None:
|
def onPreviousCard(self) -> None:
|
||||||
self.focusTo = self.editor.currentField
|
self.focusTo = self.editor.currentField
|
||||||
self.editor.call_after_note_saved(self._onPreviousCard)
|
self.editor.call_after_note_saved(self.table.to_previous_row)
|
||||||
|
|
||||||
def _onPreviousCard(self) -> None:
|
|
||||||
self._moveCur(QAbstractItemView.MoveUp)
|
|
||||||
|
|
||||||
def onNextCard(self) -> None:
|
def onNextCard(self) -> None:
|
||||||
self.focusTo = self.editor.currentField
|
self.focusTo = self.editor.currentField
|
||||||
self.editor.call_after_note_saved(self._onNextCard)
|
self.editor.call_after_note_saved(self.table.to_next_row)
|
||||||
|
|
||||||
def _onNextCard(self) -> None:
|
|
||||||
self._moveCur(QAbstractItemView.MoveDown)
|
|
||||||
|
|
||||||
def onFirstCard(self) -> None:
|
def onFirstCard(self) -> None:
|
||||||
sm = self.form.tableView.selectionModel()
|
self.table.to_first_row()
|
||||||
idx = sm.currentIndex()
|
|
||||||
self._moveCur(None, self.model.index(0, 0))
|
|
||||||
if not KeyboardModifiersPressed().shift:
|
|
||||||
return
|
|
||||||
idx2 = sm.currentIndex()
|
|
||||||
item = QItemSelection(idx2, idx)
|
|
||||||
sm.select(
|
|
||||||
item,
|
|
||||||
cast(
|
|
||||||
QItemSelectionModel.SelectionFlags,
|
|
||||||
QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def onLastCard(self) -> None:
|
def onLastCard(self) -> None:
|
||||||
sm = self.form.tableView.selectionModel()
|
self.table.to_last_row()
|
||||||
idx = sm.currentIndex()
|
|
||||||
self._moveCur(None, self.model.index(len(self.model.cards) - 1, 0))
|
|
||||||
if not KeyboardModifiersPressed().shift:
|
|
||||||
return
|
|
||||||
idx2 = sm.currentIndex()
|
|
||||||
item = QItemSelection(idx, idx2)
|
|
||||||
sm.select(
|
|
||||||
item,
|
|
||||||
cast(
|
|
||||||
QItemSelectionModel.SelectionFlags,
|
|
||||||
QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def onFind(self) -> None:
|
def onFind(self) -> None:
|
||||||
# workaround for PyQt focus bug
|
# workaround for PyQt focus bug
|
||||||
@ -1613,14 +1021,6 @@ where id in %s"""
|
|||||||
def onCardList(self) -> None:
|
def onCardList(self) -> None:
|
||||||
self.form.tableView.setFocus()
|
self.form.tableView.setFocus()
|
||||||
|
|
||||||
def focusCid(self, cid: CardId) -> None:
|
|
||||||
try:
|
|
||||||
row = list(self.model.cards).index(cid)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
self.form.tableView.clearSelection()
|
|
||||||
self.form.tableView.selectRow(row)
|
|
||||||
|
|
||||||
|
|
||||||
# Change model dialog
|
# Change model dialog
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -1796,11 +1196,11 @@ class ChangeModel(QDialog):
|
|||||||
b = self.browser
|
b = self.browser
|
||||||
b.mw.col.modSchema(check=True)
|
b.mw.col.modSchema(check=True)
|
||||||
b.mw.progress.start()
|
b.mw.progress.start()
|
||||||
b.model.beginReset()
|
b.begin_reset()
|
||||||
mm = b.mw.col.models
|
mm = b.mw.col.models
|
||||||
mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap)
|
mm.change(self.oldModel, self.nids, self.targetModel, fmap, cmap)
|
||||||
b.search()
|
b.search()
|
||||||
b.model.endReset()
|
b.end_reset()
|
||||||
b.mw.progress.finish()
|
b.mw.progress.finish()
|
||||||
b.mw.reset()
|
b.mw.reset()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>12</number>
|
<number>6</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="horizontalSpacing">
|
<property name="horizontalSpacing">
|
||||||
<number>12</number>
|
<number>12</number>
|
||||||
@ -109,6 +109,30 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="view_state" stretch="0,1">
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>5</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="radio_cards">
|
||||||
|
<property name="text">
|
||||||
|
<string>qt_accel_cards</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QRadioButton" name="radio_notes">
|
||||||
|
<property name="text">
|
||||||
|
<string>qt_accel_notes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTableView" name="tableView">
|
<widget class="QTableView" name="tableView">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -144,12 +168,12 @@
|
|||||||
<attribute name="horizontalHeaderCascadingSectionResizes">
|
<attribute name="horizontalHeaderCascadingSectionResizes">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="horizontalHeaderMinimumSectionSize">
|
|
||||||
<number>20</number>
|
|
||||||
</attribute>
|
|
||||||
<attribute name="horizontalHeaderHighlightSections">
|
<attribute name="horizontalHeaderHighlightSections">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
|
<attribute name="horizontalHeaderMinimumSectionSize">
|
||||||
|
<number>20</number>
|
||||||
|
</attribute>
|
||||||
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
@ -209,7 +233,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>750</width>
|
<width>750</width>
|
||||||
<height>24</height>
|
<height>21</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuEdit">
|
<widget class="QMenu" name="menuEdit">
|
||||||
|
@ -13,7 +13,6 @@ from anki.cards import Card
|
|||||||
from anki.collection import Config
|
from anki.collection import Config
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import (
|
from aqt.qt import (
|
||||||
QAbstractItemView,
|
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
@ -326,23 +325,16 @@ class BrowserPreviewer(MultiCardPreviewer):
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _on_prev_card(self) -> None:
|
def _on_prev_card(self) -> None:
|
||||||
self._parent.editor.call_after_note_saved(
|
self._parent.onPreviousCard()
|
||||||
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_next_card(self) -> None:
|
def _on_next_card(self) -> None:
|
||||||
self._parent.editor.call_after_note_saved(
|
self._parent.onNextCard()
|
||||||
lambda: self._parent._moveCur(QAbstractItemView.MoveDown)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _should_enable_prev(self) -> bool:
|
def _should_enable_prev(self) -> bool:
|
||||||
return super()._should_enable_prev() or self._parent.currentRow() > 0
|
return super()._should_enable_prev() or self._parent.has_previous_card()
|
||||||
|
|
||||||
def _should_enable_next(self) -> bool:
|
def _should_enable_next(self) -> bool:
|
||||||
return (
|
return super()._should_enable_next() or self._parent.has_next_card()
|
||||||
super()._should_enable_next()
|
|
||||||
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
def _render_scheduled(self) -> None:
|
def _render_scheduled(self) -> None:
|
||||||
super()._render_scheduled()
|
super()._render_scheduled()
|
||||||
|
1136
qt/aqt/table.py
Normal file
1136
qt/aqt/table.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -386,7 +386,7 @@ hooks = [
|
|||||||
),
|
),
|
||||||
Hook(
|
Hook(
|
||||||
name="browser_will_search",
|
name="browser_will_search",
|
||||||
args=["context: aqt.browser.SearchContext"],
|
args=["context: aqt.table.SearchContext"],
|
||||||
doc="""Allows you to modify the search text, or perform your own search.
|
doc="""Allows you to modify the search text, or perform your own search.
|
||||||
|
|
||||||
You can modify context.search to change the text that is sent to the
|
You can modify context.search to change the text that is sent to the
|
||||||
@ -401,12 +401,12 @@ hooks = [
|
|||||||
),
|
),
|
||||||
Hook(
|
Hook(
|
||||||
name="browser_did_search",
|
name="browser_did_search",
|
||||||
args=["context: aqt.browser.SearchContext"],
|
args=["context: aqt.table.SearchContext"],
|
||||||
doc="""Allows you to modify the list of returned card ids from a search.""",
|
doc="""Allows you to modify the list of returned card ids from a search.""",
|
||||||
),
|
),
|
||||||
Hook(
|
Hook(
|
||||||
name="browser_did_fetch_row",
|
name="browser_did_fetch_row",
|
||||||
args=["card_id: int", "row: aqt.browser.CellRow", "columns: Sequence[str]"],
|
args=["card_id: int", "row: aqt.table.CellRow", "columns: Sequence[str]"],
|
||||||
doc="""Allows you to add or modify content to a row in the browser.
|
doc="""Allows you to add or modify content to a row in the browser.
|
||||||
|
|
||||||
You can mutate the row object to change what is displayed. Any columns the
|
You can mutate the row object to change what is displayed. Any columns the
|
||||||
|
@ -237,12 +237,12 @@ service TagsService {
|
|||||||
|
|
||||||
service SearchService {
|
service SearchService {
|
||||||
rpc BuildSearchString(SearchNode) returns (String);
|
rpc BuildSearchString(SearchNode) returns (String);
|
||||||
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
|
rpc SearchCards(SearchIn) returns (SearchOut);
|
||||||
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
|
rpc SearchNotes(SearchIn) returns (SearchOut);
|
||||||
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
|
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
|
||||||
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
|
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
|
||||||
rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount);
|
rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount);
|
||||||
rpc BrowserRowForCard(CardId) returns (BrowserRow);
|
rpc BrowserRowForId(Int64) returns (BrowserRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
service StatsService {
|
service StatsService {
|
||||||
@ -795,31 +795,33 @@ message OpenCollectionIn {
|
|||||||
string log_path = 4;
|
string log_path = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchCardsIn {
|
message SearchIn {
|
||||||
string search = 1;
|
string search = 1;
|
||||||
SortOrder order = 2;
|
SortOrder order = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchCardsOut {
|
message SearchOut {
|
||||||
repeated int64 card_ids = 1;
|
repeated int64 ids = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SortOrder {
|
message SortOrder {
|
||||||
message Builtin {
|
message Builtin {
|
||||||
enum Kind {
|
enum Kind {
|
||||||
NOTE_CREATION = 0;
|
NOTE_CARDS = 0;
|
||||||
NOTE_MOD = 1;
|
NOTE_CREATION = 1;
|
||||||
NOTE_FIELD = 2;
|
NOTE_EASE = 2;
|
||||||
NOTE_TAGS = 3;
|
NOTE_MOD = 3;
|
||||||
NOTETYPE = 4;
|
NOTE_FIELD = 4;
|
||||||
CARD_MOD = 5;
|
NOTE_TAGS = 5;
|
||||||
CARD_REPS = 6;
|
NOTETYPE = 6;
|
||||||
CARD_DUE = 7;
|
CARD_MOD = 7;
|
||||||
CARD_EASE = 8;
|
CARD_REPS = 8;
|
||||||
CARD_LAPSES = 9;
|
CARD_DUE = 9;
|
||||||
CARD_INTERVAL = 10;
|
CARD_EASE = 10;
|
||||||
CARD_DECK = 11;
|
CARD_LAPSES = 11;
|
||||||
CARD_TEMPLATE = 12;
|
CARD_INTERVAL = 12;
|
||||||
|
CARD_DECK = 13;
|
||||||
|
CARD_TEMPLATE = 14;
|
||||||
}
|
}
|
||||||
Kind kind = 1;
|
Kind kind = 1;
|
||||||
bool reverse = 2;
|
bool reverse = 2;
|
||||||
@ -832,14 +834,6 @@ message SortOrder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchNotesIn {
|
|
||||||
string search = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SearchNotesOut {
|
|
||||||
repeated int64 note_ids = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SearchNode {
|
message SearchNode {
|
||||||
message Dupe {
|
message Dupe {
|
||||||
int64 notetype_id = 1;
|
int64 notetype_id = 1;
|
||||||
@ -1351,22 +1345,24 @@ message SetDeckIn {
|
|||||||
message Config {
|
message Config {
|
||||||
message Bool {
|
message Bool {
|
||||||
enum Key {
|
enum Key {
|
||||||
BROWSER_SORT_BACKWARDS = 0;
|
BROWSER_CARD_STATE = 0;
|
||||||
PREVIEW_BOTH_SIDES = 1;
|
BROWSER_SORT_BACKWARDS = 1;
|
||||||
COLLAPSE_TAGS = 2;
|
BROWSER_NOTE_SORT_BACKWARDS = 2;
|
||||||
COLLAPSE_NOTETYPES = 3;
|
PREVIEW_BOTH_SIDES = 3;
|
||||||
COLLAPSE_DECKS = 4;
|
COLLAPSE_TAGS = 4;
|
||||||
COLLAPSE_SAVED_SEARCHES = 5;
|
COLLAPSE_NOTETYPES = 5;
|
||||||
COLLAPSE_TODAY = 6;
|
COLLAPSE_DECKS = 6;
|
||||||
COLLAPSE_CARD_STATE = 7;
|
COLLAPSE_SAVED_SEARCHES = 7;
|
||||||
COLLAPSE_FLAGS = 8;
|
COLLAPSE_TODAY = 8;
|
||||||
SCHED_2021 = 9;
|
COLLAPSE_CARD_STATE = 9;
|
||||||
ADDING_DEFAULTS_TO_CURRENT_DECK = 10;
|
COLLAPSE_FLAGS = 10;
|
||||||
HIDE_AUDIO_PLAY_BUTTONS = 11;
|
SCHED_2021 = 11;
|
||||||
INTERRUPT_AUDIO_WHEN_ANSWERING = 12;
|
ADDING_DEFAULTS_TO_CURRENT_DECK = 12;
|
||||||
PASTE_IMAGES_AS_PNG = 13;
|
HIDE_AUDIO_PLAY_BUTTONS = 13;
|
||||||
PASTE_STRIPS_FORMATTING = 14;
|
INTERRUPT_AUDIO_WHEN_ANSWERING = 14;
|
||||||
NORMALIZE_NOTE_TEXT = 15;
|
PASTE_IMAGES_AS_PNG = 15;
|
||||||
|
PASTE_STRIPS_FORMATTING = 16;
|
||||||
|
NORMALIZE_NOTE_TEXT = 17;
|
||||||
}
|
}
|
||||||
Key key = 1;
|
Key key = 1;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,9 @@ use serde_json::Value;
|
|||||||
impl From<BoolKeyProto> for BoolKey {
|
impl From<BoolKeyProto> for BoolKey {
|
||||||
fn from(k: BoolKeyProto) -> Self {
|
fn from(k: BoolKeyProto) -> Self {
|
||||||
match k {
|
match k {
|
||||||
|
BoolKeyProto::BrowserCardState => BoolKey::BrowserCardState,
|
||||||
BoolKeyProto::BrowserSortBackwards => BoolKey::BrowserSortBackwards,
|
BoolKeyProto::BrowserSortBackwards => BoolKey::BrowserSortBackwards,
|
||||||
|
BoolKeyProto::BrowserNoteSortBackwards => BoolKey::BrowserNoteSortBackwards,
|
||||||
BoolKeyProto::PreviewBothSides => BoolKey::PreviewBothSides,
|
BoolKeyProto::PreviewBothSides => BoolKey::PreviewBothSides,
|
||||||
BoolKeyProto::CollapseTags => BoolKey::CollapseTags,
|
BoolKeyProto::CollapseTags => BoolKey::CollapseTags,
|
||||||
BoolKeyProto::CollapseNotetypes => BoolKey::CollapseNotetypes,
|
BoolKeyProto::CollapseNotetypes => BoolKey::CollapseNotetypes,
|
||||||
|
@ -24,21 +24,22 @@ impl SearchService for Backend {
|
|||||||
Ok(write_nodes(&node.into_node_list()).into())
|
Ok(write_nodes(&node.into_node_list()).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_cards(&self, input: pb::SearchCardsIn) -> Result<pb::SearchCardsOut> {
|
fn search_cards(&self, input: pb::SearchIn) -> Result<pb::SearchOut> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let order = input.order.unwrap_or_default().value.into();
|
let order = input.order.unwrap_or_default().value.into();
|
||||||
let cids = col.search_cards(&input.search, order)?;
|
let cids = col.search::<CardId>(&input.search, order)?;
|
||||||
Ok(pb::SearchCardsOut {
|
Ok(pb::SearchOut {
|
||||||
card_ids: cids.into_iter().map(|v| v.0).collect(),
|
ids: cids.into_iter().map(|v| v.0).collect(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_notes(&self, input: pb::SearchNotesIn) -> Result<pb::SearchNotesOut> {
|
fn search_notes(&self, input: pb::SearchIn) -> Result<pb::SearchOut> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
let nids = col.search_notes(&input.search)?;
|
let order = input.order.unwrap_or_default().value.into();
|
||||||
Ok(pb::SearchNotesOut {
|
let nids = col.search::<NoteId>(&input.search, order)?;
|
||||||
note_ids: nids.into_iter().map(|v| v.0).collect(),
|
Ok(pb::SearchOut {
|
||||||
|
ids: nids.into_iter().map(|v| v.0).collect(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -88,15 +89,17 @@ impl SearchService for Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn browser_row_for_card(&self, input: pb::CardId) -> Result<pb::BrowserRow> {
|
fn browser_row_for_id(&self, input: pb::Int64) -> Result<pb::BrowserRow> {
|
||||||
self.with_col(|col| col.browser_row_for_card(input.cid.into()).map(Into::into))
|
self.with_col(|col| col.browser_row_for_id(input.val).map(Into::into))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SortKindProto> for SortKind {
|
impl From<SortKindProto> for SortKind {
|
||||||
fn from(kind: SortKindProto) -> Self {
|
fn from(kind: SortKindProto) -> Self {
|
||||||
match kind {
|
match kind {
|
||||||
|
SortKindProto::NoteCards => SortKind::NoteCards,
|
||||||
SortKindProto::NoteCreation => SortKind::NoteCreation,
|
SortKindProto::NoteCreation => SortKind::NoteCreation,
|
||||||
|
SortKindProto::NoteEase => SortKind::NoteEase,
|
||||||
SortKindProto::NoteMod => SortKind::NoteMod,
|
SortKindProto::NoteMod => SortKind::NoteMod,
|
||||||
SortKindProto::NoteField => SortKind::NoteField,
|
SortKindProto::NoteField => SortKind::NoteField,
|
||||||
SortKindProto::NoteTags => SortKind::NoteTags,
|
SortKindProto::NoteTags => SortKind::NoteTags,
|
||||||
|
@ -10,8 +10,9 @@ use crate::i18n::I18n;
|
|||||||
use crate::{
|
use crate::{
|
||||||
card::{Card, CardId, CardQueue, CardType},
|
card::{Card, CardId, CardQueue, CardType},
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
|
config::BoolKey,
|
||||||
decks::{Deck, DeckId},
|
decks::{Deck, DeckId},
|
||||||
notes::Note,
|
notes::{Note, NoteId},
|
||||||
notetype::{CardTemplate, Notetype, NotetypeKind},
|
notetype::{CardTemplate, Notetype, NotetypeKind},
|
||||||
scheduler::{timespan::time_span, timing::SchedTimingToday},
|
scheduler::{timespan::time_span, timing::SchedTimingToday},
|
||||||
template::RenderedNode,
|
template::RenderedNode,
|
||||||
@ -51,7 +52,54 @@ pub struct Font {
|
|||||||
pub size: u32,
|
pub size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RowContext<'a> {
|
trait RowContext {
|
||||||
|
fn get_cell_text(&mut self, column: &str) -> Result<String>;
|
||||||
|
fn get_row_color(&self) -> Color;
|
||||||
|
fn get_row_font(&self) -> Result<Font>;
|
||||||
|
fn note(&self) -> &Note;
|
||||||
|
fn notetype(&self) -> &Notetype;
|
||||||
|
|
||||||
|
fn get_cell(&mut self, column: &str) -> Result<Cell> {
|
||||||
|
Ok(Cell {
|
||||||
|
text: self.get_cell_text(column)?,
|
||||||
|
is_rtl: self.get_is_rtl(column),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_creation_str(&self) -> String {
|
||||||
|
TimestampMillis(self.note().id.into())
|
||||||
|
.as_secs()
|
||||||
|
.date_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_field_str(&self) -> String {
|
||||||
|
let index = self.notetype().config.sort_field_idx as usize;
|
||||||
|
html_to_text_line(&self.note().fields()[index]).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_is_rtl(&self, column: &str) -> bool {
|
||||||
|
match column {
|
||||||
|
"noteFld" => {
|
||||||
|
let index = self.notetype().config.sort_field_idx as usize;
|
||||||
|
self.notetype().fields[index].config.rtl
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_row_for_id(&mut self, columns: &[String]) -> Result<Row> {
|
||||||
|
Ok(Row {
|
||||||
|
cells: columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| self.get_cell(column))
|
||||||
|
.collect::<Result<_>>()?,
|
||||||
|
color: self.get_row_color(),
|
||||||
|
font: self.get_row_font()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CardRowContext<'a> {
|
||||||
col: &'a Collection,
|
col: &'a Collection,
|
||||||
card: Card,
|
card: Card,
|
||||||
note: Note,
|
note: Note,
|
||||||
@ -70,6 +118,13 @@ struct RenderContext {
|
|||||||
answer_nodes: Vec<RenderedNode>,
|
answer_nodes: Vec<RenderedNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NoteRowContext<'a> {
|
||||||
|
note: Note,
|
||||||
|
notetype: Arc<Notetype>,
|
||||||
|
cards: Vec<Card>,
|
||||||
|
tr: &'a I18n,
|
||||||
|
}
|
||||||
|
|
||||||
fn card_render_required(columns: &[String]) -> bool {
|
fn card_render_required(columns: &[String]) -> bool {
|
||||||
columns
|
columns
|
||||||
.iter()
|
.iter()
|
||||||
@ -77,19 +132,28 @@ fn card_render_required(columns: &[String]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub fn browser_row_for_card(&mut self, id: CardId) -> Result<Row> {
|
pub fn browser_row_for_id(&mut self, id: i64) -> Result<Row> {
|
||||||
// this is inefficient; we may want to use an enum in the future
|
if self.get_bool(BoolKey::BrowserCardState) {
|
||||||
let columns = self.get_desktop_browser_card_columns();
|
// this is inefficient; we may want to use an enum in the future
|
||||||
let mut context = RowContext::new(self, id, card_render_required(&columns))?;
|
let columns = self.get_desktop_browser_card_columns();
|
||||||
|
CardRowContext::new(self, id, card_render_required(&columns))?
|
||||||
|
.browser_row_for_id(&columns)
|
||||||
|
} else {
|
||||||
|
let columns = self.get_desktop_browser_note_columns();
|
||||||
|
NoteRowContext::new(self, id)?.browser_row_for_id(&columns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Row {
|
fn get_note_maybe_with_fields(&self, id: NoteId, _with_fields: bool) -> Result<Note> {
|
||||||
cells: columns
|
// todo: After note.sort_field has been modified so it can be displayed in the browser,
|
||||||
.iter()
|
// we can update note_field_str() and only load the note with fields if a card render is
|
||||||
.map(|column| context.get_cell(column))
|
// necessary (see #1082).
|
||||||
.collect::<Result<_>>()?,
|
if true {
|
||||||
color: context.get_row_color(),
|
self.storage.get_note(id)?
|
||||||
font: context.get_row_font()?,
|
} else {
|
||||||
})
|
self.storage.get_note_without_fields(id)?
|
||||||
|
}
|
||||||
|
.ok_or(AnkiError::NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,18 +187,13 @@ impl RenderContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RowContext<'a> {
|
impl<'a> CardRowContext<'a> {
|
||||||
fn new(col: &'a mut Collection, id: CardId, with_card_render: bool) -> Result<Self> {
|
fn new(col: &'a mut Collection, id: i64, with_card_render: bool) -> Result<Self> {
|
||||||
let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?;
|
let card = col
|
||||||
// todo: After note.sort_field has been modified so it can be displayed in the browser,
|
.storage
|
||||||
// we can update note_field_str() and only load the note with fields if a card render is
|
.get_card(CardId(id))?
|
||||||
// necessary (see #1082).
|
.ok_or(AnkiError::NotFound)?;
|
||||||
let note = if true {
|
let note = col.get_note_maybe_with_fields(card.note_id, with_card_render)?;
|
||||||
col.storage.get_note(card.note_id)?
|
|
||||||
} else {
|
|
||||||
col.storage.get_note_without_fields(card.note_id)?
|
|
||||||
}
|
|
||||||
.ok_or(AnkiError::NotFound)?;
|
|
||||||
let notetype = col
|
let notetype = col
|
||||||
.get_notetype(note.notetype_id)?
|
.get_notetype(note.notetype_id)?
|
||||||
.ok_or(AnkiError::NotFound)?;
|
.ok_or(AnkiError::NotFound)?;
|
||||||
@ -145,7 +204,7 @@ impl<'a> RowContext<'a> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RowContext {
|
Ok(CardRowContext {
|
||||||
col,
|
col,
|
||||||
card,
|
card,
|
||||||
note,
|
note,
|
||||||
@ -181,34 +240,6 @@ impl<'a> RowContext<'a> {
|
|||||||
Ok(self.original_deck.as_ref().unwrap())
|
Ok(self.original_deck.as_ref().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cell(&mut self, column: &str) -> Result<Cell> {
|
|
||||||
Ok(Cell {
|
|
||||||
text: self.get_cell_text(column)?,
|
|
||||||
is_rtl: self.get_is_rtl(column),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cell_text(&mut self, column: &str) -> Result<String> {
|
|
||||||
Ok(match column {
|
|
||||||
"answer" => self.answer_str(),
|
|
||||||
"cardDue" => self.card_due_str(),
|
|
||||||
"cardEase" => self.card_ease_str(),
|
|
||||||
"cardIvl" => self.card_interval_str(),
|
|
||||||
"cardLapses" => self.card.lapses.to_string(),
|
|
||||||
"cardMod" => self.card.mtime.date_string(),
|
|
||||||
"cardReps" => self.card.reps.to_string(),
|
|
||||||
"deck" => self.deck_str()?,
|
|
||||||
"note" => self.notetype.name.to_owned(),
|
|
||||||
"noteCrt" => self.note_creation_str(),
|
|
||||||
"noteFld" => self.note_field_str(),
|
|
||||||
"noteMod" => self.note.mtime.date_string(),
|
|
||||||
"noteTags" => self.note.tags.join(" "),
|
|
||||||
"question" => self.question_str(),
|
|
||||||
"template" => self.template_str()?,
|
|
||||||
_ => "".to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn answer_str(&self) -> String {
|
fn answer_str(&self) -> String {
|
||||||
let render_context = self.render_context.as_ref().unwrap();
|
let render_context = self.render_context.as_ref().unwrap();
|
||||||
let answer = render_context
|
let answer = render_context
|
||||||
@ -284,15 +315,6 @@ impl<'a> RowContext<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn note_creation_str(&self) -> String {
|
|
||||||
TimestampMillis(self.note.id.into()).as_secs().date_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn note_field_str(&self) -> String {
|
|
||||||
let index = self.notetype.config.sort_field_idx as usize;
|
|
||||||
html_to_text_line(&self.note.fields()[index]).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn template_str(&self) -> Result<String> {
|
fn template_str(&self) -> Result<String> {
|
||||||
let name = &self.template()?.name;
|
let name = &self.template()?.name;
|
||||||
Ok(match self.notetype.config.kind() {
|
Ok(match self.notetype.config.kind() {
|
||||||
@ -304,15 +326,28 @@ impl<'a> RowContext<'a> {
|
|||||||
fn question_str(&self) -> String {
|
fn question_str(&self) -> String {
|
||||||
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
|
html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_is_rtl(&self, column: &str) -> bool {
|
impl RowContext for CardRowContext<'_> {
|
||||||
match column {
|
fn get_cell_text(&mut self, column: &str) -> Result<String> {
|
||||||
"noteFld" => {
|
Ok(match column {
|
||||||
let index = self.notetype.config.sort_field_idx as usize;
|
"answer" => self.answer_str(),
|
||||||
self.notetype.fields[index].config.rtl
|
"cardDue" => self.card_due_str(),
|
||||||
}
|
"cardEase" => self.card_ease_str(),
|
||||||
_ => false,
|
"cardIvl" => self.card_interval_str(),
|
||||||
}
|
"cardLapses" => self.card.lapses.to_string(),
|
||||||
|
"cardMod" => self.card.mtime.date_string(),
|
||||||
|
"cardReps" => self.card.reps.to_string(),
|
||||||
|
"deck" => self.deck_str()?,
|
||||||
|
"note" => self.notetype.name.to_owned(),
|
||||||
|
"noteCrt" => self.note_creation_str(),
|
||||||
|
"noteFld" => self.note_field_str(),
|
||||||
|
"noteMod" => self.note.mtime.date_string(),
|
||||||
|
"noteTags" => self.note.tags.join(" "),
|
||||||
|
"question" => self.question_str(),
|
||||||
|
"template" => self.template_str()?,
|
||||||
|
_ => "".to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_row_color(&self) -> Color {
|
fn get_row_color(&self) -> Color {
|
||||||
@ -344,4 +379,86 @@ impl<'a> RowContext<'a> {
|
|||||||
size: self.template()?.config.browser_font_size,
|
size: self.template()?.config.browser_font_size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn note(&self) -> &Note {
|
||||||
|
&self.note
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notetype(&self) -> &Notetype {
|
||||||
|
&self.notetype
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NoteRowContext<'a> {
|
||||||
|
fn new(col: &'a mut Collection, id: i64) -> Result<Self> {
|
||||||
|
let note = col.get_note_maybe_with_fields(NoteId(id), false)?;
|
||||||
|
let notetype = col
|
||||||
|
.get_notetype(note.notetype_id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
let cards = col.storage.all_cards_of_note(note.id)?;
|
||||||
|
|
||||||
|
Ok(NoteRowContext {
|
||||||
|
note,
|
||||||
|
notetype,
|
||||||
|
cards,
|
||||||
|
tr: &col.tr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_ease_str(&self) -> String {
|
||||||
|
let cards = self
|
||||||
|
.cards
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.ctype != CardType::New)
|
||||||
|
.collect::<Vec<&Card>>();
|
||||||
|
if cards.is_empty() {
|
||||||
|
self.tr.browsing_new().into()
|
||||||
|
} else {
|
||||||
|
let ease = cards.iter().map(|c| c.ease_factor).sum::<u16>() / cards.len() as u16;
|
||||||
|
format!("{}%", ease / 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RowContext for NoteRowContext<'_> {
|
||||||
|
fn get_cell_text(&mut self, column: &str) -> Result<String> {
|
||||||
|
Ok(match column {
|
||||||
|
"note" => self.notetype.name.to_owned(),
|
||||||
|
"noteCards" => self.cards.len().to_string(),
|
||||||
|
"noteCrt" => self.note_creation_str(),
|
||||||
|
"noteEase" => self.note_ease_str(),
|
||||||
|
"noteFld" => self.note_field_str(),
|
||||||
|
"noteMod" => self.note.mtime.date_string(),
|
||||||
|
"noteTags" => self.note.tags.join(" "),
|
||||||
|
_ => "".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_row_color(&self) -> Color {
|
||||||
|
if self
|
||||||
|
.note
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.any(|tag| tag.eq_ignore_ascii_case("marked"))
|
||||||
|
{
|
||||||
|
Color::Marked
|
||||||
|
} else {
|
||||||
|
Color::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_row_font(&self) -> Result<Font> {
|
||||||
|
Ok(Font {
|
||||||
|
name: "".to_owned(),
|
||||||
|
size: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note(&self) -> &Note {
|
||||||
|
&self.note
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notetype(&self) -> &Notetype {
|
||||||
|
&self.notetype
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ use strum::IntoStaticStr;
|
|||||||
#[derive(Debug, Clone, Copy, IntoStaticStr)]
|
#[derive(Debug, Clone, Copy, IntoStaticStr)]
|
||||||
#[strum(serialize_all = "camelCase")]
|
#[strum(serialize_all = "camelCase")]
|
||||||
pub enum BoolKey {
|
pub enum BoolKey {
|
||||||
|
BrowserCardState,
|
||||||
|
BrowserNoteSortBackwards,
|
||||||
CardCountsSeparateInactive,
|
CardCountsSeparateInactive,
|
||||||
CollapseCardState,
|
CollapseCardState,
|
||||||
CollapseDecks,
|
CollapseDecks,
|
||||||
@ -60,7 +62,8 @@ impl Collection {
|
|||||||
| BoolKey::FutureDueShowBacklog
|
| BoolKey::FutureDueShowBacklog
|
||||||
| BoolKey::ShowRemainingDueCountsInStudy
|
| BoolKey::ShowRemainingDueCountsInStudy
|
||||||
| BoolKey::CardCountsSeparateInactive
|
| BoolKey::CardCountsSeparateInactive
|
||||||
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),
|
| BoolKey::NormalizeNoteText
|
||||||
|
| BoolKey::BrowserCardState => self.get_config_optional(key).unwrap_or(true),
|
||||||
|
|
||||||
// other options default to false
|
// other options default to false
|
||||||
other => self.get_config_default(other),
|
other => self.get_config_default(other),
|
||||||
|
@ -48,6 +48,8 @@ pub(crate) enum ConfigKey {
|
|||||||
AnswerTimeLimitSecs,
|
AnswerTimeLimitSecs,
|
||||||
#[strum(to_string = "sortType")]
|
#[strum(to_string = "sortType")]
|
||||||
BrowserSortKind,
|
BrowserSortKind,
|
||||||
|
#[strum(to_string = "noteSortType")]
|
||||||
|
BrowserNoteSortKind,
|
||||||
#[strum(to_string = "curDeck")]
|
#[strum(to_string = "curDeck")]
|
||||||
CurrentDeckId,
|
CurrentDeckId,
|
||||||
#[strum(to_string = "curModel")]
|
#[strum(to_string = "curModel")]
|
||||||
@ -65,6 +67,8 @@ pub(crate) enum ConfigKey {
|
|||||||
|
|
||||||
#[strum(to_string = "activeCols")]
|
#[strum(to_string = "activeCols")]
|
||||||
DesktopBrowserCardColumns,
|
DesktopBrowserCardColumns,
|
||||||
|
#[strum(to_string = "activeNoteCols")]
|
||||||
|
DesktopBrowserNoteColumns,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
|
#[derive(PartialEq, Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
|
||||||
@ -132,6 +136,10 @@ impl Collection {
|
|||||||
self.get_config_default(ConfigKey::BrowserSortKind)
|
self.get_config_default(ConfigKey::BrowserSortKind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_browser_note_sort_kind(&self) -> SortKind {
|
||||||
|
self.get_config_default(ConfigKey::BrowserNoteSortKind)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_desktop_browser_card_columns(&self) -> Vec<String> {
|
pub(crate) fn get_desktop_browser_card_columns(&self) -> Vec<String> {
|
||||||
self.get_config_optional(ConfigKey::DesktopBrowserCardColumns)
|
self.get_config_optional(ConfigKey::DesktopBrowserCardColumns)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
@ -144,6 +152,18 @@ impl Collection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_desktop_browser_note_columns(&self) -> Vec<String> {
|
||||||
|
self.get_config_optional(ConfigKey::DesktopBrowserNoteColumns)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
vec![
|
||||||
|
"noteFld".to_string(),
|
||||||
|
"note".to_string(),
|
||||||
|
"noteTags".to_string(),
|
||||||
|
"noteMod".to_string(),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
|
pub(crate) fn get_creation_utc_offset(&self) -> Option<i32> {
|
||||||
self.get_config_optional(ConfigKey::CreationOffset)
|
self.get_config_optional(ConfigKey::CreationOffset)
|
||||||
}
|
}
|
||||||
@ -251,8 +271,10 @@ impl Collection {
|
|||||||
#[derive(Deserialize, PartialEq, Debug, Clone, Copy)]
|
#[derive(Deserialize, PartialEq, Debug, Clone, Copy)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum SortKind {
|
pub enum SortKind {
|
||||||
|
NoteCards,
|
||||||
#[serde(rename = "noteCrt")]
|
#[serde(rename = "noteCrt")]
|
||||||
NoteCreation,
|
NoteCreation,
|
||||||
|
NoteEase,
|
||||||
NoteMod,
|
NoteMod,
|
||||||
#[serde(rename = "noteFld")]
|
#[serde(rename = "noteFld")]
|
||||||
NoteField,
|
NoteField,
|
||||||
|
@ -1,198 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
parser::Node,
|
|
||||||
sqlwriter::{RequiredTable, SqlWriter},
|
|
||||||
};
|
|
||||||
use crate::{
|
|
||||||
card::CardId,
|
|
||||||
card::CardType,
|
|
||||||
collection::Collection,
|
|
||||||
config::{BoolKey, SortKind},
|
|
||||||
err::Result,
|
|
||||||
search::parser::parse,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
|
||||||
pub enum SortMode {
|
|
||||||
NoOrder,
|
|
||||||
FromConfig,
|
|
||||||
Builtin { kind: SortKind, reverse: bool },
|
|
||||||
Custom(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SortMode {
|
|
||||||
fn required_table(&self) -> RequiredTable {
|
|
||||||
match self {
|
|
||||||
SortMode::NoOrder => RequiredTable::Cards,
|
|
||||||
SortMode::FromConfig => unreachable!(),
|
|
||||||
SortMode::Builtin { kind, .. } => kind.required_table(),
|
|
||||||
SortMode::Custom(ref text) => {
|
|
||||||
if text.contains("n.") {
|
|
||||||
RequiredTable::CardsAndNotes
|
|
||||||
} else {
|
|
||||||
RequiredTable::Cards
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SortKind {
|
|
||||||
fn required_table(self) -> RequiredTable {
|
|
||||||
match self {
|
|
||||||
SortKind::NoteCreation
|
|
||||||
| SortKind::NoteMod
|
|
||||||
| SortKind::NoteField
|
|
||||||
| SortKind::Notetype
|
|
||||||
| SortKind::NoteTags
|
|
||||||
| SortKind::CardTemplate => RequiredTable::CardsAndNotes,
|
|
||||||
SortKind::CardMod
|
|
||||||
| SortKind::CardReps
|
|
||||||
| SortKind::CardDue
|
|
||||||
| SortKind::CardEase
|
|
||||||
| SortKind::CardLapses
|
|
||||||
| SortKind::CardInterval
|
|
||||||
| SortKind::CardDeck => RequiredTable::Cards,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collection {
|
|
||||||
pub fn search_cards(&mut self, search: &str, mut mode: SortMode) -> Result<Vec<CardId>> {
|
|
||||||
let top_node = Node::Group(parse(search)?);
|
|
||||||
self.resolve_config_sort(&mut mode);
|
|
||||||
let writer = SqlWriter::new(self);
|
|
||||||
|
|
||||||
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
|
||||||
self.add_order(&mut sql, mode)?;
|
|
||||||
|
|
||||||
let mut stmt = self.storage.db.prepare(&sql)?;
|
|
||||||
let ids: Vec<_> = stmt
|
|
||||||
.query_map(&args, |row| row.get(0))?
|
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
|
||||||
|
|
||||||
Ok(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_order(&mut self, sql: &mut String, mode: SortMode) -> Result<()> {
|
|
||||||
match mode {
|
|
||||||
SortMode::NoOrder => (),
|
|
||||||
SortMode::FromConfig => unreachable!(),
|
|
||||||
SortMode::Builtin { kind, reverse } => {
|
|
||||||
prepare_sort(self, kind)?;
|
|
||||||
sql.push_str(" order by ");
|
|
||||||
write_order(sql, kind, reverse);
|
|
||||||
}
|
|
||||||
SortMode::Custom(order_clause) => {
|
|
||||||
sql.push_str(" order by ");
|
|
||||||
sql.push_str(&order_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Place the matched card ids into a temporary 'search_cids' table
|
|
||||||
/// instead of returning them. Use clear_searched_cards() to remove it.
|
|
||||||
/// Returns number of added cards.
|
|
||||||
pub(crate) fn search_cards_into_table(
|
|
||||||
&mut self,
|
|
||||||
search: &str,
|
|
||||||
mode: SortMode,
|
|
||||||
) -> Result<usize> {
|
|
||||||
let top_node = Node::Group(parse(search)?);
|
|
||||||
let writer = SqlWriter::new(self);
|
|
||||||
let want_order = mode != SortMode::NoOrder;
|
|
||||||
|
|
||||||
let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?;
|
|
||||||
self.add_order(&mut sql, mode)?;
|
|
||||||
|
|
||||||
if want_order {
|
|
||||||
self.storage
|
|
||||||
.setup_searched_cards_table_to_preserve_order()?;
|
|
||||||
} else {
|
|
||||||
self.storage.setup_searched_cards_table()?;
|
|
||||||
}
|
|
||||||
let sql = format!("insert into search_cids {}", sql);
|
|
||||||
|
|
||||||
self.storage
|
|
||||||
.db
|
|
||||||
.prepare(&sql)?
|
|
||||||
.execute(&args)
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the sort mode is based on a config setting, look it up.
|
|
||||||
fn resolve_config_sort(&self, mode: &mut SortMode) {
|
|
||||||
if mode == &SortMode::FromConfig {
|
|
||||||
*mode = SortMode::Builtin {
|
|
||||||
kind: self.get_browser_sort_kind(),
|
|
||||||
reverse: self.get_bool(BoolKey::BrowserSortBackwards),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the order clause to the sql.
|
|
||||||
fn write_order(sql: &mut String, kind: SortKind, reverse: bool) {
|
|
||||||
let tmp_str;
|
|
||||||
let order = match kind {
|
|
||||||
SortKind::NoteCreation => "n.id asc, c.ord asc",
|
|
||||||
SortKind::NoteMod => "n.mod asc, c.ord asc",
|
|
||||||
SortKind::NoteField => "n.sfld collate nocase asc, c.ord asc",
|
|
||||||
SortKind::CardMod => "c.mod asc",
|
|
||||||
SortKind::CardReps => "c.reps asc",
|
|
||||||
SortKind::CardDue => "c.type asc, c.due asc",
|
|
||||||
SortKind::CardEase => {
|
|
||||||
tmp_str = format!("c.type = {} asc, c.factor asc", CardType::New as i8);
|
|
||||||
&tmp_str
|
|
||||||
}
|
|
||||||
SortKind::CardLapses => "c.lapses asc",
|
|
||||||
SortKind::CardInterval => "c.ivl asc",
|
|
||||||
SortKind::NoteTags => "n.tags asc",
|
|
||||||
SortKind::CardDeck => "(select pos from sort_order where did = c.did) asc",
|
|
||||||
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc",
|
|
||||||
SortKind::CardTemplate => concat!(
|
|
||||||
"coalesce((select pos from sort_order where ntid = n.mid and ord = c.ord),",
|
|
||||||
// need to fall back on ord 0 for cloze cards
|
|
||||||
"(select pos from sort_order where ntid = n.mid and ord = 0)) asc"
|
|
||||||
),
|
|
||||||
};
|
|
||||||
if order.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if reverse {
|
|
||||||
sql.push_str(
|
|
||||||
&order
|
|
||||||
.to_ascii_lowercase()
|
|
||||||
.replace(" desc", "")
|
|
||||||
.replace(" asc", " desc"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
sql.push_str(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn needs_aux_sort_table(kind: SortKind) -> bool {
|
|
||||||
use SortKind::*;
|
|
||||||
matches!(kind, CardDeck | Notetype | CardTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
|
|
||||||
if !needs_aux_sort_table(kind) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
use SortKind::*;
|
|
||||||
let sql = match kind {
|
|
||||||
CardDeck => include_str!("deck_order.sql"),
|
|
||||||
Notetype => include_str!("notetype_order.sql"),
|
|
||||||
CardTemplate => include_str!("template_order.sql"),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
col.storage.db.execute_batch(sql)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,14 +1,278 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod cards;
|
|
||||||
mod notes;
|
|
||||||
mod parser;
|
mod parser;
|
||||||
mod sqlwriter;
|
mod sqlwriter;
|
||||||
pub(crate) mod writer;
|
pub(crate) mod writer;
|
||||||
|
|
||||||
pub use cards::SortMode;
|
|
||||||
pub use parser::{
|
pub use parser::{
|
||||||
parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind,
|
parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind,
|
||||||
};
|
};
|
||||||
pub use writer::{concatenate_searches, replace_search_node, write_nodes, BoolSeparator};
|
pub use writer::{concatenate_searches, replace_search_node, write_nodes, BoolSeparator};
|
||||||
|
|
||||||
|
use rusqlite::types::FromSql;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
card::CardId,
|
||||||
|
card::CardType,
|
||||||
|
collection::Collection,
|
||||||
|
config::{BoolKey, SortKind},
|
||||||
|
err::Result,
|
||||||
|
notes::NoteId,
|
||||||
|
prelude::AnkiError,
|
||||||
|
search::parser::parse,
|
||||||
|
};
|
||||||
|
use sqlwriter::{RequiredTable, SqlWriter};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum SearchItems {
|
||||||
|
Cards,
|
||||||
|
Notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum SortMode {
|
||||||
|
NoOrder,
|
||||||
|
FromConfig,
|
||||||
|
Builtin { kind: SortKind, reverse: bool },
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AsSearchItems {
|
||||||
|
fn as_search_items() -> SearchItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSearchItems for CardId {
|
||||||
|
fn as_search_items() -> SearchItems {
|
||||||
|
SearchItems::Cards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSearchItems for NoteId {
|
||||||
|
fn as_search_items() -> SearchItems {
|
||||||
|
SearchItems::Notes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchItems {
|
||||||
|
fn required_table(&self) -> RequiredTable {
|
||||||
|
match self {
|
||||||
|
SearchItems::Cards => RequiredTable::Cards,
|
||||||
|
SearchItems::Notes => RequiredTable::Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortMode {
|
||||||
|
fn required_table(&self) -> RequiredTable {
|
||||||
|
match self {
|
||||||
|
SortMode::NoOrder => RequiredTable::CardsOrNotes,
|
||||||
|
SortMode::FromConfig => unreachable!(),
|
||||||
|
SortMode::Builtin { kind, .. } => kind.required_table(),
|
||||||
|
SortMode::Custom(ref text) => {
|
||||||
|
if text.contains("n.") {
|
||||||
|
if text.contains("c.") {
|
||||||
|
RequiredTable::CardsAndNotes
|
||||||
|
} else {
|
||||||
|
RequiredTable::Notes
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RequiredTable::Cards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortKind {
|
||||||
|
fn required_table(self) -> RequiredTable {
|
||||||
|
match self {
|
||||||
|
SortKind::NoteCards
|
||||||
|
| SortKind::NoteCreation
|
||||||
|
| SortKind::NoteEase
|
||||||
|
| SortKind::NoteMod
|
||||||
|
| SortKind::NoteField
|
||||||
|
| SortKind::Notetype
|
||||||
|
| SortKind::NoteTags => RequiredTable::Notes,
|
||||||
|
SortKind::CardTemplate => RequiredTable::CardsAndNotes,
|
||||||
|
SortKind::CardMod
|
||||||
|
| SortKind::CardReps
|
||||||
|
| SortKind::CardDue
|
||||||
|
| SortKind::CardEase
|
||||||
|
| SortKind::CardLapses
|
||||||
|
| SortKind::CardInterval
|
||||||
|
| SortKind::CardDeck => RequiredTable::Cards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn search<T>(&mut self, search: &str, mut mode: SortMode) -> Result<Vec<T>>
|
||||||
|
where
|
||||||
|
T: FromSql + AsSearchItems,
|
||||||
|
{
|
||||||
|
let items = T::as_search_items();
|
||||||
|
let top_node = Node::Group(parse(search)?);
|
||||||
|
self.resolve_config_sort(items, &mut mode);
|
||||||
|
let writer = SqlWriter::new(self, items);
|
||||||
|
|
||||||
|
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
||||||
|
self.add_order(&mut sql, items, mode)?;
|
||||||
|
|
||||||
|
let mut stmt = self.storage.db.prepare(&sql)?;
|
||||||
|
let ids: Vec<_> = stmt
|
||||||
|
.query_map(&args, |row| row.get(0))?
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_cards(&mut self, search: &str, mode: SortMode) -> Result<Vec<CardId>> {
|
||||||
|
self.search(search, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_notes(&mut self, search: &str) -> Result<Vec<NoteId>> {
|
||||||
|
self.search(search, SortMode::NoOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_order(&mut self, sql: &mut String, items: SearchItems, mode: SortMode) -> Result<()> {
|
||||||
|
match mode {
|
||||||
|
SortMode::NoOrder => (),
|
||||||
|
SortMode::FromConfig => unreachable!(),
|
||||||
|
SortMode::Builtin { kind, reverse } => {
|
||||||
|
prepare_sort(self, kind)?;
|
||||||
|
sql.push_str(" order by ");
|
||||||
|
write_order(sql, items, kind, reverse)?;
|
||||||
|
}
|
||||||
|
SortMode::Custom(order_clause) => {
|
||||||
|
sql.push_str(" order by ");
|
||||||
|
sql.push_str(&order_clause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place the matched card ids into a temporary 'search_cids' table
|
||||||
|
/// instead of returning them. Use clear_searched_cards() to remove it.
|
||||||
|
/// Returns number of added cards.
|
||||||
|
pub(crate) fn search_cards_into_table(
|
||||||
|
&mut self,
|
||||||
|
search: &str,
|
||||||
|
mode: SortMode,
|
||||||
|
) -> Result<usize> {
|
||||||
|
let top_node = Node::Group(parse(search)?);
|
||||||
|
let writer = SqlWriter::new(self, SearchItems::Cards);
|
||||||
|
let want_order = mode != SortMode::NoOrder;
|
||||||
|
|
||||||
|
let (mut sql, args) = writer.build_query(&top_node, mode.required_table())?;
|
||||||
|
self.add_order(&mut sql, SearchItems::Cards, mode)?;
|
||||||
|
|
||||||
|
if want_order {
|
||||||
|
self.storage
|
||||||
|
.setup_searched_cards_table_to_preserve_order()?;
|
||||||
|
} else {
|
||||||
|
self.storage.setup_searched_cards_table()?;
|
||||||
|
}
|
||||||
|
let sql = format!("insert into search_cids {}", sql);
|
||||||
|
|
||||||
|
self.storage
|
||||||
|
.db
|
||||||
|
.prepare(&sql)?
|
||||||
|
.execute(&args)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the sort mode is based on a config setting, look it up.
|
||||||
|
fn resolve_config_sort(&self, items: SearchItems, mode: &mut SortMode) {
|
||||||
|
if mode == &SortMode::FromConfig {
|
||||||
|
*mode = match items {
|
||||||
|
SearchItems::Cards => SortMode::Builtin {
|
||||||
|
kind: self.get_browser_sort_kind(),
|
||||||
|
reverse: self.get_bool(BoolKey::BrowserSortBackwards),
|
||||||
|
},
|
||||||
|
SearchItems::Notes => SortMode::Builtin {
|
||||||
|
kind: self.get_browser_note_sort_kind(),
|
||||||
|
reverse: self.get_bool(BoolKey::BrowserNoteSortBackwards),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the order clause to the sql.
|
||||||
|
fn write_order(sql: &mut String, items: SearchItems, kind: SortKind, reverse: bool) -> Result<()> {
|
||||||
|
let order = match items {
|
||||||
|
SearchItems::Cards => card_order_from_sortkind(kind),
|
||||||
|
SearchItems::Notes => note_order_from_sortkind(kind),
|
||||||
|
};
|
||||||
|
if order.is_empty() {
|
||||||
|
return Err(AnkiError::InvalidInput {
|
||||||
|
info: format!("Can't sort {:?} by {:?}.", items, kind),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if reverse {
|
||||||
|
sql.push_str(
|
||||||
|
&order
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.replace(" desc", "")
|
||||||
|
.replace(" asc", " desc"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sql.push_str(&order);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
|
||||||
|
match kind {
|
||||||
|
SortKind::NoteCreation => "n.id asc, c.ord asc".into(),
|
||||||
|
SortKind::NoteMod => "n.mod asc, c.ord asc".into(),
|
||||||
|
SortKind::NoteField => "n.sfld collate nocase asc, c.ord asc".into(),
|
||||||
|
SortKind::CardMod => "c.mod asc".into(),
|
||||||
|
SortKind::CardReps => "c.reps asc".into(),
|
||||||
|
SortKind::CardDue => "c.type asc, c.due asc".into(),
|
||||||
|
SortKind::CardEase => format!("c.type = {} asc, c.factor asc", CardType::New as i8).into(),
|
||||||
|
SortKind::CardLapses => "c.lapses asc".into(),
|
||||||
|
SortKind::CardInterval => "c.ivl asc".into(),
|
||||||
|
SortKind::NoteTags => "n.tags asc".into(),
|
||||||
|
SortKind::CardDeck => "(select pos from sort_order where did = c.did) asc".into(),
|
||||||
|
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||||
|
SortKind::CardTemplate => concat!(
|
||||||
|
"coalesce((select pos from sort_order where ntid = n.mid and ord = c.ord),",
|
||||||
|
// need to fall back on ord 0 for cloze cards
|
||||||
|
"(select pos from sort_order where ntid = n.mid and ord = 0)) asc"
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
_ => "".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
|
||||||
|
match kind {
|
||||||
|
SortKind::NoteCards => "(select pos from sort_order where nid = n.id) asc".into(),
|
||||||
|
SortKind::NoteCreation => "n.id asc".into(),
|
||||||
|
SortKind::NoteEase => "(select pos from sort_order where nid = n.id) asc".into(),
|
||||||
|
SortKind::NoteMod => "n.mod asc".into(),
|
||||||
|
SortKind::NoteField => "n.sfld collate nocase asc".into(),
|
||||||
|
SortKind::NoteTags => "n.tags asc".into(),
|
||||||
|
SortKind::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||||
|
_ => "".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
|
||||||
|
use SortKind::*;
|
||||||
|
let sql = match kind {
|
||||||
|
CardDeck => include_str!("deck_order.sql"),
|
||||||
|
Notetype => include_str!("notetype_order.sql"),
|
||||||
|
CardTemplate => include_str!("template_order.sql"),
|
||||||
|
NoteCards => include_str!("note_cards_order.sql"),
|
||||||
|
NoteEase => include_str!("note_ease_order.sql"),
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
col.storage.db.execute_batch(sql)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
10
rslib/src/search/note_cards_order.sql
Normal file
10
rslib/src/search/note_cards_order.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
DROP TABLE IF EXISTS sort_order;
|
||||||
|
CREATE TEMPORARY TABLE sort_order (
|
||||||
|
pos integer PRIMARY KEY,
|
||||||
|
nid integer NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
INSERT INTO sort_order (nid)
|
||||||
|
SELECT nid
|
||||||
|
FROM cards
|
||||||
|
GROUP BY nid
|
||||||
|
ORDER BY COUNT(*);
|
11
rslib/src/search/note_ease_order.sql
Normal file
11
rslib/src/search/note_ease_order.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
DROP TABLE IF EXISTS sort_order;
|
||||||
|
CREATE TEMPORARY TABLE sort_order (
|
||||||
|
pos integer PRIMARY KEY,
|
||||||
|
nid integer NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
INSERT INTO sort_order (nid)
|
||||||
|
SELECT nid
|
||||||
|
FROM cards
|
||||||
|
WHERE type != 0
|
||||||
|
GROUP BY nid
|
||||||
|
ORDER BY AVG(factor);
|
@ -1,23 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
use super::{parser::Node, sqlwriter::SqlWriter};
|
|
||||||
use crate::collection::Collection;
|
|
||||||
use crate::err::Result;
|
|
||||||
use crate::notes::NoteId;
|
|
||||||
use crate::search::parser::parse;
|
|
||||||
|
|
||||||
impl Collection {
|
|
||||||
pub fn search_notes(&mut self, search: &str) -> Result<Vec<NoteId>> {
|
|
||||||
let top_node = Node::Group(parse(search)?);
|
|
||||||
let writer = SqlWriter::new(self);
|
|
||||||
let (sql, args) = writer.build_notes_query(&top_node)?;
|
|
||||||
|
|
||||||
let mut stmt = self.storage.db.prepare(&sql)?;
|
|
||||||
let ids: Vec<_> = stmt
|
|
||||||
.query_map(&args, |row| row.get(0))?
|
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
|
||||||
|
|
||||||
Ok(ids)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,10 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use super::parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind};
|
use super::{
|
||||||
|
parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind},
|
||||||
|
SearchItems,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
card::{CardQueue, CardType},
|
card::{CardQueue, CardType},
|
||||||
collection::Collection,
|
collection::Collection,
|
||||||
@ -22,55 +25,48 @@ use std::{borrow::Cow, fmt::Write};
|
|||||||
pub(crate) struct SqlWriter<'a> {
|
pub(crate) struct SqlWriter<'a> {
|
||||||
col: &'a mut Collection,
|
col: &'a mut Collection,
|
||||||
sql: String,
|
sql: String,
|
||||||
|
items: SearchItems,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
normalize_note_text: bool,
|
normalize_note_text: bool,
|
||||||
table: RequiredTable,
|
table: RequiredTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqlWriter<'_> {
|
impl SqlWriter<'_> {
|
||||||
pub(crate) fn new(col: &mut Collection) -> SqlWriter<'_> {
|
pub(crate) fn new(col: &mut Collection, items: SearchItems) -> SqlWriter<'_> {
|
||||||
let normalize_note_text = col.get_bool(BoolKey::NormalizeNoteText);
|
let normalize_note_text = col.get_bool(BoolKey::NormalizeNoteText);
|
||||||
let sql = String::new();
|
let sql = String::new();
|
||||||
let args = vec![];
|
let args = vec![];
|
||||||
SqlWriter {
|
SqlWriter {
|
||||||
col,
|
col,
|
||||||
sql,
|
sql,
|
||||||
|
items,
|
||||||
args,
|
args,
|
||||||
normalize_note_text,
|
normalize_note_text,
|
||||||
table: RequiredTable::CardsOrNotes,
|
table: items.required_table(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn build_cards_query(
|
pub(super) fn build_query(
|
||||||
mut self,
|
mut self,
|
||||||
node: &Node,
|
node: &Node,
|
||||||
table: RequiredTable,
|
table: RequiredTable,
|
||||||
) -> Result<(String, Vec<String>)> {
|
) -> Result<(String, Vec<String>)> {
|
||||||
self.table = table.combine(node.required_table());
|
self.table = self.table.combine(table.combine(node.required_table()));
|
||||||
self.write_cards_table_sql();
|
self.write_table_sql();
|
||||||
self.write_node_to_sql(&node)?;
|
self.write_node_to_sql(&node)?;
|
||||||
Ok((self.sql, self.args))
|
Ok((self.sql, self.args))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn build_notes_query(mut self, node: &Node) -> Result<(String, Vec<String>)> {
|
fn write_table_sql(&mut self) {
|
||||||
self.table = RequiredTable::Notes.combine(node.required_table());
|
|
||||||
self.write_notes_table_sql();
|
|
||||||
self.write_node_to_sql(&node)?;
|
|
||||||
Ok((self.sql, self.args))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_cards_table_sql(&mut self) {
|
|
||||||
let sql = match self.table {
|
let sql = match self.table {
|
||||||
RequiredTable::Cards => "select c.id from cards c where ",
|
RequiredTable::Cards => "select c.id from cards c where ",
|
||||||
_ => "select c.id from cards c, notes n where c.nid=n.id and ",
|
|
||||||
};
|
|
||||||
self.sql.push_str(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_notes_table_sql(&mut self) {
|
|
||||||
let sql = match self.table {
|
|
||||||
RequiredTable::Notes => "select n.id from notes n where ",
|
RequiredTable::Notes => "select n.id from notes n where ",
|
||||||
_ => "select distinct n.id from cards c, notes n where c.nid=n.id and ",
|
_ => match self.items {
|
||||||
|
SearchItems::Cards => "select c.id from cards c, notes n where c.nid=n.id and ",
|
||||||
|
SearchItems::Notes => {
|
||||||
|
"select distinct n.id from cards c, notes n where c.nid=n.id and "
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
self.sql.push_str(sql);
|
self.sql.push_str(sql);
|
||||||
}
|
}
|
||||||
@ -592,7 +588,7 @@ mod test {
|
|||||||
// shortcut
|
// shortcut
|
||||||
fn s(req: &mut Collection, search: &str) -> (String, Vec<String>) {
|
fn s(req: &mut Collection, search: &str) -> (String, Vec<String>) {
|
||||||
let node = Node::Group(parse(search).unwrap());
|
let node = Node::Group(parse(search).unwrap());
|
||||||
let mut writer = SqlWriter::new(req);
|
let mut writer = SqlWriter::new(req, SearchItems::Cards);
|
||||||
writer.table = RequiredTable::Notes.combine(node.required_table());
|
writer.table = RequiredTable::Notes.combine(node.required_table());
|
||||||
writer.write_node_to_sql(&node).unwrap();
|
writer.write_node_to_sql(&node).unwrap();
|
||||||
(writer.sql, writer.args)
|
(writer.sql, writer.args)
|
||||||
|
Loading…
Reference in New Issue
Block a user